---
title: Conventions
description: The rules that cross the AdCrunch APIs — ids, provider values, money, time, casing, paths, and pagination — including the places where the APIs do not agree yet.
---

This page states each rule as the API applies it today. Where the four APIs do not agree, the page says so.

## Ids

- **An advertiser id** is `acc_` and the account id of the provider, for example `acc_1203456789012345` for a Meta ad account and `acc_4829301756` for a Google Ads customer. An advertiser is an ad account that your organization connects.
- **An entity id** is the id that its provider gives, with no prefix: a campaign, an ad group, an ad, a creative, or an asset group. The same id can exist at two providers, or for two types at one provider. So an entity is unique for its `provider`, its `type`, and its `id` together.
- **A Google Ads ad** has a composite id, `{adGroupId}~{adId}`, because Google Ads keeps an ad on the link between the ad group and the ad.
- **A Skill, a Brand, a Persona, and a Campaign Plan** have a `slug`. The slug is the handle in the path, for example `/context/brands/acme`. A Persona belongs to one Brand, so its handle is the pair: `/context/brands/acme/personas/loyalists`.
- **An id that AdCrunch makes** has a prefix: `ast_` for an Asset, `doc_` for a Document, `lni_` for a Line Item, and `psn_` for a Persona.
- **A Mutation** has the `workflowId` that `POST /mutations` returns.

## Providers

The field `provider` takes the API value of a provider, for example `meta`, `tiktok`, or `gads`. Send the value exactly. `gads` is Google Ads, and no value is `google`. [What each provider supports](/connect/providers) lists the API value of each provider, and what each provider can do.

### Which providers an operation works with

Some operations work with some providers only. A creative comes from Meta, an asset group comes from Google Ads, and a change runs on Meta. Each of those operations starts with one line of badges:

**Available on:** [![Meta](/providers/meta.svg)](/connect/providers) [![TikTok](/providers/tiktok.svg)](/connect/providers)

The same operation carries those API values in `x-adcrunch-providers`, beside its description in the OpenAPI document. A code generator reads that field, and it reads no prose.

**An operation with no badges works the same way whatever your organization connects.** The mutation history, every Context operation, and every Asset operation except registration are of this kind.

The line names the providers and stops there. To read what each provider supports, and where it stops, use [the one table](/connect/providers).

## Money

Every API states money in **whole units** of its currency, as a number with decimals. `50` is 50.00, and `40.5` is 40.50.

| Where | Unit | 50.00 is |
| --- | --- | --- |
| Observe: `budget` on an entity | whole units | `50` |
| Observe: `spend` and each money metric on an Insight | whole units | `50` |
| Mutations: `dailyBudget`, `lifetimeBudget` | whole units | `50` |
| Context: each amount on a Campaign Plan and a Line Item | whole units | `50` |

A budget that you read from Observe goes back to `POST /mutations` with no conversion.

:::warning[No more decimals than the currency has]

An amount with more decimals than its currency has is refused, never rounded. `40.505` is refused in euros, and `50.5` is refused in yen.

A Mutation checks the table of its provider. Meta counts some currencies with no decimals, such as TWD, so `50.5` TWD is valid on a Campaign Plan and refused by `POST /mutations`.

:::

Each amount is in the currency of its advertiser or of its Campaign Plan. A `currency` field names that currency as an ISO 4217 code.

`GET /observe/insights` also takes a `currency` parameter. With it, AdCrunch converts each row at the European Central Bank rate of the date of that row. It converts the rows before it adds them. Without the parameter, a group that mixes two currencies answers `currency: null`.

## Time

- **A timestamp that AdCrunch writes** is a number: milliseconds since the Unix epoch, in UTC. `createdAt` and `updatedAt` are examples.
- **A calendar date** is a string, `YYYY-MM-DD`: `since` and `until` on Insights, and each date on a Campaign Plan.
- **A delivery time of Meta** is an ISO 8601 string: `startTime` and `endTime` when you create an ad set.

## Casing

The casing of a field says who named it.

- **A field that AdCrunch names** is camelCase: `advertiserId`, `entityId`, `dailyBudget`, `createdAt`.
- **A field that a provider names** keeps the casing of the provider. A metric key is one: `conversion_value`, `action_add_to_cart`. So an Insight row holds `advertiserId` beside `conversion_value`. The provider payload of an entity is another: a TikTok campaign answers `campaign_id`.

