# Read API

> Read stats, goals, attribution, ad spend and short links with a read key. JSON over HTTPS, OpenAPI 3.1, CSV feeds and copyable curl examples.

Source: https://datacenter.so/docs/api
Section: Developers

The read API gives your own code, a client's dashboard or a spreadsheet the same numbers you see in Datacenter. It only reads. Nothing it does can change a site, a goal or a link.

The read API is on paid plans: Solo, Studio, Agency and Custom. It also powers the [MCP server](https://datacenter.so/docs/mcp) and [Google Sheets feeds](https://datacenter.so/docs/looker-studio).

## Make a key

1. Open **Settings**, then **API**.
2. Name the key, pick what it reads (**All sites**, or one site) and press **Make a key**.
3. Copy the key. It is shown once. Datacenter keeps only a hash of it.

A key starts with `st_rk_`. **All sites** covers sites you add later too. For a client, a script that runs somewhere else, or a shared sheet, make a key for one site. **Revoke** stops a key at once.

Read keys are not [Events API](https://datacenter.so/docs/events-api) keys. Events API keys (`st_sk_`) send events and cannot read. Read keys (`st_rk_`) read and cannot send.

## First request

```bash
export DATACENTER_KEY="st_rk_..."
curl https://datacenter.so/api/v1/sites \
  -H "Authorization: Bearer $DATACENTER_KEY"
```

```json
{
  "data": [
    {
      "id": "3f9a1c2e7b4d8a06",
      "name": "Demo shop",
      "domain": "shop.example.com",
      "client_label": null,
      "timezone": "America/New_York",
      "tracking_mode": "attribution",
      "created_at": "2026-09-14T16:02:11.000Z"
    }
  ],
  "pagination": { "page": 1, "limit": 100, "has_more": false, "next_page": null }
}
```

Every other call takes a site `id` from this list.

## Endpoints

All are `GET` under `https://datacenter.so`. The full schema is the [OpenAPI 3.1 document](https://datacenter.so/api/v1/openapi.json).

| Endpoint | What it returns |
|---|---|
| `/api/v1/sites` | Sites the key reads |
| `/api/v1/sites/{site_id}` | One site |
| `/api/v1/sites/{site_id}/stats` | Totals for a range, the span before it, and the change |
| `/api/v1/sites/{site_id}/timeseries` | The same totals per hour, day or month |
| `/api/v1/sites/{site_id}/breakdown/{dimension}` | Top sources, pages, campaigns, countries, devices or goals |
| `/api/v1/sites/{site_id}/goals` | Goals with conversions |
| `/api/v1/sites/{site_id}/attribution` | Conversions and revenue credited by source, campaign, ad, platform or short link |
| `/api/v1/sites/{site_id}/ads` | Ad spend, credited revenue and ROAS |
| `/api/v1/links` | Short links with clicks |
| `/api/v1/links/{link_id}` | One link: clicks per day and where they came from |

### Totals

```bash
curl "https://datacenter.so/api/v1/sites/3f9a1c2e7b4d8a06/stats?period=7d" \
  -H "Authorization: Bearer $DATACENTER_KEY"
```

```json
{
  "data": {
    "site_id": "3f9a1c2e7b4d8a06",
    "range": { "period": "7d", "date_from": "2026-10-17", "date_to": "2026-10-23", "timezone": "America/New_York", "from": "2026-10-17T00:00:00-04:00", "to": "2026-10-23T11:00:00-04:00" },
    "previous_range": { "compare": "previous", "match_weekdays": false, "from": "2026-10-10T00:00:00-04:00", "to": "2026-10-16T11:00:00-04:00" },
    "filters": {},
    "metrics": { "visitors": 1840, "visits": 2210, "pageviews": 5120, "views_per_visit": 2.32, "bounce_rate": 41, "visit_duration": 96, "conversions": 63, "conversion_rate": 2.9 },
    "previous": { "visitors": 1702, "visits": 2050, "pageviews": 4790, "views_per_visit": 2.34, "bounce_rate": 43, "visit_duration": 91, "conversions": 55, "conversion_rate": 2.7 },
    "change_pct": { "visitors": 8.1, "visits": 7.8, "pageviews": 6.9, "views_per_visit": -0.9, "bounce_rate": -4.7, "visit_duration": 5.5, "conversions": 14.5, "conversion_rate": 7.4 }
  }
}
```

The numbers above are an example. `bounce_rate` and `conversion_rate` are percents. `visit_duration` is seconds per visit. `conversions` counts visits that reached any goal. `previous_range` is the span the change is measured against. By default it is the same number of days just before, ending at the same time of day, so a part day is compared with a part day. This month, quarter or year compares with the same days of the one before: October 1 to 23 against September 1 to 23. Add `compare=year` for the same dates a year back, and `match_weekdays=true` with it to go back 364 days so a Monday lines up with a Monday. Add `compare=false` to skip it.

### Per day

```bash
curl "https://datacenter.so/api/v1/sites/3f9a1c2e7b4d8a06/timeseries?period=30d&interval=day" \
  -H "Authorization: Bearer $DATACENTER_KEY"
```

`interval` is `day` (the default), `hour` (up to 7 days) or `month`. Every bucket is in the list, empty ones too. `date` is the local day (`2026-10-01`), hour (`2026-10-01 14:00`) or month (`2026-10`).

### Breakdowns

```bash
curl "https://datacenter.so/api/v1/sites/3f9a1c2e7b4d8a06/breakdown/source?period=30d&limit=10" \
  -H "Authorization: Bearer $DATACENTER_KEY"
```

Dimensions: `source`, `referrer`, `utm_source`, `utm_medium`, `utm_campaign` (or `campaign`), `utm_content`, `utm_term`, `page`, `entry_page`, `country`, `device`, `browser`, `os` and `goal`. Rows come most visitors first, each with visitors, visits, pageviews, bounce rate, visit duration, conversions and conversion rate. A blank source is `Direct / None`.

### Goals

```bash
curl "https://datacenter.so/api/v1/sites/3f9a1c2e7b4d8a06/goals?period=mtd" \
  -H "Authorization: Bearer $DATACENTER_KEY"
```

Each goal has `conversions` (visits that reached it), `events` (every match) and `conversion_rate` (percent of visits).

### Attribution

```bash
curl "https://datacenter.so/api/v1/sites/3f9a1c2e7b4d8a06/attribution?period=30d&by=campaign&model=last" \
  -H "Authorization: Bearer $DATACENTER_KEY"
```

This is the [attribution report](https://datacenter.so/docs/attribution-report). `by` is `source`, `campaign`, `content` (the ad), `term` (the ad set or keyword), `platform` or `link`. `model` is `last`, `first`, `linear`, `position`, `decay` or `any`. Without it, the site's own model from its Attribution tab applies, and the answer's `model` says which. `goal_id` counts one goal. Revenue is per currency, such as `{"USD": 1240.5}`, and never converted.

### Ad spend and ROAS

```bash
curl "https://datacenter.so/api/v1/sites/3f9a1c2e7b4d8a06/ads?period=30d&level=campaign" \
  -H "Authorization: Bearer $DATACENTER_KEY"
```

From connected ad platforms and [spend files](https://datacenter.so/docs/csv-spend-import). `level` is `platform`, `campaign`, `adset` or `ad`. `revenue` is what Datacenter credits to the ads, last touch. `platform_reported` is what the platform itself counts. `roas` is null when there is no spend or when spend and revenue are in different currencies. `connected` is `false` when the site has no ad spend.

### Short links

```bash
curl "https://datacenter.so/api/v1/links?period=30d" \
  -H "Authorization: Bearer $DATACENTER_KEY"
curl "https://datacenter.so/api/v1/links/LINK_ID?period=30d" \
  -H "Authorization: Bearer $DATACENTER_KEY"
```

A key for one site lists the links tied to that site. Add `site_id` to an all-sites key to do the same. `status` is `active` (the default), `archived` or `all`.

## Dates and time zones

Send a `period`, or `date_from` and `date_to` as `YYYY-MM-DD`. Both days are included.

| `period` | Covers |
|---|---|
| `today` | Today, up to now |
| `yesterday` | Yesterday |
| `7d`, `30d`, `90d` | The last 7, 30 or 90 days, today included (default `30d`) |
| `12m` | The last 365 days |
| `mtd` | This month, up to now |
| `lastmonth` | Last calendar month |
| `quarter` | This quarter, up to now |
| `lastquarter` | Last calendar quarter |
| `year` | This year, up to now |

Days start at local midnight in one time zone: the `timezone` you send (an IANA name such as `Europe/Berlin`), else the site's, else UTC. Every answer repeats the range with ISO 8601 times and their offset, so you never have to guess. Days and months in a time series use the zone's offset at the start of the range.

Short link clicks are kept in whole UTC days, so link calls are always UTC.

A range can be up to 731 days, and only as far back as your plan keeps stats.

## Filters

Add `filter=dimension:value`. Repeat it to combine, up to 8.

```bash
curl -G "https://datacenter.so/api/v1/sites/3f9a1c2e7b4d8a06/stats" \
  --data-urlencode "period=30d" \
  --data-urlencode "filter=source:Instagram" \
  --data-urlencode "filter=device:Mobile" \
  -H "Authorization: Bearer $DATACENTER_KEY"
```

Filters work on stats, time series, breakdowns and goals. A `goal` filter takes a goal id from the goals call.

## Pages of results

Lists take `limit` (1 to 1,000, default 100) and `page` (from 1). The answer says if there is more:

```json
"pagination": { "page": 1, "limit": 100, "has_more": true, "next_page": 2 }
```

Lists go 1,000 rows deep.

## CSV feeds

Add `format=csv` to any list for a CSV file with every row. For tools that cannot send headers, such as Google Sheets, the key can ride in the address, for CSV feeds only:

```text
https://datacenter.so/api/v1/sites/3f9a1c2e7b4d8a06/timeseries?period=90d&format=csv&key=st_rk_...
```

Anyone with that address can read what the key reads. Use a key for one site, and revoke it if the address leaks. See [Google Sheets and Looker Studio](https://datacenter.so/docs/looker-studio).

## Errors

Errors have a status code and the same shape:

```json
{ "error": { "code": "invalid_parameter", "message": "date_from must be a date as YYYY-MM-DD.", "field": "date_from" } }
```

| Status | Code | Means |
|---|---|---|
| 400 | `invalid_parameter`, `range_too_long`, `key_in_url` | A parameter is wrong. `field` names it |
| 401 | `unauthorized` | No key, or the key was revoked |
| 403 | `plan` | The account's plan does not include the read API |
| 403 | `history_limit` | The range starts before the plan keeps stats |
| 404 | `not_found` | No such site, link or endpoint for this key |
| 429 | `rate_limited` | Over the rate limit. Wait `Retry-After` seconds |
| 502, 503 | `upstream_error`, `stats_unavailable` | Stats could not be read. Try again in a minute |

A site or link another key or account owns reads as `not_found`, never as forbidden.

## Rate limit

Each key can make 120 requests a minute, across the read API, CSV feeds and the MCP server. Every answer has `X-RateLimit-Limit` and `X-RateLimit-Remaining`.

## Safety

- Keep keys on a server or in a password manager. The API sends no CORS headers, so a web page cannot call it.
- Each key reads only its sites. Revoke it in **Settings**, **API** and it stops at once.
- Answers never include visitor emails, names or phone numbers.
