Skip to content
AdCrunch
Esc
↑↓navigate↵open⌘Jpreview
On this page

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 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 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 TikTok

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. 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 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.

Was this page helpful?