Conventions
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 exampleacc_1203456789012345for a Meta ad account andacc_4829301756for 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, itstype, and itsidtogether. - 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, andpsn_for a Persona. - A Mutation has the
workflowIdthatPOST /mutationsreturns.
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 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:
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.
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.
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.
createdAtandupdatedAtare examples. - A calendar date is a string,
YYYY-MM-DD:sinceanduntilon Insights, and each date on a Campaign Plan. - A delivery time of Meta is an ISO 8601 string:
startTimeandendTimewhen 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 holdsadvertiserIdbesideconversion_value. The provider payload of an entity is another: a TikTok campaign answerscampaign_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 lists them.
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:
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.