One API is an exception. **Context** takes snake_case in a request, and answers camelCase. You send `website_url`, and you read `websiteUrl`. The query parameter `base_revision` is snake_case too.

Send each field in the casing that its operation page shows.

## Paths

No published path ends in a slash: `/context/skills`, `/assets`, `/observe/campaigns`. A request to a path that ends in a slash goes to the same operation. `/context/skills/` answers as `/context/skills` does.

## Live read and stored read

An Observe operation reads from one of two sources, and its path decides which. No parameter changes the source.

| Path | Source |
| --- | --- |
| `GET /observe/campaigns`, and the same for `ad-groups`, `ads`, `creatives` and `asset-groups` | **Stored read**: what AdCrunch holds |
| `GET /observe/campaigns?advertiserId=acc_…` | **Stored read** of one ad account |
| `GET /observe/insights` | **Stored read** |
| `GET /observe/{advertiserId}/campaigns`, and the same for the other resources | **Live read**: what the provider holds now |
| `GET /observe/{advertiserId}/campaigns/{id}` | **Live read**: the payload of the provider, as it sends it |

Every operation page says "Live read" or "Stored read" first.

A **stored read** answers what the ingestion of AdCrunch stored. It is fast, it reads every ad account of your organization in one request, and it takes every filter, `status` included. A change that a person made in the interface of the provider can take up to one day to appear.

A **live read** asks the provider for one ad account, now. It is current, but each request counts against the rate limits of that provider, and it can fail when the provider fails. A live row has no `createdAt`, `updatedAt` or `deletedAt`, because AdCrunch stores nothing for it. A live read does not update what AdCrunch holds.

`status` is the status an entity is set to, not whether it delivers. It means the same thing on both reads: every row of the answer carries the status you asked for. A live read takes a status only where one request to the provider selects exactly the entities in that status. Any other status gets a `501`, and the stored read takes every status. The page of each live read lists the statuses each provider takes. A status that a provider never uses answers an empty list.

A provider that does not have a kind of entity answers an empty list on a live read. TikTok and Google Ads have no separate creative, and only Google Ads has asset groups. A provider that AdCrunch cannot read live answers `501`. [What each provider supports](/connect/providers) lists them.

:::warning[The source moved from a parameter to the path]

Before this rule, `GET /observe/{advertiserId}/campaigns` read the store unless the request sent `fresh=true`, and every answer carried `source`. Both are gone. A request that read one ad account from the store must now send `GET /observe/campaigns?advertiserId=acc_…`.

:::

## Pagination

Every list pages in one form: the lists of Observe (the entities and `GET /observe/insights`), `GET /mutations`, `GET /assets`, and the lists of Context.

| Field | Where | Meaning |
| --- | --- | --- |
| `limit` | query | The greatest number of rows on the page, from 1 to 500. The default is 100. A greater value gets a `400` with `error` of `invalid_request`. |
| `cursor` | query | The `nextCursor` of the previous answer. Omit it for the first page. |
| `data` | answer | The rows of the page. |
| `nextCursor` | answer | Present when more rows exist. The last page has no `nextCursor`. |

A list has no `total` and no `offset`. To read every row, send each `nextCursor` back as `cursor` until an answer has none:

```bash
curl "https://api.adcrunch.dev/observe/campaigns?limit=500" \
  -H "Authorization: Bearer $ADCRUNCH_API_KEY"
# Send the nextCursor of the answer back:
curl "https://api.adcrunch.dev/observe/campaigns?limit=500&cursor=eyJwIjpb..." \
  -H "Authorization: Bearer $ADCRUNCH_API_KEY"
```

A cursor is bound to its query. Send it with the same filters as the request that answered it, and do not build or change it. A cursor that AdCrunch cannot use gets a `400` with `invalid_cursor`. You can change `limit` from one page to the next.

A stored list puts the row that AdCrunch stored last first. So a row that arrives while you page does not move a page that you already read. `GET /observe/insights` orders its rows by the period, then the advertiser and the entity of the breakdown, then the provider.

A live read pages the same way. Its `nextCursor` carries the position of the provider, so send it back on the same path. A live cursor on a stored read, or a stored cursor on a live read, gets a `400` with `invalid_cursor`. At TikTok, every page keeps the page size of the first page.
