# API reference

Every endpoint, field and response of the export API. Generated from [openapi.yaml](/docs/openapi.yaml) (OpenAPI 3.1, also as [JSON](/docs/openapi.json)).

- **Base URL:** `https://global-api.attentionpanel.com`
- **Auth:** `Authorization: <api key>` on every request, no scheme prefix.
- **Body:** JSON in and out. Unknown request fields are rejected.
- **Errors:** `{ "statusCode": 400, "message": "FROM_DATE_NOT_ALLOWED" }`. The `message` is the code to branch on.
- **Two ways to export:** the change feed for mobile `v2` (`feed: true` for the first pull, then `since` with the last `next_cursor`), which keeps a copy of the export current; and the day export (`from_date`, `to_date`) for one-off pulls and desktop. [Integration →](/docs/integration)
- **Cursors:** treat `next_cursor` as an opaque string and pass it back unchanged. It does not expire.

## Versions

The mobile export has two versions. Desktop has none: leave `version` out.

| Version | Days | Status |
|---|---|---|
| `v2` | From 13 March 2026 in Sweden and Denmark, 26 March in Norway and 19 May in Finland | The current export, and the only one with the change feed. |
| `v1` | Up to 11 November 2026 | The legacy export, no longer updated. [Moving from the legacy mobile export →](/docs/legacy-mobile-export) |

