<!-- https://developers.silktide.com/v1/analytics/acquisition/get-sources | Analytics / Acquisition -->

# List where visitors came from, one row per source and medium

`GET /v1/results/analytics/sources`

Lists where visitors came from: a search engine, a social network, an AI assistant such as ChatGPT, an email, or another website that linked to yours. It is the table on the Sources screen in Analytics, and takes the [Analytics arguments](https://developers.silktide.com/v1/analytics.md) and the [table arguments](https://developers.silktide.com/v1/concepts/tables.md).

A row is one source and one medium together, so the same source can appear on more than one row, once for each medium it arrived through. `syntheticKey` tells the rows apart; `source` and `medium` are the values to filter other queries by.

Direct is a fallback, not proof that someone typed the address: a visit whose referrer or campaign was not passed on is counted as direct too. Where a visitor came from is only known for visitors collected at the Cookie-free or Complete consent level.

`sourceName` is the name to show. Where an analytics administrator gave a source a name of their own, `sourceName` is that name and `isLabeled` is `true`; `source` stays the value Silktide recorded.

Rows are sorted by `visitors`, busiest first. `bounceRate`, `pagesPerSession` and `topEntryUrl` cannot be sorted by: a `sort` naming one of them answers with an error.

Send `goalId` to see which sources lead to a goal, and `previousPeriod=1` to compare each row with the period before.

## Request

### Query parameters

- `view` (string, optional, matches `^(all|[1-9]\d*)$`): The analytics view to read: its id, the number after `/analytics/` in its address in Silktide, or `all` for all of the account's analytics. Send this or `websiteId`.
- `websiteId` (integer, optional, at least 1): Instead of `view`, a website whose analytics to read. The website must have Analytics set up.
- `sectionId` (integer, optional): With `websiteId`, a section of the website. Defaults to `0`, the whole website.
- `period` (string, optional): The date range. Defaults to `all`. The spaced forms (`-7 days`, `month to date`) are older names that keep working. `custom` needs `from` and `to`. One of: `all`, `today`, `last-7-days`, `last-30-days`, `last-90-days`, `month-to-date`, `last-month`, `year-to-date`, `last-12-months`, `custom`, `-7 days`, `-30 days`, `-90 days`, `-1 month`, `month to date`, `year to date`, `-12 months`.
- `from` (string, optional): With `period=custom`, the first day, such as `2026-10-01`.
- `to` (string, optional): With `period=custom`, the last day.
- `timezone` (string, optional): The IANA time zone days are read in, such as `America/New_York`. Defaults to `UTC`.
- `filters` (map of strings, optional): Only visits matching every filter, keyed by what is filtered: `filters[url]=contains:/blog`. Each value is `{operator}:{value}`, with operator `is`, `not`, `contains`, `starts`, `gt` or `lt`. Keys include `url`, `hostName`, `entryUrl`, `exitUrl`, `event`, `outgoingUrl`, `referrer`, `source`, `medium`, `campaign`, `content`, `term`, `countryCode`, `regionCode`, `language`, `deviceType` (`desktop`, `tablet` or `mobile`), `operatingSystem` and `browser`.
- `columns` (string, optional): The columns to return, comma-separated, such as `url,pageViews`. Left out, the table's default columns. Send a string even in a JSON body: an array is ignored.
- `sort` (string, optional): The column to sort by, then `:a` for ascending or `:d` for descending, such as `pageViews:d`. Left out, the table's own order.
- `search` (string, optional): Only rows matching this text. What it searches depends on the table, most often the URL or title.
- `page` (integer, optional, at least 1): The page of rows to return, from `1`. Defaults to `1`.
- `rowsPerPage` (integer, optional, at least 1): How many rows a page holds. Defaults to `100`.
- `goalId` (integer, optional, at least 1): A goal, by the `goalId` Get goals gives it. Adds `goalCompletions` to every row. Left out, no goal.
- `previousPeriod` (string, optional): `1` to add the figures of the period before, the same length and ending where this one starts, as `previous…` fields. Left out or `0`, none. Needs a `period` with a start: with `all`, nothing is added. One of: `0`, `1`.

## Response

Status `200`.

- `result` (object, required): The answer.
  - `page` (integer, required, at least 1): The page returned. Not always the page asked for, when that was out of range.
  - `minPage` (integer, required, at least 1): The first page. Always `1`.
  - `maxPage` (integer, required): The last page at this `rowsPerPage`. With no rows, `0` or `1` depending on the table.
  - `numRows` (integer, required): How many rows there are across every page.
  - `rowsPerPage` (integer, required, at least 1): How many rows a page holds.
  - `rows` (list of objects, required): The rows on this page.
    - `source` (string, required): The source as recorded: a known name such as `Google`, a website's host such as `chatgpt.com`, or the link's `utm_source`. Empty for direct visits. Filter by it with `filters[source]`.
    - `medium` (string, required): The medium as recorded, such as `search`, `social`, `email`, `ai-assistant`, `referral`, `unknown` (other websites) or the link's `utm_medium`. Empty for direct visits. Filter by it with `filters[medium]`.
    - `sourceName` (string, required): The source's name, as Silktide shows it: the account's label for it where one is set, and `Direct` for direct visits.
    - `mediumName` (string, required): The medium's name, as Silktide shows it, such as `Search` or `Other websites`.
    - `isLabeled` (boolean, required): Whether `sourceName` is a label the account gave this source.
    - `sampleUrl` (string, required, can be null, uri): An address on the source's website. Null when there is none, as for direct visits.
    - `visitors` (integer, required): Unique visitors who arrived from it.
    - `pageViews` (integer, required): Pages those visitors viewed.
    - `bounceRate` (integer, required, can be null, 0 to 100): The share of its visits that viewed one page and left, as a whole percentage. Null when none of its visits started in the period.
    - `pagesPerSession` (number, required, can be null): Pages viewed per visit, to one decimal place. Null when none of its visits started in the period.
    - `topEntryUrl` (string, required): The page its visits most often began on. Empty when none of its visits started in the period.
    - `syntheticKey` (string, required): The row's id, made from its `source` and `medium` together. The same pair always has the same id, on every request.
    - `sourceFilterValue` (string, required): What to filter by to see only this source, as `filters[source]=is:{sourceFilterValue}`: its `source`, or `__empty__` for direct visits, whose `source` is empty.
    - `mediumFilterValue` (string, required): What to filter by to see only this medium, as `filters[medium]=is:{mediumFilterValue}`: its `medium`, or `__empty__` for direct visits.
    - `goalCompletions` (integer, optional): How many times its visitors completed the goal. Only with `goalId`.
    - `previousVisitors` (integer, optional): Unique visitors in the period before. Only with `previousPeriod=1` and a `period` other than `all`. `0` when it had none then.
    - `previousPageViews` (integer, optional): Page views in the period before. Only with `previousPeriod=1` and a `period` other than `all`. `0` when it had none then.
- `selector` (string, required): The query that answered, as its path: `/v1/results/scores`.
- `duration` (number, required): Seconds Silktide took to answer.

## Errors

| Status | Type | When |
| --- | --- | --- |
| 404 | `ModelNotFound` | No view or website with that id exists in the account, or the API key's user cannot see it. |
| 403 | `AccessDenied` | The API key's user cannot read that analytics view. |
| 200 | `ModelNotFound` | The website has no Analytics set up. The body is an error despite the status. |
| 200 | `InvalidRequest` | Neither `view` nor `websiteId` was sent. The error's own `status` is `400` and `properties.field` is `view`. The body is an error despite the status. |
| 200 | `InvalidArgument` | A filter names a key or operator Analytics does not know. The body is an error despite the status. |
| 200 | `InvalidArgument` | `period` is not one of its values, or is `custom` without both `from` and `to`. The body is an error despite the status. |
| 404 | `ModelNotFound` | `goalId` names no goal of the view. |
| 200 | - | `sort` names a column the table cannot sort by. Sort by `sourceName`, `mediumName`, `sampleUrl`, `visitors`, `pageViews` or `goalCompletions` only. The body is an error despite the status. |
| 401 | `NotLoggedIn` | No API key was sent, or it is not a key for this region. |
| 429 | `TooManyRequests` | Too many requests to this path in five seconds. Retry after the number of seconds in the `Retry-After` header, which `properties.retryAfter` repeats. |
| 503 | - | Silktide is busy. Retry after the number of seconds in the `Retry-After` header. |

## Access

- Permission: View analytics view (View all analytics for `view=all`)
- Plan feature: none
- Edition: Silktide 4 and Silktide 5
- Cost: free
- Stability: stable, since 1.0.0

## Example: The top sources over the last 30 days

```bash
curl "https://api.eu.silktide.com/v1/results/analytics/sources?view=all&period=last-30-days&rowsPerPage=3" \
  -H "Authorization: silktide token=$SILKTIDE_API_KEY"
```

Response:

```json
{
  "result": {
    "page": 1,
    "minPage": 1,
    "maxPage": 5,
    "numRows": 15,
    "rowsPerPage": 3,
    "rows": [
      {
        "source": "",
        "medium": "",
        "sourceName": "Direct",
        "mediumName": "Direct",
        "isLabeled": false,
        "sampleUrl": null,
        "visitors": 285,
        "pageViews": 742,
        "bounceRate": 9,
        "pagesPerSession": 2.5,
        "topEntryUrl": "https://fakewinebarrel.com/",
        "syntheticKey": "336d5ebc5436534e61d16e63ddfca327",
        "sourceFilterValue": "__empty__",
        "mediumFilterValue": "__empty__"
      },
      {
        "source": "Google",
        "medium": "search",
        "sourceName": "Google",
        "mediumName": "Search",
        "isLabeled": false,
        "sampleUrl": "https://www.google.com/",
        "visitors": 214,
        "pageViews": 556,
        "bounceRate": 8,
        "pagesPerSession": 2.5,
        "topEntryUrl": "https://fakewinebarrel.com/",
        "syntheticKey": "6b80fd09425cac18badc8834e40a836d",
        "sourceFilterValue": "Google",
        "mediumFilterValue": "search"
      },
      {
        "source": "chatgpt.com",
        "medium": "ai-assistant",
        "sourceName": "chatgpt.com",
        "mediumName": "AI assistant",
        "isLabeled": false,
        "sampleUrl": "https://chatgpt.com/",
        "visitors": 31,
        "pageViews": 78,
        "bounceRate": 10,
        "pagesPerSession": 2.5,
        "topEntryUrl": "https://fakewinebarrel.com/wines/",
        "syntheticKey": "5f1c8e0d2a7b4c3e9d6a1b0f8e2c7d4a",
        "sourceFilterValue": "chatgpt.com",
        "mediumFilterValue": "ai-assistant"
      }
    ]
  },
  "selector": "/v1/results/analytics/sources",
  "duration": 0.11
}
```

## Example: Social sources, with completions of a goal

```bash
curl "https://api.eu.silktide.com/v1/results/analytics/sources?view=all&period=last-90-days&filters=%7B%22medium%22%3A%22is%3Asocial%22%7D&goalId=1&rowsPerPage=2" \
  -H "Authorization: silktide token=$SILKTIDE_API_KEY"
```

Response:

```json
{
  "result": {
    "page": 1,
    "minPage": 1,
    "maxPage": 4,
    "numRows": 7,
    "rowsPerPage": 2,
    "rows": [
      {
        "source": "Facebook",
        "medium": "social",
        "sourceName": "Facebook",
        "mediumName": "Social",
        "isLabeled": false,
        "sampleUrl": "https://facebook.com/",
        "visitors": 73,
        "pageViews": 173,
        "bounceRate": 12,
        "pagesPerSession": 2.3,
        "topEntryUrl": "https://fakewinebarrel.com/",
        "syntheticKey": "fa341a11807b6cb1348fbf64980baa8b",
        "sourceFilterValue": "Facebook",
        "mediumFilterValue": "social",
        "goalCompletions": 2
      },
      {
        "source": "Youtube",
        "medium": "social",
        "sourceName": "YouTube",
        "mediumName": "Social",
        "isLabeled": false,
        "sampleUrl": "https://youtube.com/",
        "visitors": 46,
        "pageViews": 98,
        "bounceRate": 14,
        "pagesPerSession": 2.1,
        "topEntryUrl": "https://fakewinebarrel.com/",
        "syntheticKey": "22b252f0357bdaa3fb66020db6876026",
        "sourceFilterValue": "Youtube",
        "mediumFilterValue": "social",
        "goalCompletions": 1
      }
    ]
  },
  "selector": "/v1/results/analytics/sources",
  "duration": 0.15
}
```