- **Send `version` on every mobile request.** A version in the request always applies.
- **Without it,** a request gets the version set for your key: `v2`, unless your key was set up on the legacy export. A change-feed pull is always `v2`.
- **Outside a version's days:** a `from_date` before 13 March 2026 with `v2` is a 400 `V2_FROM_DATE_NOT_AVAILABLE`, with the first day in `available_from`. A `to_date` after 11 November 2026 with `v1` is a 400 `V1_NOT_AVAILABLE_AFTER`, with the last day in `available_to`.
- **Every job states its version:** `version` in [Get an export](/docs/api-reference#get-export).

## Create an export

`POST /impressions/export`

Starts an export job for one platform and market: a day export over an inclusive range of UTC dates (`from_date`, `to_date`), or, for mobile `v2`, the change feed. A feed starts with a first pull (`feed: true`), which returns every current impression and a cursor; each later pull passes the last cursor (`since`) and returns every impression added, changed or removed since. Upsert the rows on `impression_id` and treat `removed: true` as retired.

Request body:

| Field | Type | Description |
|---|---|---|
| `device` | string · required | The platform. One of `mobile`, `desktop`. |
| `market` | string · required | The market. One of `semarket`, `nomarket`, `dkmarket`, `fimarket`. |
| `from_date` | date | Day export: `YYYY-MM-DD`, inclusive. Not before the key's earliest allowed date. |
| `to_date` | date | Day export: `YYYY-MM-DD`, inclusive. Not before `from_date`. |
| `include_html_metadata` | boolean | Desktop only. Adds `html_metadata` when the key is entitled to it. |
| `version` | string | Mobile only. The mobile data version. When omitted: the version set for the key, else `v2`; a feed pull is always `v2`. `v2` data starts on a date set per deployment: an earlier `from_date` returns 400 `V2_FROM_DATE_NOT_AVAILABLE` with that date in `available_from`. Mobile `v1` data ends on 2026-11-11: a later `to_date` returns 400 `V1_NOT_AVAILABLE_AFTER` with that date in `available_to`. One of `v1`, `v2`. |
| `feed` | boolean | Change feed, mobile `v2`: the first pull. Returns every current impression from the key's earliest allowed date, and a cursor. Always `true`. |
| `since` | string | Change feed, mobile `v2`: the `next_cursor` of the last pull. Returns every impression added, changed or removed since. A cursor does not expire. |

Response 200:

| Field | Type | Description |
|---|---|---|
| `export_id` | string · required | Use in the other endpoints. URL-encode it in paths. |
| `status` | string · required | Always `RUNNING`. |
| `version` | string | The data version the job exports. One of `v1`, `v2`. |

Errors:

| Status · message | Cause |
|---|---|
| 400 (validation message) | Missing or unknown field, bad date, `from_date` after `to_date`, or neither dates nor `feed` or `since`. |
| 400 FROM_DATE_NOT_ALLOWED | `from_date` is before the key's earliest allowed date. |
| 400 MAX_PARALLEL_RUNS_LIMIT_REACHED | The key already has its maximum number of running jobs for this platform, market and version. |
| 400 V2_FROM_DATE_NOT_AVAILABLE | `version` `v2` with `from_date` before the first day of v2 data, which `available_from` gives (`YYYY-MM-DD`). |
| 400 V1_NOT_AVAILABLE_AFTER | Mobile `version` `v1` with `to_date` after the last day of v1 data, which `available_to` gives (`YYYY-MM-DD`); v1 is no longer built. Request `v2` for later days. `action` says the same. |
| 400 V2_ONLY_AVAILABLE_FOR_MOBILE | `version` `v2` with a device other than `mobile`. |
| 400 FEED_TAKES_NO_DATES | `feed` or `since` together with `from_date` or `to_date`. |
| 400 FEED_OR_SINCE | Both `feed` and `since`: a pull is either the first one or one since a cursor. |
| 400 FEED_MUST_BE_TRUE | `feed` other than `true`. |
| 400 FEED_ONLY_AVAILABLE_FOR_MOBILE | `feed` or `since` with a device other than `mobile`. |
| 400 FEED_ONLY_AVAILABLE_FOR_V2 | `feed` or `since` with a `version` other than `v2`. |
| 400 CURSOR_INVALID | `since` is not a cursor this API returned. |
| 400 CURSOR_GAP | Changes after `since` were lost on our side. Start a new feed with `feed: true`; impressions you hold that are not in it are retired. `action` says the same. |
| 401 | No `Authorization` header. |
| 403 | Invalid key, or platform or market outside its entitlements. |
| 500 EXPORT_VERSION_MISCONFIGURED | The default export version set for the key is not valid, and we have been alerted. A request that names its `version` is not affected. |

## Get an export

`GET /exports/{export_id}`

Returns the job's state, and the files once it has succeeded.

Response 200:

| Field | Type | Description |
|---|---|---|
| `status` | string · required | Only `RUNNING` changes. One of `RUNNING`, `SUCCEEDED`, `FAILED`, `CANCELLED`. |
| `version` | string | The data version the job exports. One of `v1`, `v2`. |
| `download_urls` | string[] | On `SUCCEEDED`. Presigned, one archive per calendar month in the range (for a feed pull, per month of the impressions it returns; none when nothing changed), named `<export_id>_<year>_<month>.tar.gz` and holding a `day=DD/` folder of gzipped JSON Lines per day. Valid 24 hours after completion. Send no `Authorization` header to them. |
| `failure_reason` | string | On `FAILED`: `NO_DATA` (no rows for the request), `CURSOR_INVALID` or `CURSOR_GAP` (as the 400 codes of a feed request, found while the job ran), or `INTERNAL_ERROR`. One of `NO_DATA`, `INTERNAL_ERROR`, `CURSOR_INVALID`, `CURSOR_GAP`. |
| `next_cursor` | string | On `SUCCEEDED`, for a change-feed pull: pass it as `since` in the next pull. |

Errors:

| Status · message | Cause |
|---|---|
| 400 EXPORT_EXPIRED | The download URLs are older than 24 hours. Create a new export. |
| 404 | Unknown `export_id`, or one created with another key. |

## Cancel an export

`POST /exports/{export_id}/cancel`

Stops a running job.

Response 200:

| Field | Type | Description |
|---|---|---|
| `export_id` | string · required | The cancelled job. |
| `status` | string · required | Always `CANCELLED`. |

Errors:

| Status · message | Cause |
|---|---|
| 400 EXPORT_NOT_IN_CANCELABLE_STATE | The job is no longer running. |
| 404 | Unknown `export_id`, or one created with another key. |
