{"components":{"securitySchemes":{"bearerAuth":{"description":"Send `Authorization: Bearer <credential>`.\n\nUse an **API key** (`acr_…`), from the AdCrunch console under Settings → API keys.\n\nThe credential names the organization, and no operation takes an organization parameter.\n\nSee https://docs.adcrunch.dev/api/authentication.","scheme":"bearer","type":"http"}}},"info":{"contact":{"email":"tech@adcrunch.dev"},"description":"Every AdCrunch API, in one document. Each API also publishes its own, at `/openapi/<name>.json`.\n\nOne origin serves the four, and the first segment of a path names the API. `/observe` reads the campaigns, the ads and the Insights of the ad accounts your organization connects. `/mutations` changes what runs on them. `/context` holds the Skills, Brands, Personas, Documents and Campaign Plans an organization writes down for its agents. `/assets` holds your images and videos, and places them in the library of an ad account.\n\nOne credential works on the four — see the security scheme.\n\nThe guide explains what these operations share: authentication, the failure shape, and the conventions for ids, money, time and pagination. It also describes three flows that span several operations: a change, an upload, and an edit that does not overwrite.\n\nStart at https://docs.adcrunch.dev/api/introduction.","license":{"name":"Proprietary","url":"https://adcrunch.dev"},"title":"AdCrunch API","version":"1.0.0"},"openapi":"3.0.3","paths":{"/observe/campaigns":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers) [![OpenAI](/providers/openai.svg)](https://docs.adcrunch.dev/connect/providers)\n\nLists the campaigns of every ad account your organization connects, across the providers, newest first.\n\nA campaign is a `campaign` at every provider, so this reads them all in one call. Send `provider` to read one platform.\n\n## Paging\n\n`limit` counts rows of the answer itself, not rows of each advertiser, so a `limit` of 10 answers at most 10 rows however many advertisers you own. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page. The row that AdCrunch stored last comes first, so a campaign that arrives while you page does not move a page you already read.\n\n## Where a campaign comes from\n\nThis is a **stored read**: it answers what AdCrunch holds, which its ingestion refreshes. A campaign a person changed in the provider’s own interface a moment ago may not be here yet — name one advertiser in the path, `/observe/{advertiserId}/campaigns`, to read the provider itself.\n\n## Budget and objective\n\nEvery row carries its `budget`, in **whole units** of its `currency` — the unit of `spend` on an Insight — with `budgetType` and `budgetLevel`. `budgetLevel` says whether the budget lives on the campaign or on its ad groups; when it is `ad_group`, read `/observe/ad-groups` for the amounts. `objective` is the provider’s own word, and it is null on a Google Ads campaign, which has none.\n\nA row carries the fields AdCrunch normalizes. For the provider’s own payload, read one campaign by id.","x-adcrunch-providers":["meta","tiktok","gads","openai"],"operationId":"listCampaigns","responses":{"200":{"content":{"application/json":{"example":{"data":[{"adsetBudgetSharing":null,"advertiserId":"acc_1203456789012345","bidStrategy":null,"budget":250,"budgetLevel":"campaign","budgetType":"daily","createdAt":1789610000000,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"id":"120210000000000","name":"Spring sale — prospecting","objective":"OUTCOME_SALES","path":"120210000000000","provider":"meta","status":"ACTIVE","type":"campaign","updatedAt":1790000000000,"updatedTime":1789900000000}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwibWV0YSIsImNhbXBhaWduIiwiMTIwMjEwMDAwMDAwMDAwIl0sInEiOiIxaDV4cDRhcnZ4bSJ9"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"adsetBudgetSharing":{"nullable":true,"description":"Whether the ad sets of this campaign share their daily budgets: Meta ad set budget sharing. Meta refuses a change of bid strategy while it is on. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads.","type":"boolean"},"bidStrategy":{"nullable":true,"description":"How the provider bids for this entity, in the provider’s word: Meta `LOWEST_COST_WITHOUT_CAP` (Highest volume), `LOWEST_COST_WITH_BID_CAP` (Bid cap), `COST_CAP` (Cost per result goal) or `LOWEST_COST_WITH_MIN_ROAS` (ROAS goal). The strategy sits where the budget sits: on the campaign when the campaign carries the budget, otherwise on each ad set. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose bid AdCrunch does not read yet.","type":"string"},"budget":{"nullable":true,"description":"The budget this entity carries itself, in **whole units** of `currency`: `50` is 50.00, never 50 cents. It is the unit of `spend` on an Insight, so a budget and a spend compare directly. AdCrunch converts each provider’s own unit — Meta’s minor unit, TikTok’s whole units and Google Ads’ micros — so every provider answers the same unit here.\n\nNull when the entity carries no budget of its own: `budgetLevel` then says where its budget lives. A campaign answers a budget only when its `budgetLevel` is `campaign`, so a budget always paces delivery at the level that answers it. Null also when AdCrunch does not know the account currency yet, because nobody can read a budget with no currency.\n\nAt Google Ads one budget may be shared by several campaigns, and each of them answers the whole shared amount.\n\n**A stored budget can be older than the rest of the row.** Meta does not mark a campaign changed when only its budget changes, and AdCrunch does not yet follow a Google Ads budget change, so the ingestion can miss a budget edited in the provider’s own interface. A live read answers the budget the provider holds now.\n\nAdCrunch takes a budget in the same whole units when it writes one, so a budget you read here goes back with no conversion.","type":"number"},"budgetLevel":{"nullable":true,"description":"Where the budget of this entity’s campaign lives. `campaign` when the campaign carries it — Meta Advantage campaign budget, TikTok Campaign Budget Optimization, and every Google Ads campaign. `ad_group` when each ad group carries its own. Null on a TikTok campaign whose payload does not say.","type":"string","enum":["campaign","ad_group"]},"budgetType":{"nullable":true,"description":"`daily` for a budget spent each day, `lifetime` for one spent over the whole life of the entity. Null exactly when `budget` has no provider value to describe.","type":"string","enum":["daily","lifetime"]},"objective":{"nullable":true,"description":"The campaign’s objective, kept as the provider writes it: Meta `OUTCOME_SALES`, TikTok `TRAFFIC`. It is the provider’s word, for the same reason `type` is. Null on every Google Ads campaign, because Google Ads has no objective.","type":"string"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `campaign` at every provider. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","adsetBudgetSharing","bidStrategy","budget","budgetLevel","budgetType","objective","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"adsetBudgetSharing":null,"advertiserId":"acc_1203456789012345","bidStrategy":null,"budget":250,"budgetLevel":"campaign","budgetType":"daily","createdAt":1789610000000,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"id":"120210000000000","name":"Spring sale — prospecting","objective":"OUTCOME_SALES","path":"120210000000000","provider":"meta","status":"ACTIVE","type":"campaign","updatedAt":1790000000000,"updatedTime":1789900000000}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwibWV0YSIsImNhbXBhaWduIiwiMTIwMjEwMDAwMDAwMDAwIl0sInEiOiIxaDV4cDRhcnZ4bSJ9"}}}},"description":"The matching campaigns, and how many there are in all."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."}},"summary":"Stored read: List campaigns","tags":["Campaigns"],"parameters":[{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"advertiserId","in":"query","required":false,"schema":{"description":"Read one ad account from what AdCrunch holds, prefixed `acc_`. One your organization does not own answers an empty list rather than a failure. To ask the provider instead, put the advertiser in the path.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},{"name":"provider","in":"query","required":false,"schema":{"description":"Read one provider. Omit it to read every provider your organization connects.","anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]},{"type":"string","enum":["x"]},{"type":"string","enum":["openai"]}]}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers. TikTok `ENABLE` and `DISABLE` are read here as `ACTIVE` and `PAUSED`.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}}]}},"/observe/{advertiserId}/campaigns":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers) [![OpenAI](/providers/openai.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for the campaigns of one ad account, now. This is a **live read**: the path names the advertiser, so the answer is what the provider holds, not what AdCrunch holds.\n\nAn advertiser that your organization does not own answers `404`, with `error` of `not_found`. An advertiser that does not exist answers the same. The answer does not tell the two apart.\n\nA live row carries no `createdAt`, `updatedAt` or `deletedAt`, because AdCrunch stores nothing for it. Each request counts against the rate limits of the provider.\n\nFor the stored rows of the same advertiser, read `/observe/campaigns?advertiserId=…`. That path takes every `status`, and the Status section below lists the statuses this one takes.\n\n`limit` and `cursor` page the answer. Send `nextCursor` back as `cursor` to get the next page. A live cursor is good only for a live read. At TikTok, every page keeps the page size of the first.\n\n## Status\n\n`status` selects the status an entity is set to, not whether it delivers, and every row of the answer carries it. A provider takes a status only where one request to it selects exactly the entities in that status. Any other status answers `501`.\n\nThe stored read, `/observe/campaigns?advertiserId=…&status=…`, takes every status. A status that a provider never uses answers an empty list.\n\n| Provider | Takes `status` |\n| --- | --- |\n| Meta | none yet |\n| TikTok | `DELETED`, `ARCHIVED` |\n| Google Ads | `ACTIVE`, `PAUSED`, `DELETED`, `ARCHIVED` |\n| OpenAI | `DELETED` |","x-adcrunch-providers":["meta","tiktok","gads","openai"],"operationId":"listAdvertiserCampaigns","responses":{"200":{"content":{"application/json":{"example":{"data":[{"adsetBudgetSharing":null,"advertiserId":"acc_1203456789012345","bidStrategy":null,"budget":250,"budgetLevel":"campaign","budgetType":"daily","createdAt":null,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"id":"120210000000000","name":"Spring sale — prospecting","objective":"OUTCOME_SALES","path":"120210000000000","provider":"meta","status":"ACTIVE","type":"campaign","updatedAt":null,"updatedTime":1789900000000}],"nextCursor":"eyJwIjp7ImFmdGVyIjoiUVZGSVVteFpBVmhSUTJ0QlVXdFIiLCJwcm92aWRlciI6Im1ldGEifSwicSI6IjF6ZnVpaHR5aGJiIiwibCI6dHJ1ZX0"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"adsetBudgetSharing":{"nullable":true,"description":"Whether the ad sets of this campaign share their daily budgets: Meta ad set budget sharing. Meta refuses a change of bid strategy while it is on. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads.","type":"boolean"},"bidStrategy":{"nullable":true,"description":"How the provider bids for this entity, in the provider’s word: Meta `LOWEST_COST_WITHOUT_CAP` (Highest volume), `LOWEST_COST_WITH_BID_CAP` (Bid cap), `COST_CAP` (Cost per result goal) or `LOWEST_COST_WITH_MIN_ROAS` (ROAS goal). The strategy sits where the budget sits: on the campaign when the campaign carries the budget, otherwise on each ad set. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose bid AdCrunch does not read yet.","type":"string"},"budget":{"nullable":true,"description":"The budget this entity carries itself, in **whole units** of `currency`: `50` is 50.00, never 50 cents. It is the unit of `spend` on an Insight, so a budget and a spend compare directly. AdCrunch converts each provider’s own unit — Meta’s minor unit, TikTok’s whole units and Google Ads’ micros — so every provider answers the same unit here.\n\nNull when the entity carries no budget of its own: `budgetLevel` then says where its budget lives. A campaign answers a budget only when its `budgetLevel` is `campaign`, so a budget always paces delivery at the level that answers it. Null also when AdCrunch does not know the account currency yet, because nobody can read a budget with no currency.\n\nAt Google Ads one budget may be shared by several campaigns, and each of them answers the whole shared amount.\n\n**A stored budget can be older than the rest of the row.** Meta does not mark a campaign changed when only its budget changes, and AdCrunch does not yet follow a Google Ads budget change, so the ingestion can miss a budget edited in the provider’s own interface. A live read answers the budget the provider holds now.\n\nAdCrunch takes a budget in the same whole units when it writes one, so a budget you read here goes back with no conversion.","type":"number"},"budgetLevel":{"nullable":true,"description":"Where the budget of this entity’s campaign lives. `campaign` when the campaign carries it — Meta Advantage campaign budget, TikTok Campaign Budget Optimization, and every Google Ads campaign. `ad_group` when each ad group carries its own. Null on a TikTok campaign whose payload does not say.","type":"string","enum":["campaign","ad_group"]},"budgetType":{"nullable":true,"description":"`daily` for a budget spent each day, `lifetime` for one spent over the whole life of the entity. Null exactly when `budget` has no provider value to describe.","type":"string","enum":["daily","lifetime"]},"objective":{"nullable":true,"description":"The campaign’s objective, kept as the provider writes it: Meta `OUTCOME_SALES`, TikTok `TRAFFIC`. It is the provider’s word, for the same reason `type` is. Null on every Google Ads campaign, because Google Ads has no objective.","type":"string"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `campaign` at every provider. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","adsetBudgetSharing","bidStrategy","budget","budgetLevel","budgetType","objective","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"adsetBudgetSharing":null,"advertiserId":"acc_1203456789012345","bidStrategy":null,"budget":250,"budgetLevel":"campaign","budgetType":"daily","createdAt":null,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"id":"120210000000000","name":"Spring sale — prospecting","objective":"OUTCOME_SALES","path":"120210000000000","provider":"meta","status":"ACTIVE","type":"campaign","updatedAt":null,"updatedTime":1789900000000}],"nextCursor":"eyJwIjp7ImFmdGVyIjoiUVZGSVVteFpBVmhSUTJ0QlVXdFIiLCJwcm92aWRlciI6Im1ldGEifSwicSI6IjF6ZnVpaHR5aGJiIiwibCI6dHJ1ZX0"}}}},"description":"The matching campaigns of this advertiser."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"Your organization does not own the advertiser in the path, or it does not exist. The answer does not tell the two apart. `error` is `not_found`. AdCrunch asks the provider nothing."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: List an advertiser’s campaigns","tags":["Campaigns"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers: the status the entity is set to, not whether it delivers. Every row of the answer carries this status. A provider that cannot select this status exactly answers `501`; the description of this operation lists what each provider selects.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}}]}},"/observe/{advertiserId}/campaigns/{id}":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers) [![OpenAI](/providers/openai.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for one campaign now, and gives its payload with the field names of the provider. This is the escape hatch from the normalized fields.\n\nThe keys are the provider’s. A Meta campaign answers `id`, `name`, `objective`, `daily_budget`, `lifetime_budget`, `bid_strategy` and `is_adset_budget_sharing_enabled`; a TikTok campaign answers `campaign_id`, `campaign_name`, `objective_type`, `budget_mode` and `budget_optimize_on`; a Google Ads campaign answers `id`, `name`, `status`, `budgetAmountMicros` and `budgetTotalAmountMicros`.\n\n**The payload holds the fields AdCrunch asks the provider for, and no more.** It is not everything the provider knows about the campaign: a bid strategy, a spending limit and a schedule are outside the set AdCrunch reads, so they are in no payload here.\n\n## Money\n\nAdCrunch changes one thing in the payload: the money of Meta, `daily_budget` and `lifetime_budget`. Meta writes each amount in a unit that depends on the currency. This read gives each one as a number in **whole units** of the currency of the advertiser. It is the unit of `budget` on a row: `250` is 250.00. When AdCrunch does not know the currency of the advertiser, each of these fields is `null`.\n\nA TikTok amount is in whole units already, so it does not change. A Google Ads field whose name ends in `Micros` states its unit, so it stays in micros: `12340000` is 12.34.\n\n## When there is nothing to give\n\n**`404`** — the provider holds no such campaign on this ad account, the provider has no campaign at all, or your organization does not own the advertiser in the path. They are deliberately not told apart: distinguishing them would confirm that a campaign exists to somebody who cannot read it.","x-adcrunch-providers":["meta","tiktok","gads","openai"],"operationId":"getCampaign","responses":{"200":{"content":{"application/json":{"example":{"account_id":"1203456789012345","bid_strategy":"COST_CAP","created_time":"2026-09-16T23:06:40+0000","daily_budget":250,"id":"120210000000000","is_adset_budget_sharing_enabled":false,"lifetime_budget":0,"name":"Spring sale — prospecting","objective":"OUTCOME_SALES","start_time":"2026-09-17T00:00:00+0000","status":"ACTIVE","updated_time":"2026-09-20T10:26:40+0000"},"schema":{"description":"The provider’s own payload for this entity. The keys are the provider’s: a TikTok campaign answers `campaign_id`, `campaign_name`, `objective_type`, `budget_mode` and the rest of TikTok’s vocabulary. AdCrunch changes only the money of Meta: `bid_amount`, `daily_budget` and `lifetime_budget` are numbers in whole units of the currency of the advertiser. Read the resource listing instead when you want the normalized fields.","example":{"account_id":"1203456789012345","bid_strategy":"COST_CAP","created_time":"2026-09-16T23:06:40+0000","daily_budget":250,"id":"120210000000000","is_adset_budget_sharing_enabled":false,"lifetime_budget":0,"name":"Spring sale — prospecting","objective":"OUTCOME_SALES","start_time":"2026-09-17T00:00:00+0000","status":"ACTIVE","updated_time":"2026-09-20T10:26:40+0000"}}}},"description":"The payload the provider holds now, with the money of Meta in whole units."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The provider holds no such entity on this ad account, the provider does not have this kind of entity, or your organization does not own the advertiser in the path. They are deliberately not told apart. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: Get one campaign","tags":["Campaigns"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The id the provider gives, with no prefix. Every listing answers it as `id`."}}]}},"/observe/ad-groups":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers) [![OpenAI](/providers/openai.svg)](https://docs.adcrunch.dev/connect/providers)\n\nLists the ad groups of every ad account your organization connects, across the providers, newest first.\n\n## One concept, three words\n\nAn ad group is a Meta **ad set**, a TikTok **ad group** and a Google Ads **ad group**. This path reads all three at once, and every row keeps the word its own provider writes, in `type`. AdCrunch renames nothing: a Meta `adset` and a Google `ad_group` are not the same object, and a caller that must speak to the provider needs the provider’s word.\n\n## Narrowing\n\nSend `campaignId` to read one campaign’s ad groups, and `provider` to read one platform. `limit` counts rows of the answer itself, not rows of each advertiser. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page.\n\n## Budget\n\nAn ad group that carries its own budget answers it in `budget`, in **whole units** of its `currency`, and a `budgetLevel` of `ad_group`. One whose campaign carries the budget answers a null `budget` and a `budgetLevel` of `campaign`: read the campaign for the amount. Every Google Ads ad group is the second kind, because Google Ads keeps every budget on the campaign.\n\n## What is not here\n\nPerformance Max campaigns have **no ad groups**. Google’s automation delivers them through an asset group instead, which is a child of the campaign and a sibling of an ad group — so a Performance Max campaign answers nothing here, and that is not a fault. Read `/observe/asset-groups` for those.","x-adcrunch-providers":["meta","tiktok","gads","openai"],"operationId":"listAdGroups","responses":{"200":{"content":{"application/json":{"example":{"data":[{"advertiserId":"acc_1203456789012345","bidAmount":null,"bidStrategy":null,"budget":null,"budgetLevel":"campaign","budgetType":null,"campaignId":"120210000000000","createdAt":1789610000000,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"endTime":null,"id":"120210000000001","name":"Women 25-44, France","path":"120210000000000/120210000000001","provider":"meta","roasFloor":null,"startTime":null,"status":"ACTIVE","type":"adset","updatedAt":null,"updatedTime":1789900000000}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwibWV0YSIsImFkc2V0IiwiMTIwMjEwMDAwMDAwMDAxIl0sInEiOiI1bHgzc2UzOTBpIn0"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"bidAmount":{"nullable":true,"description":"The amount that the bid strategy takes: a bid cap or a cost cap, in **whole units** of `currency`, the unit of `budget`. A bid is per optimization event, and per 1,000 impressions when the optimization goal is impressions or reach. Null for a strategy that takes no amount or a ROAS floor, and when AdCrunch does not know the account currency. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads.","type":"number"},"bidStrategy":{"nullable":true,"description":"How the provider bids for this entity, in the provider’s word: Meta `LOWEST_COST_WITHOUT_CAP` (Highest volume), `LOWEST_COST_WITH_BID_CAP` (Bid cap), `COST_CAP` (Cost per result goal) or `LOWEST_COST_WITH_MIN_ROAS` (ROAS goal). The strategy sits where the budget sits: on the campaign when the campaign carries the budget, otherwise on each ad set. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose bid AdCrunch does not read yet.","type":"string"},"budget":{"nullable":true,"description":"The budget this entity carries itself, in **whole units** of `currency`: `50` is 50.00, never 50 cents. It is the unit of `spend` on an Insight, so a budget and a spend compare directly. AdCrunch converts each provider’s own unit — Meta’s minor unit, TikTok’s whole units and Google Ads’ micros — so every provider answers the same unit here.\n\nNull when the entity carries no budget of its own: `budgetLevel` then says where its budget lives. A campaign answers a budget only when its `budgetLevel` is `campaign`, so a budget always paces delivery at the level that answers it. Null also when AdCrunch does not know the account currency yet, because nobody can read a budget with no currency.\n\nAt Google Ads one budget may be shared by several campaigns, and each of them answers the whole shared amount.\n\n**A stored budget can be older than the rest of the row.** Meta does not mark a campaign changed when only its budget changes, and AdCrunch does not yet follow a Google Ads budget change, so the ingestion can miss a budget edited in the provider’s own interface. A live read answers the budget the provider holds now.\n\nAdCrunch takes a budget in the same whole units when it writes one, so a budget you read here goes back with no conversion.","type":"number"},"budgetLevel":{"nullable":true,"description":"Where the budget of this entity’s campaign lives. `campaign` when the campaign carries it — Meta Advantage campaign budget, TikTok Campaign Budget Optimization, and every Google Ads campaign. `ad_group` when each ad group carries its own. Null on a TikTok campaign whose payload does not say.","type":"string","enum":["campaign","ad_group"]},"budgetType":{"nullable":true,"description":"`daily` for a budget spent each day, `lifetime` for one spent over the whole life of the entity. Null exactly when `budget` has no provider value to describe.","type":"string","enum":["daily","lifetime"]},"campaignId":{"nullable":true,"description":"The campaign it belongs to, as the bare id its provider gives. Null only when the stored path of the row names no campaign.","type":"string"},"endTime":{"nullable":true,"description":"When delivery of the ad set stops, as an ISO 8601 timestamp that the provider writes: Meta `2026-12-31T23:59:00+0100`. Null when the ad set has no end. It goes back to a schedule change as it is. Null on a stored row: AdCrunch keeps the schedule in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose schedule AdCrunch does not read yet.","type":"string"},"roasFloor":{"nullable":true,"description":"The lowest return on ad spend that the provider bids for, as a ratio: `1.5` is a value of 1.5 times the spend. Meta `LOWEST_COST_WITH_MIN_ROAS` only, and null for every other strategy. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it.","type":"number"},"startTime":{"nullable":true,"description":"When delivery of the ad set starts, as an ISO 8601 timestamp that the provider writes: Meta `2026-10-01T00:00:00+0200`. It goes back to a schedule change as it is. Null on a stored row: AdCrunch keeps the schedule in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose schedule AdCrunch does not read yet.","type":"string"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `adset` at Meta, `adgroup` at TikTok and `ad_group` at Google Ads. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","bidAmount","bidStrategy","budget","budgetLevel","budgetType","campaignId","endTime","roasFloor","startTime","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"advertiserId":"acc_1203456789012345","bidAmount":null,"bidStrategy":null,"budget":null,"budgetLevel":"campaign","budgetType":null,"campaignId":"120210000000000","createdAt":1789610000000,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"endTime":null,"id":"120210000000001","name":"Women 25-44, France","path":"120210000000000/120210000000001","provider":"meta","roasFloor":null,"startTime":null,"status":"ACTIVE","type":"adset","updatedAt":null,"updatedTime":1789900000000}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwibWV0YSIsImFkc2V0IiwiMTIwMjEwMDAwMDAwMDAxIl0sInEiOiI1bHgzc2UzOTBpIn0"}}}},"description":"The matching ad groups, and how many there are in all. Each row keeps its provider’s own `type`: `adset`, `adgroup` or `ad_group`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."}},"summary":"Stored read: List ad groups","tags":["Ad groups"],"parameters":[{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"advertiserId","in":"query","required":false,"schema":{"description":"Read one ad account from what AdCrunch holds, prefixed `acc_`. One your organization does not own answers an empty list rather than a failure. To ask the provider instead, put the advertiser in the path.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},{"name":"provider","in":"query","required":false,"schema":{"description":"Read one provider. Omit it to read every provider your organization connects.","anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]},{"type":"string","enum":["x"]},{"type":"string","enum":["openai"]}]}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers. TikTok `ENABLE` and `DISABLE` are read here as `ACTIVE` and `PAUSED`.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}},{"name":"campaignId","in":"query","required":false,"schema":{"description":"Read only what this campaign holds. Send the bare id its provider gives, which is the `campaignId` every row answers.","type":"string","pattern":"^\\d+$"}}]}},"/observe/{advertiserId}/ad-groups":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for the ad groups of one ad account, now. This is a **live read**: the path names the advertiser, so the answer is what the provider holds, not what AdCrunch holds.\n\nAn advertiser that your organization does not own answers `404`, with `error` of `not_found`. An advertiser that does not exist answers the same. The answer does not tell the two apart.\n\nSend `campaignId` to read one campaign’s ad groups.\n\nA live row carries no `createdAt`, `updatedAt` or `deletedAt`, because AdCrunch stores nothing for it. Each request counts against the rate limits of the provider.\n\nFor the stored rows of the same advertiser, read `/observe/ad-groups?advertiserId=…`. That path takes every `status`, and the Status section below lists the statuses this one takes.\n\n`limit` and `cursor` page the answer. Send `nextCursor` back as `cursor` to get the next page. A live cursor is good only for a live read. At TikTok, every page keeps the page size of the first.\n\n## Status\n\n`status` selects the status an entity is set to, not whether it delivers, and every row of the answer carries it. A provider takes a status only where one request to it selects exactly the entities in that status. Any other status answers `501`.\n\nThe stored read, `/observe/ad-groups?advertiserId=…&status=…`, takes every status. A status that a provider never uses answers an empty list.\n\n| Provider | Takes `status` |\n| --- | --- |\n| Meta | none yet |\n| TikTok | `DELETED`, `ARCHIVED` |\n| Google Ads | `ACTIVE`, `PAUSED`, `DELETED`, `ARCHIVED` |","x-adcrunch-providers":["meta","tiktok","gads"],"operationId":"listAdvertiserAdGroups","responses":{"200":{"content":{"application/json":{"example":{"data":[{"advertiserId":"acc_1203456789012345","bidAmount":null,"bidStrategy":null,"budget":null,"budgetLevel":"campaign","budgetType":null,"campaignId":"120210000000000","createdAt":null,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"endTime":null,"id":"120210000000001","name":"Women 25-44, France","path":"120210000000000/120210000000001","provider":"meta","roasFloor":null,"startTime":null,"status":"ACTIVE","type":"adset","updatedAt":null,"updatedTime":1789900000000}],"nextCursor":"eyJwIjp7ImFmdGVyIjoiUVZGSVVteFpBVmhSUTJ0QlVXdFIiLCJwcm92aWRlciI6Im1ldGEifSwicSI6IjEzMTJ3a3A5bXQxIiwibCI6dHJ1ZX0"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"bidAmount":{"nullable":true,"description":"The amount that the bid strategy takes: a bid cap or a cost cap, in **whole units** of `currency`, the unit of `budget`. A bid is per optimization event, and per 1,000 impressions when the optimization goal is impressions or reach. Null for a strategy that takes no amount or a ROAS floor, and when AdCrunch does not know the account currency. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads.","type":"number"},"bidStrategy":{"nullable":true,"description":"How the provider bids for this entity, in the provider’s word: Meta `LOWEST_COST_WITHOUT_CAP` (Highest volume), `LOWEST_COST_WITH_BID_CAP` (Bid cap), `COST_CAP` (Cost per result goal) or `LOWEST_COST_WITH_MIN_ROAS` (ROAS goal). The strategy sits where the budget sits: on the campaign when the campaign carries the budget, otherwise on each ad set. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose bid AdCrunch does not read yet.","type":"string"},"budget":{"nullable":true,"description":"The budget this entity carries itself, in **whole units** of `currency`: `50` is 50.00, never 50 cents. It is the unit of `spend` on an Insight, so a budget and a spend compare directly. AdCrunch converts each provider’s own unit — Meta’s minor unit, TikTok’s whole units and Google Ads’ micros — so every provider answers the same unit here.\n\nNull when the entity carries no budget of its own: `budgetLevel` then says where its budget lives. A campaign answers a budget only when its `budgetLevel` is `campaign`, so a budget always paces delivery at the level that answers it. Null also when AdCrunch does not know the account currency yet, because nobody can read a budget with no currency.\n\nAt Google Ads one budget may be shared by several campaigns, and each of them answers the whole shared amount.\n\n**A stored budget can be older than the rest of the row.** Meta does not mark a campaign changed when only its budget changes, and AdCrunch does not yet follow a Google Ads budget change, so the ingestion can miss a budget edited in the provider’s own interface. A live read answers the budget the provider holds now.\n\nAdCrunch takes a budget in the same whole units when it writes one, so a budget you read here goes back with no conversion.","type":"number"},"budgetLevel":{"nullable":true,"description":"Where the budget of this entity’s campaign lives. `campaign` when the campaign carries it — Meta Advantage campaign budget, TikTok Campaign Budget Optimization, and every Google Ads campaign. `ad_group` when each ad group carries its own. Null on a TikTok campaign whose payload does not say.","type":"string","enum":["campaign","ad_group"]},"budgetType":{"nullable":true,"description":"`daily` for a budget spent each day, `lifetime` for one spent over the whole life of the entity. Null exactly when `budget` has no provider value to describe.","type":"string","enum":["daily","lifetime"]},"campaignId":{"nullable":true,"description":"The campaign it belongs to, as the bare id its provider gives. Null only when the stored path of the row names no campaign.","type":"string"},"endTime":{"nullable":true,"description":"When delivery of the ad set stops, as an ISO 8601 timestamp that the provider writes: Meta `2026-12-31T23:59:00+0100`. Null when the ad set has no end. It goes back to a schedule change as it is. Null on a stored row: AdCrunch keeps the schedule in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose schedule AdCrunch does not read yet.","type":"string"},"roasFloor":{"nullable":true,"description":"The lowest return on ad spend that the provider bids for, as a ratio: `1.5` is a value of 1.5 times the spend. Meta `LOWEST_COST_WITH_MIN_ROAS` only, and null for every other strategy. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it.","type":"number"},"startTime":{"nullable":true,"description":"When delivery of the ad set starts, as an ISO 8601 timestamp that the provider writes: Meta `2026-10-01T00:00:00+0200`. It goes back to a schedule change as it is. Null on a stored row: AdCrunch keeps the schedule in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose schedule AdCrunch does not read yet.","type":"string"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `adset` at Meta, `adgroup` at TikTok and `ad_group` at Google Ads. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","bidAmount","bidStrategy","budget","budgetLevel","budgetType","campaignId","endTime","roasFloor","startTime","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"advertiserId":"acc_1203456789012345","bidAmount":null,"bidStrategy":null,"budget":null,"budgetLevel":"campaign","budgetType":null,"campaignId":"120210000000000","createdAt":null,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"endTime":null,"id":"120210000000001","name":"Women 25-44, France","path":"120210000000000/120210000000001","provider":"meta","roasFloor":null,"startTime":null,"status":"ACTIVE","type":"adset","updatedAt":null,"updatedTime":1789900000000}],"nextCursor":"eyJwIjp7ImFmdGVyIjoiUVZGSVVteFpBVmhSUTJ0QlVXdFIiLCJwcm92aWRlciI6Im1ldGEifSwicSI6IjEzMTJ3a3A5bXQxIiwibCI6dHJ1ZX0"}}}},"description":"The matching ad groups of this advertiser."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"Your organization does not own the advertiser in the path, or it does not exist. The answer does not tell the two apart. `error` is `not_found`. AdCrunch asks the provider nothing."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: List an advertiser’s ad groups","tags":["Ad groups"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers: the status the entity is set to, not whether it delivers. Every row of the answer carries this status. A provider that cannot select this status exactly answers `501`; the description of this operation lists what each provider selects.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}},{"name":"campaignId","in":"query","required":false,"schema":{"description":"Read only what this campaign holds. Send the bare id its provider gives, which is the `campaignId` every row answers.","type":"string","pattern":"^\\d+$"}}]}},"/observe/{advertiserId}/ad-groups/{id}":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for one ad group now, and gives its payload with the field names of the provider.\n\nThe keys are the provider’s, and so is the shape. A Meta ad set answers `id`, `name`, `campaign_id`, `status`, its two budget fields, its bid as `bid_strategy`, `bid_amount` and `bid_constraints`, and its schedule as `start_time` and `end_time`; a TikTok ad group answers `adgroup_id`, `budget_mode`, `optimization_goal`, `billing_event` and `placement_type`; a Google Ads ad group answers `id`, `name`, `status` and `campaignId`.\n\n**The payload holds the fields AdCrunch asks the provider for, and no more.** Targeting is outside that set at every provider, so no payload here carries it.\n\n## Money\n\nAdCrunch changes one thing in the payload: the money of Meta, `bid_amount`, `daily_budget` and `lifetime_budget`. Meta writes each amount in a unit that depends on the currency. This read gives each one as a number in **whole units** of the currency of the advertiser. It is the unit of `budget` on a row: `250` is 250.00. When AdCrunch does not know the currency of the advertiser, each of these fields is `null`.\n\nA TikTok amount is in whole units already, so it does not change. A Google Ads field whose name ends in `Micros` states its unit, so it stays in micros: `12340000` is 12.34.\n\n## When there is nothing to give\n\n**`404`** — the provider holds no such ad group on this ad account, the provider has no ad group at all, or your organization does not own the advertiser in the path. They are deliberately not told apart: distinguishing them would confirm that a ad group exists to somebody who cannot read it.","x-adcrunch-providers":["meta","tiktok","gads"],"operationId":"getAdGroup","responses":{"200":{"content":{"application/json":{"example":{"account_id":"1203456789012345","bid_amount":12.5,"bid_strategy":"COST_CAP","campaign_id":"120210000000000","created_time":"2026-09-16T23:06:40+0000","end_time":"2026-12-31T23:59:00+0100","id":"120210000000001","name":"Women 25-44, France","start_time":"2026-09-17T00:00:00+0200","status":"ACTIVE","updated_time":"2026-09-20T10:26:40+0000"},"schema":{"description":"The provider’s own payload for this entity. The keys are the provider’s: a TikTok campaign answers `campaign_id`, `campaign_name`, `objective_type`, `budget_mode` and the rest of TikTok’s vocabulary. AdCrunch changes only the money of Meta: `bid_amount`, `daily_budget` and `lifetime_budget` are numbers in whole units of the currency of the advertiser. Read the resource listing instead when you want the normalized fields.","example":{"account_id":"1203456789012345","bid_amount":12.5,"bid_strategy":"COST_CAP","campaign_id":"120210000000000","created_time":"2026-09-16T23:06:40+0000","end_time":"2026-12-31T23:59:00+0100","id":"120210000000001","name":"Women 25-44, France","start_time":"2026-09-17T00:00:00+0200","status":"ACTIVE","updated_time":"2026-09-20T10:26:40+0000"}}}},"description":"The payload the provider holds now, with the money of Meta in whole units."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The provider holds no such entity on this ad account, the provider does not have this kind of entity, or your organization does not own the advertiser in the path. They are deliberately not told apart. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: Get one ad group","tags":["Ad groups"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The id the provider gives, with no prefix. Every listing answers it as `id`."}}]}},"/observe/ads":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers) [![OpenAI](/providers/openai.svg)](https://docs.adcrunch.dev/connect/providers)\n\nLists the ads of every ad account your organization connects, across the providers, newest first.\n\n## One concept, two words\n\nAn ad is an `ad` at Meta and TikTok. At Google Ads it is an **`ad_group_ad`** — the link resource that joins an ad to an ad group, whose id is the composite `{adGroupId}~{adId}`. AdCrunch stores that link as the leaf and does not model a separate shared ad, so a Google row answers the composite id. Every row keeps its own provider’s word in `type`.\n\n## Narrowing\n\nTwo parents, and they are not the same question. `adGroupId` reads the ads of one ad group. `campaignId` reads **every** ad of a campaign, whatever ad group holds it — a grandparent, which the ad group filter cannot express. Send both to narrow twice.\n\n## What is not here\n\nPerformance Max campaigns have **no ads**. Google’s automation assembles placements from an asset group, so nothing below a Performance Max campaign appears here.","x-adcrunch-providers":["meta","tiktok","gads","openai"],"operationId":"listAds","responses":{"200":{"content":{"application/json":{"example":{"data":[{"adGroupId":"169283746152","advertiserId":"acc_4829301756","campaignId":"20481523697","createdAt":1789610000000,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"169283746152~712398456012","name":"Responsive search ad — winter","path":"20481523697/169283746152/169283746152~712398456012","provider":"gads","status":"ACTIVE","type":"ad_group_ad","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwiZ2FkcyIsImFkX2dyb3VwX2FkIiwiMTY5MjgzNzQ2MTUyfjcxMjM5ODQ1NjAxMiJdLCJxIjoiZTBmZ2hvbDFrYSJ9"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"adGroupId":{"nullable":true,"description":"The ad group that holds the ad, as the bare id its provider gives. Null only when the stored path of the row names no ad group.","type":"string"},"campaignId":{"nullable":true,"description":"The campaign it belongs to, as the bare id its provider gives. Null only when the stored path of the row names no campaign.","type":"string"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `ad` at Meta and TikTok, and `ad_group_ad` at Google Ads. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","adGroupId","campaignId","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"adGroupId":"169283746152","advertiserId":"acc_4829301756","campaignId":"20481523697","createdAt":1789610000000,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"169283746152~712398456012","name":"Responsive search ad — winter","path":"20481523697/169283746152/169283746152~712398456012","provider":"gads","status":"ACTIVE","type":"ad_group_ad","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwiZ2FkcyIsImFkX2dyb3VwX2FkIiwiMTY5MjgzNzQ2MTUyfjcxMjM5ODQ1NjAxMiJdLCJxIjoiZTBmZ2hvbDFrYSJ9"}}}},"description":"The matching ads, and how many there are in all. Each row keeps its provider’s own `type`: `ad`, or `ad_group_ad` at Google Ads."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."}},"summary":"Stored read: List ads","tags":["Ads"],"parameters":[{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"advertiserId","in":"query","required":false,"schema":{"description":"Read one ad account from what AdCrunch holds, prefixed `acc_`. One your organization does not own answers an empty list rather than a failure. To ask the provider instead, put the advertiser in the path.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},{"name":"provider","in":"query","required":false,"schema":{"description":"Read one provider. Omit it to read every provider your organization connects.","anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]},{"type":"string","enum":["x"]},{"type":"string","enum":["openai"]}]}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers. TikTok `ENABLE` and `DISABLE` are read here as `ACTIVE` and `PAUSED`.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}},{"name":"campaignId","in":"query","required":false,"schema":{"description":"Read only what this campaign holds. Send the bare id its provider gives, which is the `campaignId` every row answers.","type":"string","pattern":"^\\d+$"}},{"name":"adGroupId","in":"query","required":false,"schema":{"description":"Read only what this ad group holds. Send the bare id its provider gives, which is the `adGroupId` every row answers.","type":"string","pattern":"^\\d+$"}}]}},"/observe/{advertiserId}/ads":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for the ads of one ad account, now. This is a **live read**: the path names the advertiser, so the answer is what the provider holds, not what AdCrunch holds.\n\nAn advertiser that your organization does not own answers `404`, with `error` of `not_found`. An advertiser that does not exist answers the same. The answer does not tell the two apart.\n\nSend `adGroupId` to read one ad group’s ads, or `campaignId` to read every ad of a campaign whatever ad group holds it.\n\nA live row carries no `createdAt`, `updatedAt` or `deletedAt`, because AdCrunch stores nothing for it. Each request counts against the rate limits of the provider.\n\nFor the stored rows of the same advertiser, read `/observe/ads?advertiserId=…`. That path takes every `status`, and the Status section below lists the statuses this one takes.\n\n`limit` and `cursor` page the answer. Send `nextCursor` back as `cursor` to get the next page. A live cursor is good only for a live read. At TikTok, every page keeps the page size of the first.\n\n## Status\n\n`status` selects the status an entity is set to, not whether it delivers, and every row of the answer carries it. A provider takes a status only where one request to it selects exactly the entities in that status. Any other status answers `501`.\n\nThe stored read, `/observe/ads?advertiserId=…&status=…`, takes every status. A status that a provider never uses answers an empty list.\n\n| Provider | Takes `status` |\n| --- | --- |\n| Meta | none yet |\n| TikTok | `DELETED`, `ARCHIVED` |\n| Google Ads | `ACTIVE`, `PAUSED`, `DELETED`, `ARCHIVED` |","x-adcrunch-providers":["meta","tiktok","gads"],"operationId":"listAdvertiserAds","responses":{"200":{"content":{"application/json":{"example":{"data":[{"adGroupId":"120210000000001","advertiserId":"acc_1203456789012345","campaignId":"120210000000000","createdAt":null,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"id":"120210000000002","name":"Carousel — bestsellers","path":"120210000000000/120210000000001/120210000000002","provider":"meta","status":"ACTIVE","type":"ad","updatedAt":null,"updatedTime":1789900000000}],"nextCursor":"eyJwIjp7ImFmdGVyIjoiUVZGSVVteFpBVmhSUTJ0QlVXdFIiLCJwcm92aWRlciI6Im1ldGEifSwicSI6IjFwc2sxcGFja3VlIiwibCI6dHJ1ZX0"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"adGroupId":{"nullable":true,"description":"The ad group that holds the ad, as the bare id its provider gives. Null only when the stored path of the row names no ad group.","type":"string"},"campaignId":{"nullable":true,"description":"The campaign it belongs to, as the bare id its provider gives. Null only when the stored path of the row names no campaign.","type":"string"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `ad` at Meta and TikTok, and `ad_group_ad` at Google Ads. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","adGroupId","campaignId","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"adGroupId":"120210000000001","advertiserId":"acc_1203456789012345","campaignId":"120210000000000","createdAt":null,"createdTime":1789600000000,"currency":"EUR","deletedAt":null,"id":"120210000000002","name":"Carousel — bestsellers","path":"120210000000000/120210000000001/120210000000002","provider":"meta","status":"ACTIVE","type":"ad","updatedAt":null,"updatedTime":1789900000000}],"nextCursor":"eyJwIjp7ImFmdGVyIjoiUVZGSVVteFpBVmhSUTJ0QlVXdFIiLCJwcm92aWRlciI6Im1ldGEifSwicSI6IjFwc2sxcGFja3VlIiwibCI6dHJ1ZX0"}}}},"description":"The matching ads of this advertiser."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"Your organization does not own the advertiser in the path, or it does not exist. The answer does not tell the two apart. `error` is `not_found`. AdCrunch asks the provider nothing."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: List an advertiser’s ads","tags":["Ads"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers: the status the entity is set to, not whether it delivers. Every row of the answer carries this status. A provider that cannot select this status exactly answers `501`; the description of this operation lists what each provider selects.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}},{"name":"campaignId","in":"query","required":false,"schema":{"description":"Read only what this campaign holds. Send the bare id its provider gives, which is the `campaignId` every row answers.","type":"string","pattern":"^\\d+$"}},{"name":"adGroupId","in":"query","required":false,"schema":{"description":"Read only what this ad group holds. Send the bare id its provider gives, which is the `adGroupId` every row answers.","type":"string","pattern":"^\\d+$"}}]}},"/observe/{advertiserId}/ads/{id}":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for one ad now, and gives its payload with the field names of the provider.\n\nThe keys are the provider’s. A Meta ad answers `id`, `name`, `campaign_id`, `adset_id`, `status` and a `creative` that holds one key, the creative id. A Meta ad that has a bid of its own also answers `bid_amount`. A TikTok ad answers `ad_id`, `ad_format`, `call_to_action`, `landing_page_url`, `video_id` and `image_ids`. A Google Ads `ad_group_ad` answers `id`, `name`, `status`, `adGroupId` and `campaignId`.\n\n**The payload holds the fields AdCrunch asks the provider for, and no more.** Read the creative under `/observe/{advertiserId}/creatives` for the material a Meta ad runs.\n\n## Money\n\nAdCrunch changes one thing in the payload: the money of Meta, `bid_amount`. Meta writes each amount in a unit that depends on the currency. This read gives each one as a number in **whole units** of the currency of the advertiser. It is the unit of `budget` on a row: `250` is 250.00. When AdCrunch does not know the currency of the advertiser, each of these fields is `null`.\n\nA TikTok amount is in whole units already, so it does not change. A Google Ads field whose name ends in `Micros` states its unit, so it stays in micros: `12340000` is 12.34.\n\n## When there is nothing to give\n\n**`404`** — the provider holds no such ad on this ad account, the provider has no ad at all, or your organization does not own the advertiser in the path. They are deliberately not told apart: distinguishing them would confirm that a ad exists to somebody who cannot read it.","x-adcrunch-providers":["meta","tiktok","gads"],"operationId":"getAd","responses":{"200":{"content":{"application/json":{"example":{"account_id":"1203456789012345","adset_id":"120210000000001","campaign_id":"120210000000000","created_time":"2026-09-16T23:06:40+0000","creative":{"id":"120210000000003"},"id":"120210000000002","name":"Carousel — bestsellers","status":"ACTIVE","updated_time":"2026-09-20T10:26:40+0000"},"schema":{"description":"The provider’s own payload for this entity. The keys are the provider’s: a TikTok campaign answers `campaign_id`, `campaign_name`, `objective_type`, `budget_mode` and the rest of TikTok’s vocabulary. AdCrunch changes only the money of Meta: `bid_amount`, `daily_budget` and `lifetime_budget` are numbers in whole units of the currency of the advertiser. Read the resource listing instead when you want the normalized fields.","example":{"account_id":"1203456789012345","adset_id":"120210000000001","campaign_id":"120210000000000","created_time":"2026-09-16T23:06:40+0000","creative":{"id":"120210000000003"},"id":"120210000000002","name":"Carousel — bestsellers","status":"ACTIVE","updated_time":"2026-09-20T10:26:40+0000"}}}},"description":"The payload the provider holds now, with the money of Meta in whole units."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The provider holds no such entity on this ad account, the provider does not have this kind of entity, or your organization does not own the advertiser in the path. They are deliberately not told apart. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: Get one ad","tags":["Ads"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The id the provider gives, with no prefix. Every listing answers it as `id`."}}]}},"/observe/creatives":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers)\n\nLists the creatives of every ad account your organization connects, newest first.\n\n## Meta only, today\n\nA standalone Creative is Meta’s model. **TikTok has none** — its creative material (the video or image, the ad text, the call to action) is embedded in the Ad itself, so a TikTok advertiser lists nothing here and AdCrunch does not invent a creative for it. Google Ads has none either: it keeps its creative material on the `ad_group_ad` link resource.\n\nAn empty answer for those providers is normal, and it is not a failure.\n\n## No parent\n\nA creative is **account-level**: it belongs to an advertiser and not to any one campaign or ad group, and one creative can be reused across many ads. So there is no campaign or ad group to filter by, and a row carries no `campaignId` and no `adGroupId`.\n\n## Freshness\n\nA creative is re-read only when the ad that carries it changes. A body-copy tweak or a thumbnail swap made in the provider’s own interface lags until the next ad-level change reaches AdCrunch.\n\n## Not an Asset\n\nA Creative is the provider’s — the assembled payload it returns once an ad exists. An **Asset** is yours: a source media file your organization owns, held before any ad references it. They are deliberately unlinked; read the Assets API for those.","x-adcrunch-providers":["meta"],"operationId":"listCreatives","responses":{"200":{"content":{"application/json":{"example":{"data":[{"advertiserId":"acc_1203456789012345","createdAt":1789610000000,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"120210000000003","name":"Bestsellers carousel — FR","path":"120210000000003","provider":"meta","status":"ACTIVE","type":"creative","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwibWV0YSIsImNyZWF0aXZlIiwiMTIwMjEwMDAwMDAwMDAzIl0sInEiOiI5MTZmZHNjbWx6In0"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `creative`. Only Meta has a creative of its own. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"advertiserId":"acc_1203456789012345","createdAt":1789610000000,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"120210000000003","name":"Bestsellers carousel — FR","path":"120210000000003","provider":"meta","status":"ACTIVE","type":"creative","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwibWV0YSIsImNyZWF0aXZlIiwiMTIwMjEwMDAwMDAwMDAzIl0sInEiOiI5MTZmZHNjbWx6In0"}}}},"description":"The matching creatives, and how many there are in all. Only Meta advertisers answer rows today."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."}},"summary":"Stored read: List creatives","tags":["Creatives"],"parameters":[{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"advertiserId","in":"query","required":false,"schema":{"description":"Read one ad account from what AdCrunch holds, prefixed `acc_`. One your organization does not own answers an empty list rather than a failure. To ask the provider instead, put the advertiser in the path.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},{"name":"provider","in":"query","required":false,"schema":{"description":"Read one provider. Omit it to read every provider your organization connects.","anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]},{"type":"string","enum":["x"]},{"type":"string","enum":["openai"]}]}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers. TikTok `ENABLE` and `DISABLE` are read here as `ACTIVE` and `PAUSED`.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}}]}},"/observe/{advertiserId}/creatives":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for the creatives of one ad account, now. This is a **live read**: the path names the advertiser, so the answer is what the provider holds, not what AdCrunch holds.\n\nAn advertiser that your organization does not own answers `404`, with `error` of `not_found`. An advertiser that does not exist answers the same. The answer does not tell the two apart.\n\nMeta is the one provider with a separate creative. An advertiser on TikTok or Google Ads answers an empty list, because its provider has none. An advertiser on a provider AdCrunch cannot read live answers `501`.\n\nA live row carries no `createdAt`, `updatedAt` or `deletedAt`, because AdCrunch stores nothing for it. Each request counts against the rate limits of the provider.\n\nFor the stored rows of the same advertiser, read `/observe/creatives?advertiserId=…`. That path takes every `status`, and the Status section below lists the statuses this one takes.\n\n`limit` and `cursor` page the answer. Send `nextCursor` back as `cursor` to get the next page. A live cursor is good only for a live read. At TikTok, every page keeps the page size of the first.\n\n## Status\n\n`status` selects the status an entity is set to, not whether it delivers, and every row of the answer carries it. A provider takes a status only where one request to it selects exactly the entities in that status. Any other status answers `501`.\n\nThe stored read, `/observe/creatives?advertiserId=…&status=…`, takes every status. A status that a provider never uses answers an empty list.\n\n| Provider | Takes `status` |\n| --- | --- |\n| Meta | `PAUSED`, `ARCHIVED` |","x-adcrunch-providers":["meta"],"operationId":"listAdvertiserCreatives","responses":{"200":{"content":{"application/json":{"example":{"data":[{"advertiserId":"acc_1203456789012345","createdAt":null,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"120210000000003","name":"Bestsellers carousel — FR","path":"120210000000003","provider":"meta","status":"ACTIVE","type":"creative","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjp7ImFmdGVyIjoiUVZGSVVteFpBVmhSUTJ0QlVXdFIiLCJwcm92aWRlciI6Im1ldGEifSwicSI6IjIyYWowNHU4dHA1IiwibCI6dHJ1ZX0"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `creative`. Only Meta has a creative of its own. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"advertiserId":"acc_1203456789012345","createdAt":null,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"120210000000003","name":"Bestsellers carousel — FR","path":"120210000000003","provider":"meta","status":"ACTIVE","type":"creative","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjp7ImFmdGVyIjoiUVZGSVVteFpBVmhSUTJ0QlVXdFIiLCJwcm92aWRlciI6Im1ldGEifSwicSI6IjIyYWowNHU4dHA1IiwibCI6dHJ1ZX0"}}}},"description":"The matching creatives of this advertiser. An advertiser on a provider with no separate creative answers an empty list."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"Your organization does not own the advertiser in the path, or it does not exist. The answer does not tell the two apart. `error` is `not_found`. AdCrunch asks the provider nothing."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: List an advertiser’s creatives","tags":["Creatives"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers: the status the entity is set to, not whether it delivers. Every row of the answer carries this status. A provider that cannot select this status exactly answers `501`; the description of this operation lists what each provider selects.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}}]}},"/observe/{advertiserId}/creatives/{id}":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for one creative now, and gives its payload exactly as Meta sends it: the headline in `title`, the body copy in `body`, the image or the video id, the call to action and the link.\n\n## When there is nothing to give\n\n**`404`** — the provider holds no such creative on this ad account, the provider has no creative at all, or your organization does not own the advertiser in the path. They are deliberately not told apart: distinguishing them would confirm that a creative exists to somebody who cannot read it.","x-adcrunch-providers":["meta"],"operationId":"getCreative","responses":{"200":{"content":{"application/json":{"example":{"account_id":"1203456789012345","body":"Our bestsellers, now at 20% off.","call_to_action_type":"SHOP_NOW","id":"120210000000003","image_url":"https://scontent.xx.fbcdn.net/v/t45.1600-4/bestsellers.jpg","link_url":"https://example.com/bestsellers","name":"Bestsellers carousel — FR","status":"ACTIVE","thumbnail_url":"https://scontent.xx.fbcdn.net/v/t45.1600-4/thumb.jpg","title":"Bestsellers"},"schema":{"description":"The provider’s own payload for this entity. The keys are the provider’s: a TikTok campaign answers `campaign_id`, `campaign_name`, `objective_type`, `budget_mode` and the rest of TikTok’s vocabulary. AdCrunch changes only the money of Meta: `bid_amount`, `daily_budget` and `lifetime_budget` are numbers in whole units of the currency of the advertiser. Read the resource listing instead when you want the normalized fields.","example":{"account_id":"1203456789012345","body":"Our bestsellers, now at 20% off.","call_to_action_type":"SHOP_NOW","id":"120210000000003","image_url":"https://scontent.xx.fbcdn.net/v/t45.1600-4/bestsellers.jpg","link_url":"https://example.com/bestsellers","name":"Bestsellers carousel — FR","status":"ACTIVE","thumbnail_url":"https://scontent.xx.fbcdn.net/v/t45.1600-4/thumb.jpg","title":"Bestsellers"}}}},"description":"The payload the provider holds now, verbatim."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The provider holds no such entity on this ad account, the provider does not have this kind of entity, or your organization does not own the advertiser in the path. They are deliberately not told apart. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: Get one creative","tags":["Creatives"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The id the provider gives, with no prefix. Every listing answers it as `id`."}}]}},"/observe/asset-groups":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers)\n\nLists the asset groups of every ad account your organization connects, newest first.\n\n## Google Ads only, and Performance Max only\n\nAn asset group is Performance Max’s delivery container. A Performance Max campaign has **no ad groups and no ads**: the asset group holds the headlines, images, videos and audience signals that Google’s automation assembles into placements itself.\n\nEvery other provider lists nothing here, and that is normal rather than a failure.\n\n## A sibling, not a child\n\nAn asset group is a child of a **campaign** and a **sibling** of an ad group — not a layer beneath one. That is why it takes `campaignId` and never `adGroupId`, and why a Performance Max campaign answers nothing under `/observe/ad-groups` or `/observe/ads`.","x-adcrunch-providers":["gads"],"operationId":"listAssetGroups","responses":{"200":{"content":{"application/json":{"example":{"data":[{"advertiserId":"acc_4829301756","budgetLevel":"campaign","campaignId":"20993874561","createdAt":1789610000000,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"6390218745","name":"Performance Max — winter","path":"20993874561/6390218745","provider":"gads","status":"ACTIVE","type":"asset_group","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwiZ2FkcyIsImFzc2V0X2dyb3VwIiwiNjM5MDIxODc0NSJdLCJxIjoiMmdtajdydHJyam8ifQ"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"budgetLevel":{"nullable":true,"description":"Where the budget of this entity’s campaign lives. `campaign` when the campaign carries it — Meta Advantage campaign budget, TikTok Campaign Budget Optimization, and every Google Ads campaign. `ad_group` when each ad group carries its own. Null on a TikTok campaign whose payload does not say.","type":"string","enum":["campaign","ad_group"]},"campaignId":{"nullable":true,"description":"The campaign it belongs to, as the bare id its provider gives. Null only when the stored path of the row names no campaign.","type":"string"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `asset_group`. Only Google Ads has an asset group. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","budgetLevel","campaignId","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"advertiserId":"acc_4829301756","budgetLevel":"campaign","campaignId":"20993874561","createdAt":1789610000000,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"6390218745","name":"Performance Max — winter","path":"20993874561/6390218745","provider":"gads","status":"ACTIVE","type":"asset_group","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjpbMTc4OTYxMDAwMDAwMCwiZ2FkcyIsImFzc2V0X2dyb3VwIiwiNjM5MDIxODc0NSJdLCJxIjoiMmdtajdydHJyam8ifQ"}}}},"description":"The matching asset groups, and how many there are in all. Only Google Ads advertisers answer rows."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."}},"summary":"Stored read: List asset groups","tags":["Asset groups"],"parameters":[{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"advertiserId","in":"query","required":false,"schema":{"description":"Read one ad account from what AdCrunch holds, prefixed `acc_`. One your organization does not own answers an empty list rather than a failure. To ask the provider instead, put the advertiser in the path.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},{"name":"provider","in":"query","required":false,"schema":{"description":"Read one provider. Omit it to read every provider your organization connects.","anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]},{"type":"string","enum":["x"]},{"type":"string","enum":["openai"]}]}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers. TikTok `ENABLE` and `DISABLE` are read here as `ACTIVE` and `PAUSED`.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}},{"name":"campaignId","in":"query","required":false,"schema":{"description":"Read only what this campaign holds. Send the bare id its provider gives, which is the `campaignId` every row answers.","type":"string","pattern":"^\\d+$"}}]}},"/observe/{advertiserId}/asset-groups":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for the asset groups of one ad account, now. This is a **live read**: the path names the advertiser, so the answer is what the provider holds, not what AdCrunch holds.\n\nAn advertiser that your organization does not own answers `404`, with `error` of `not_found`. An advertiser that does not exist answers the same. The answer does not tell the two apart.\n\nSend `campaignId` to read one Performance Max campaign’s asset groups.\n\nGoogle Ads is the one provider that has an asset group. An advertiser on Meta or TikTok answers an empty list, because its provider has none. An advertiser on a provider AdCrunch cannot read live answers `501`.\n\nA live row carries no `createdAt`, `updatedAt` or `deletedAt`, because AdCrunch stores nothing for it. Each request counts against the rate limits of the provider.\n\nFor the stored rows of the same advertiser, read `/observe/asset-groups?advertiserId=…`. That path takes every `status`, and the Status section below lists the statuses this one takes.\n\n`limit` and `cursor` page the answer. Send `nextCursor` back as `cursor` to get the next page. A live cursor is good only for a live read. At TikTok, every page keeps the page size of the first.\n\n## Status\n\n`status` selects the status an entity is set to, not whether it delivers, and every row of the answer carries it. A provider takes a status only where one request to it selects exactly the entities in that status. Any other status answers `501`.\n\nThe stored read, `/observe/asset-groups?advertiserId=…&status=…`, takes every status. A status that a provider never uses answers an empty list.\n\n| Provider | Takes `status` |\n| --- | --- |\n| Google Ads | `ACTIVE`, `PAUSED`, `DELETED`, `ARCHIVED` |","x-adcrunch-providers":["gads"],"operationId":"listAdvertiserAssetGroups","responses":{"200":{"content":{"application/json":{"example":{"data":[{"advertiserId":"acc_4829301756","budgetLevel":"campaign","campaignId":"20993874561","createdAt":null,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"6390218745","name":"Performance Max — winter","path":"20993874561/6390218745","provider":"gads","status":"ACTIVE","type":"asset_group","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjp7ImFmdGVySWQiOiI2MzkwMjE4NzQ1IiwicHJvdmlkZXIiOiJnYWRzIn0sInEiOiJ5ZHB4bHR3ZTYxIiwibCI6dHJ1ZX0"},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser that owns it, prefixed `acc_`."},"createdAt":{"nullable":true,"description":"When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.","type":"number"},"createdTime":{"nullable":true,"description":"When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.","type":"number"},"currency":{"nullable":true,"description":"The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.","type":"string"},"deletedAt":{"nullable":true,"description":"When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.","type":"number"},"id":{"type":"string","description":"The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers."},"name":{"type":"string","description":"The name at the provider."},"path":{"type":"string","description":"The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison."},"provider":{"type":"string","description":"The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes."},"status":{"type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"description":"The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`."},"updatedAt":{"nullable":true,"description":"When AdCrunch last rewrote this row. Null if it never changed.","type":"number"},"updatedTime":{"nullable":true,"description":"When the provider last edited the entity. Null where the provider reports none.","type":"number"},"budgetLevel":{"nullable":true,"description":"Where the budget of this entity’s campaign lives. `campaign` when the campaign carries it — Meta Advantage campaign budget, TikTok Campaign Budget Optimization, and every Google Ads campaign. `ad_group` when each ad group carries its own. Null on a TikTok campaign whose payload does not say.","type":"string","enum":["campaign","ad_group"]},"campaignId":{"nullable":true,"description":"The campaign it belongs to, as the bare id its provider gives. Null only when the stored path of the row names no campaign.","type":"string"},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `asset_group`. Only Google Ads has an asset group. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object."}},"required":["advertiserId","createdAt","createdTime","currency","deletedAt","id","name","path","provider","status","updatedAt","updatedTime","budgetLevel","campaignId","type"],"additionalProperties":false},"description":"The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"advertiserId":"acc_4829301756","budgetLevel":"campaign","campaignId":"20993874561","createdAt":null,"createdTime":null,"currency":"EUR","deletedAt":null,"id":"6390218745","name":"Performance Max — winter","path":"20993874561/6390218745","provider":"gads","status":"ACTIVE","type":"asset_group","updatedAt":null,"updatedTime":null}],"nextCursor":"eyJwIjp7ImFmdGVySWQiOiI2MzkwMjE4NzQ1IiwicHJvdmlkZXIiOiJnYWRzIn0sInEiOiJ5ZHB4bHR3ZTYxIiwibCI6dHJ1ZX0"}}}},"description":"The matching asset groups of this advertiser. An advertiser that is not on Google Ads answers an empty list."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"Your organization does not own the advertiser in the path, or it does not exist. The answer does not tell the two apart. `error` is `not_found`. AdCrunch asks the provider nothing."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: List an advertiser’s asset groups","tags":["Asset groups"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"ids","in":"query","required":false,"schema":{"description":"Ids to filter on, separated by commas. An id is the bare id its provider gives.","type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"name":"status","in":"query","required":false,"schema":{"description":"Read one status, normalized across the providers: the status the entity is set to, not whether it delivers. Every row of the answer carries this status. A provider that cannot select this status exactly answers `501`; the description of this operation lists what each provider selects.","type":"string","enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"]}},{"name":"campaignId","in":"query","required":false,"schema":{"description":"Read only what this campaign holds. Send the bare id its provider gives, which is the `campaignId` every row answers.","type":"string","pattern":"^\\d+$"}}]}},"/observe/{advertiserId}/asset-groups/{id}":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers)\n\nAsks the provider for one asset group now, and gives its payload exactly as Google Ads sends it: its `id`, its `name`, its `status` and the `campaignId` of the Performance Max campaign that holds it.\n\n**Those four keys are the whole payload.** AdCrunch reads an asset group with one query, and the headlines, the images, the videos and the audience signals are separate Google Ads resources that this API does not read.\n\n## When there is nothing to give\n\n**`404`** — the provider holds no such asset group on this ad account, the provider has no asset group at all, or your organization does not own the advertiser in the path. They are deliberately not told apart: distinguishing them would confirm that a asset group exists to somebody who cannot read it.","x-adcrunch-providers":["gads"],"operationId":"getAssetGroup","responses":{"200":{"content":{"application/json":{"example":{"campaignId":"20993874561","id":"6390218745","name":"Performance Max — winter","status":"ENABLED"},"schema":{"description":"The provider’s own payload for this entity. The keys are the provider’s: a TikTok campaign answers `campaign_id`, `campaign_name`, `objective_type`, `budget_mode` and the rest of TikTok’s vocabulary. AdCrunch changes only the money of Meta: `bid_amount`, `daily_budget` and `lifetime_budget` are numbers in whole units of the currency of the advertiser. Read the resource listing instead when you want the normalized fields.","example":{"campaignId":"20993874561","id":"6390218745","name":"Performance Max — winter","status":"ENABLED"}}}},"description":"The payload the provider holds now, verbatim."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The provider holds no such entity on this ad account, the provider does not have this kind of entity, or your organization does not own the advertiser in the path. They are deliberately not told apart. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds."},"501":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["live_read_unsupported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`."},"502":{"content":{"application/json":{"schema":{"type":"object","properties":{"platformError":{"description":"The provider’s own error object, unchanged. For Meta it carries `code`, `error_subcode`, `type`, `error_user_title` and `error_user_msg` — branch on the subcode for anything AdCrunch does not model. It never holds the request AdCrunch sent."},"provider":{"type":"string","description":"The ad platform this failure came from: `meta`, `tiktok` or `gads`."},"error":{"type":"string","enum":["provider_error"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["platformError","provider","error","message"],"additionalProperties":false}}},"description":"The provider refused the request, or could not answer it. Often transient, so a retry is worth making."}},"summary":"Live read: Get one asset group","tags":["Asset groups"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`."}},{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The id the provider gives, with no prefix. Every listing answers it as `id`."}}]}},"/observe/insights":{"get":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers) [![OpenAI](/providers/openai.svg)](https://docs.adcrunch.dev/connect/providers)\n\nGives aggregated metric rows for your organization: what an ad account, a campaign, an ad group or an ad spent over a date range, and what it returned.\n\nEach row carries the metrics you name in `select`. The default metrics are `spend` and `impressions`. Each row also carries a `currency`.\n\n**A metric a provider does not report reads `0`, and never null.** Google Ads and OpenAI report no reach, so `reach`, `cpp` and `frequency` read `0` on every row of theirs. Compare those three inside one provider only.\n\n## Date range\n\n**A date range is required.** Send `since`, and optionally `until`, both written `YYYY-MM-DD`. Or send `relative`: `last_7_days`, `last_15_days` or `last_month`. A request with neither is refused with `400 invalid_request`.\n\nSet `interval` to `day`, `week` or `month` to get one row for each period, each carrying a `date`. A week is dated by its Monday, a month by its first day. Omit `interval` to get one row for the whole range.\n\n## Filters and groups\n\n`advertiserId`, `entityId`, `entityType` and `provider` are **filters**: they decide which rows are read. `breakdown` is separate: it decides how the rows are **grouped**, and which columns name the group.\n\n| `breakdown` | Rows | Each row also carries |\n| --- | --- | --- |\n| omitted | one, for everything that matched | nothing else |\n| a concept | one for each entity of that level, at every provider | `provider`, `advertiserId`, `entityId`, `type` |\n| `provider` | one for each ad platform | `provider` |\n| `advertiser` | one for each ad account | `provider`, `advertiserId` |\n| a provider entity type | one for each entity of that type | `provider`, `advertiserId`, `entityId`, `type` |\n\n### Concepts\n\nA concept names one level with the same word at every provider. There are five: `campaigns`, `ad-groups`, `ads`, `creatives` and `asset-groups`, the names of the entity resources of this API.\n\n| Concept | Meta | TikTok | Google Ads |\n| --- | --- | --- | --- |\n| `campaigns` | `campaign` | `campaign` | `campaign` |\n| `ad-groups` | `adset` | `adgroup` | `ad_group` |\n| `ads` | `ad` | `ad` | `ad_group_ad` |\n| `creatives` | `creative` | — | — |\n| `asset-groups` | — | — | `asset_group` |\n\n`breakdown=ad-groups` answers one row for each ad group of each provider together. **Each row keeps the word of its provider** in `type`: `adset` on a Meta row, `adgroup` on a TikTok row. A provider that does not have the concept adds no row. A concept is valid with or without `provider`. `entityType` takes a concept too.\n\n### Provider entity types\n\nA provider entity type is the word of one provider: `campaign`, `adset` at Meta, `adgroup` at TikTok, `ad_group` at Google Ads, `ad`, and so on. It reads only the rows that carry that word, so `breakdown=ad` reads the ads of Meta and TikTok, and not the `ad_group_ad` rows of Google Ads.\n\nA provider entity type below `campaign` needs a `provider` or an entity filter. `breakdown=adset` with no `provider` is refused with `400 invalid_request`, and its issue names the breakdowns that are available. Send `breakdown=ad-groups` to read that level at every provider.\n\n## Money\n\nMoney is in **whole units** of the row’s `currency`, with decimals: a `spend` of `1284.57` is 1,284.57. It is the unit a `budget` takes on an entity, so a budget and a spend compare directly. The Mutations API and a Campaign Plan take the same unit.\n\nMoney comes back in the account currency of the rows it came from, and each row says which in its `currency`. A group that mixes two account currencies answers `currency: null` — the money in it was added across currencies and cannot be compared.\n\nSend `currency` — an ISO 4217 code such as `USD` — to make them comparable. AdCrunch converts each day at the European Central Bank rate **of that day**, before adding the rows up, and every row then answers that currency.\n\n## Advertisers\n\nOmit `advertiserId` to read every advertiser of your organization in one call. Send it to read one, and AdCrunch then also makes sure your organization owns it. An `advertiserId` your organization does not own answers an empty list, not a failure.\n\n## Paging\n\nThe answer is one page, `{ data, nextCursor }`, of at most `limit` rows (100 by default, 500 at most). When it holds `nextCursor`, more rows exist: send it back as `cursor`, with the same parameters, to get the next page.\n\nThe rows are in one order across every provider: the period (with an `interval`), then the advertiser and the entity of the breakdown, then the provider. So a page never splits one group, and the pages of a portfolio read hold no more than `limit` rows however many providers you connect.\n\nThe rows always come from what AdCrunch holds. A live read belongs to one advertiser, and no provider aggregates an organization.","x-adcrunch-providers":["meta","tiktok","gads","openai"],"operationId":"listInsights","parameters":[{"description":"Filter rows to a single advertiser. Short-circuits the per-org advertiser fan-out and verifies the advertiser belongs to the caller's organization.","in":"query","name":"advertiserId","required":false,"schema":{"description":"Filter rows to a single advertiser. Short-circuits the per-org advertiser fan-out and verifies the advertiser belongs to the caller's organization.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},{"description":"Group results by this level. Send a concept to read one level at every provider: `campaigns`, `ad-groups`, `ads`, `creatives` or `asset-groups`. For example, `ad-groups` reads Meta `adset`, TikTok `adgroup` and Google Ads `ad_group` together. Each row keeps the word of its provider in `type`. A concept is valid with or without `provider`, and a provider that does not have the concept adds no row. A provider-native entity type (`campaign`, `adset`, `adgroup`, `ad_group`, `ad_group_ad`, `ad`, …) reads the word of one provider. Below `campaign`, a native type needs `provider` or an entity filter. `provider` and `advertiser` group by the ad platform and by the ad account. Omit `breakdown` to aggregate every matching row into one result.","in":"query","name":"breakdown","required":false,"schema":{"description":"Group results by this level. Send a concept to read one level at every provider: `campaigns`, `ad-groups`, `ads`, `creatives` or `asset-groups`. For example, `ad-groups` reads Meta `adset`, TikTok `adgroup` and Google Ads `ad_group` together. Each row keeps the word of its provider in `type`. A concept is valid with or without `provider`, and a provider that does not have the concept adds no row. A provider-native entity type (`campaign`, `adset`, `adgroup`, `ad_group`, `ad_group_ad`, `ad`, …) reads the word of one provider. Below `campaign`, a native type needs `provider` or an entity filter. `provider` and `advertiser` group by the ad platform and by the ad account. Omit `breakdown` to aggregate every matching row into one result.","type":"string","enum":["campaigns","ad-groups","ads","creatives","asset-groups","provider","advertiser","campaign","adset","adgroup","ad_group","ad_group_ad","insertion_order","line_item","asset_group","keyword","audience","listing_group","ad","creative"]}},{"description":"Display currency (ISO 4217, e.g. `USD`). When provided, monetary metrics are converted from each row’s account currency using the ECB reference rate of the row’s own date, before any aggregation. Omit to get unconverted account-currency values — pass it whenever the organization has advertisers in more than one currency.","in":"query","name":"currency","required":false,"schema":{"description":"Display currency (ISO 4217, e.g. `USD`). When provided, monetary metrics are converted from each row’s account currency using the ECB reference rate of the row’s own date, before any aggregation. Omit to get unconverted account-currency values — pass it whenever the organization has advertisers in more than one currency.","type":"string","pattern":"^[A-Za-z]{3}$"}},{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","in":"query","name":"cursor","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"description":"Filter to a single entity (any type), or — combined with a concept or an entity-type `breakdown` — scope the breakdown to that entity’s subtree.","in":"query","name":"entityId","required":false,"schema":{"description":"Filter to a single entity (any type), or — combined with a concept or an entity-type `breakdown` — scope the breakdown to that entity’s subtree.","type":"string"}},{"description":"Filter rows to one level. Send a concept (`campaigns`, `ad-groups`, `ads`, `creatives` or `asset-groups`) to read that level at every provider, or a provider-native entity type to read the word of one provider.","in":"query","name":"entityType","required":false,"schema":{"description":"Filter rows to one level. Send a concept (`campaigns`, `ad-groups`, `ads`, `creatives` or `asset-groups`) to read that level at every provider, or a provider-native entity type to read the word of one provider.","type":"string"}},{"description":"Bucket the time series at this granularity. Omit to collapse to a single row per group.","in":"query","name":"interval","required":false,"schema":{"description":"Bucket the time series at this granularity. Omit to collapse to a single row per group.","type":"string","enum":["day","week","month"]}},{"description":"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`.","in":"query","name":"limit","required":false,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}},{"description":"Restrict to a single provider. Ignored when an entity filter (`advertiserId`, etc.) is provided — that filter already pins the provider.","in":"query","name":"provider","required":false,"schema":{"description":"Restrict to a single provider. Ignored when an entity filter (`advertiserId`, etc.) is provided — that filter already pins the provider.","anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]},{"type":"string","enum":["x"]},{"type":"string","enum":["openai"]}]}},{"in":"query","name":"relative","required":false,"schema":{"anyOf":[{"type":"string","enum":["last_15_days"]},{"type":"string","enum":["last_month"]},{"type":"string","enum":["last_7_days"]}]}},{"description":"Metric columns to include on each row, as a comma-separated string (e.g. `spend,clicks,ctr`) or repeated values. Defaults to `spend,impressions`.","in":"query","name":"select","required":false,"schema":{"description":"Metric columns to include on each row, as a comma-separated string (e.g. `spend,clicks,ctr`) or repeated values. Defaults to `spend,impressions`.","anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},{"in":"query","name":"since","required":false,"schema":{"type":"string"}},{"in":"query","name":"until","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"data":[{"advertiserId":"acc_1203456789012345","currency":"EUR","entityId":"120210000000000","impressions":184213,"provider":"meta","spend":1284.57,"type":"campaign"},{"advertiserId":"acc_4829301756","currency":"EUR","entityId":"20481523697","impressions":46902,"provider":"gads","spend":612.4,"type":"campaign"}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"description":"The advertiser this row belongs to, prefixed `acc_`. Present when `breakdown` is `advertiser`, a concept or an entity type.","type":"string"},"currency":{"nullable":true,"description":"The ISO 4217 currency the money in this row is expressed in. It is the account currency when every row in the group shares one, the currency you asked for when you sent `currency`, and **null** when the group mixes two or more account currencies. A null here means the money in this row was added up across currencies and cannot be compared — send `currency` to make it comparable.","type":"string"},"date":{"description":"The first day of the period, `YYYY-MM-DD`. Present only when you send `interval`. A week is dated by its Monday, a month by its first day.","type":"string"},"entityId":{"description":"The entity this row belongs to, as the bare id its provider gives it. Present when `breakdown` is a concept or an entity type.","type":"string"},"provider":{"description":"The ad platform: `meta`, `tiktok` or `gads`. Present when `breakdown` is set to anything. https://docs.adcrunch.dev/connect/providers lists every provider and what each one reports.","type":"string"},"type":{"description":"The provider entity type of `entityId`, for example `campaign`, `adset`, `adgroup`, `ad_group` or `ad`. Present when `breakdown` is a concept or an entity type. A concept does not change it: with `breakdown=ad-groups`, a Meta row reads `adset` and a TikTok row reads `adgroup`.","type":"string"},"spend":{"description":"Money spent, in the row’s `currency`.","type":"number"},"clicks":{"description":"Clicks.","type":"number"},"impressions":{"description":"Impressions.","type":"number"},"ctr":{"description":"Click-through rate, **as a percentage**: clicks ÷ impressions × 100. A `ctr` of 1.5 means 1.5%.","type":"number"},"cpc":{"description":"Cost per click: spend ÷ clicks.","type":"number"},"cpm":{"description":"Cost per thousand impressions: spend ÷ impressions × 1000.","type":"number"},"cpp":{"description":"Cost per thousand people reached: spend ÷ reach × 1000. Google Ads and OpenAI report no reach, so this reads 0 there.","type":"number"},"cpa":{"description":"Cost per conversion: spend ÷ conversions.","type":"number"},"roas":{"description":"Return on ad spend: conversion value ÷ spend.","type":"number"},"reach":{"description":"People reached. Google Ads and OpenAI do not report it, so it reads 0 there.","type":"number"},"frequency":{"description":"Impressions ÷ reach. Google Ads and OpenAI report no reach, so this reads 0 there.","type":"number"},"conversions":{"description":"Conversions, as the provider counts them.","type":"number"},"conversion_value":{"description":"The value attributed to those conversions, as money.","type":"number"},"action_add_to_cart":{"description":"Add-to-cart actions. Meta only; other providers read 0.","type":"number"},"action_add_to_cart_value":{"description":"The value attributed to those add-to-cart actions. Meta only.","type":"number"},"action_purchase":{"description":"Purchases. Meta only; other providers read 0.","type":"number"},"action_purchase_value":{"description":"The value attributed to those purchases. Meta only.","type":"number"},"action_initiate_checkout":{"description":"Checkouts started. Meta only; other providers read 0.","type":"number"},"action_initiate_checkout_value":{"description":"The value attributed to those checkouts. Meta only.","type":"number"}},"required":["currency"],"additionalProperties":false,"description":"One aggregated row. Which keys it carries is decided by `select`, `breakdown` and `interval` — see each field."},"description":"One row for each group. An empty array means no data matched, which is a normal answer and not a failure."},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"advertiserId":"acc_1203456789012345","currency":"EUR","entityId":"120210000000000","impressions":184213,"provider":"meta","spend":1284.57,"type":"campaign"},{"advertiserId":"acc_4829301756","currency":"EUR","entityId":"20481523697","impressions":46902,"provider":"gads","spend":612.4,"type":"campaign"}]}}}},"description":"One page of the matching rows. This page is the last one, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `observe:read`. `error` is `forbidden`."}},"summary":"Stored read: List insights","tags":["Insights"]}},"/mutations":{"post":{"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers)\n\nStarts a change on a live advertiser and answers immediately.\n\nThe change does **not** happen during this request. The answer carries a `workflowId`. Poll `GET /mutations/{id}` with it to find out how the change ended. A Mutation is durable, so the id stays valid across retries and restarts.\n\nA refused request starts nothing. AdCrunch refuses a body that does not match, an advertiser that is not yours, an advertiser with no usable connection, and an advertiser connected without write access before a Mutation exists. So a failed call leaves no row in the history.\n\n**This operation checks the credential first and the body second.** A missing or unusable credential answers `401` even when the body is wrong as well. A body that does not match then answers `400` with `error` of `invalid_request`, and `issues` names each field.\n\nA create never starts delivery: a campaign, an ad set and an ad all arrive **paused**, and so does a copy of a campaign or an ad set. This API also cannot delete, so `meta_set_status` with `ARCHIVED` is how an object is retired.","x-adcrunch-providers":["meta"],"operationId":"startMutation","responses":{"200":{"content":{"application/json":{"example":{"workflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94"},"schema":{"type":"object","properties":{"workflowId":{"type":"string","description":"The id of the Mutation. Poll GET /mutations/{id} with it to find out whether the change succeeded."}},"required":["workflowId"],"additionalProperties":false,"example":{"workflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94"}}}},"description":"The Mutation started. Poll it for the outcome."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["missing_write_access"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The caller does not hold `mutation:write`, or the advertiser is connected without write access. In the second case `error` is `missing_write_access` and `message` says how to fix it. AdCrunch started nothing."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The advertiser does not exist, it belongs to another organization, or it is not a Meta ad account. `error` is `not_found` in each case. An advertiser that does not exist and an advertiser of another organization get the same answer on purpose, so a caller cannot learn which ids exist. AdCrunch started nothing."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","description":"The ad platform of the advertiser. It is `meta` today."},"error":{"type":"string","enum":["provider_not_connected"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["provider","error","message"],"additionalProperties":false}}},"description":"The advertiser has no usable connection: a person removed it, it expired, or the provider refused it. A person must connect it again. `error` is `provider_not_connected`, the code that a live read of Observe sends for the same failure. AdCrunch started nothing."}},"summary":"Start a mutation","tags":["Mutations"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the object to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found."},"kind":{"type":"string","enum":["meta_set_status"]},"level":{"type":"string","enum":["campaign","adset","ad"],"description":"Which object the id refers to."},"status":{"type":"string","enum":["ACTIVE","PAUSED","ARCHIVED"],"description":"The status to set. ACTIVE starts delivery and starts spend. PAUSED stops delivery and can be reversed. ARCHIVED retires the object, and on Meta it is close to one-way: you cannot make an archived object active again from this API. Archive is the only way to retire an object, because this API never deletes one."}},"required":["id","kind","level","status"],"additionalProperties":false,"title":"meta_set_status"},{"type":"object","properties":{"dailyBudget":{"description":"The amount to spend each day. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give a daily budget or a lifetime budget, not both. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time.","type":"number","minimum":0,"exclusiveMinimum":true},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign or ad set to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found."},"kind":{"type":"string","enum":["meta_update_budget"]},"level":{"type":"string","enum":["campaign","adset"],"description":"Which object the id refers to. An ad has no budget of its own."},"lifetimeBudget":{"description":"The amount to spend across the whole schedule. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. A lifetime budget needs an end time on the ad set. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time.","type":"number","minimum":0,"exclusiveMinimum":true}},"required":["id","kind","level"],"additionalProperties":false,"description":"Change the daily or the lifetime budget of a campaign with Advantage campaign budget, or of an ad set. Before you change a budget, read its live amount: GET /observe/{advertiserId}/campaigns and GET /observe/{advertiserId}/ad-groups read Meta now. A daily budget is not a hard limit for one day: Meta can spend up to 175% of it on one day, and up to 7 times it in one week.","title":"meta_update_budget"},{"type":"object","properties":{"endTime":{"description":"When delivery stops, as an ISO 8601 timestamp in the future, for example 2026-12-31T23:59:00+01:00. Send null for no end. An ad set with a lifetime budget must keep an end time. A later end time adds days of spend, so the total spend of a daily budget increases. With no end, the ad set spends until you pause it, up to 7 times its daily budget in one week. With a lifetime budget, the total spend stays within the lifetime budget. An ACTIVE ad set that has ended delivers again when it gets a new end time or no end, with no status change. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time. To undo a schedule change, send the times that it replaced (priorValue). An end time that is not in the future cannot go back: Meta refuses it, and AdCrunch refuses it too. To stop an ad set after you extend its end, set its status to PAUSED with meta_set_status.","nullable":true,"type":"string"},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the ad set to change: the native id, digits only, not an AdCrunch id. The ad set must be in the ad account of the advertiser, else the change ends with not_found. A campaign has no schedule that this API can change."},"kind":{"type":"string","enum":["meta_update_schedule"]},"startTime":{"description":"When delivery starts, as an ISO 8601 timestamp. Meta gets it as you send it, also on an ad set that has started, and Meta decides what it does. Give startTime, endTime, or both.","type":"string"}},"required":["id","kind"],"additionalProperties":false,"title":"meta_update_schedule"},{"type":"object","properties":{"adsetBudgetSharing":{"description":"Let the ad sets share their budgets. Only when both campaign budgets are omitted; false when omitted. Each ad set can then share up to 20 % of its daily budget with the other ad sets of the campaign. Sharing works with daily budgets only. It needs a bidStrategy, and it locks that bid strategy for the life of the campaign. Sharing cannot be turned on later.","type":"boolean"},"bidStrategy":{"description":"How Meta bids. LOWEST_COST_WITHOUT_CAP (\"Highest volume\" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP (\"Bid cap\") takes bidCap. COST_CAP (\"Cost per result goal\") takes costCap. LOWEST_COST_WITH_MIN_ROAS (\"ROAS goal\") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. On a campaign, give only the strategy. It applies when the campaign carries a budget, or when adsetBudgetSharing is true, which requires it. Give the amount of a bid cap, a cost cap or a ROAS goal on each ad set, with the same bidStrategy. Omit it to let Meta apply its default strategy.","type":"string","enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"]},"dailyBudget":{"description":"The amount the campaign spends each day, shared across its ad sets. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give one of the two budgets, or neither. When each ad set carries its own budget, for example one ad set for each Line Item, omit both budgets.","type":"number","minimum":0,"exclusiveMinimum":true},"kind":{"type":"string","enum":["meta_create_campaign"]},"lifetimeBudget":{"description":"The amount the campaign spends across its whole schedule, shared across its ad sets. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give one of the two budgets, or neither. When each ad set carries its own budget, for example one ad set for each Line Item, omit both budgets.","type":"number","minimum":0,"exclusiveMinimum":true},"name":{"type":"string","minLength":1,"description":"The campaign name. It is visible in Meta Ads Manager."},"objective":{"type":"string","enum":["OUTCOME_AWARENESS","OUTCOME_ENGAGEMENT","OUTCOME_LEADS","OUTCOME_SALES","OUTCOME_TRAFFIC","OUTCOME_APP_PROMOTION"],"description":"The result the campaign optimizes for. It cannot be changed after the campaign is created, and it limits which optimization goals the ad sets below it can use."},"specialAdCategories":{"default":[],"description":"Declare a regulated category when the campaign advertises one: EMPLOYMENT, HOUSING, CREDIT, ISSUES_ELECTIONS_POLITICS, ONLINE_GAMBLING_AND_GAMING, or FINANCIAL_PRODUCTS_SERVICES. Meta restricts targeting for each of them, and a wrong declaration breaks the advertiser policy agreement. Send an empty array or NONE when none applies. Do not guess: ask the advertiser.","type":"array","items":{"type":"string","enum":["NONE","EMPLOYMENT","HOUSING","CREDIT","ISSUES_ELECTIONS_POLITICS","ONLINE_GAMBLING_AND_GAMING","FINANCIAL_PRODUCTS_SERVICES"]}}},"required":["kind","name","objective","specialAdCategories"],"additionalProperties":false,"title":"meta_create_campaign"},{"type":"object","properties":{"ageMax":{"description":"The oldest age to target, from 13 to 65. Omit it to target every age above the minimum. 65 and above is one group on Meta.","type":"integer","minimum":13,"maximum":65},"bidCap":{"description":"The highest bid Meta places in an auction. Only with LOWEST_COST_WITH_BID_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has.","type":"number","minimum":0,"exclusiveMinimum":true},"costCap":{"description":"The average cost per result that Meta tries to keep. Only with COST_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has. A cost cap can stop the ad set before it spends its full budget.","type":"number","minimum":0,"exclusiveMinimum":true},"roasFloor":{"description":"The lowest return on ad spend that Meta bids for, as a ratio: 1.5 means purchase value of 1.5 times the spend. Only with LOWEST_COST_WITH_MIN_ROAS. From 0.01 to 1000, with at most 4 decimals.","type":"number","minimum":0.01,"maximum":1000,"multipleOf":0.0001},"bidStrategy":{"description":"How Meta bids. LOWEST_COST_WITHOUT_CAP (\"Highest volume\" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP (\"Bid cap\") takes bidCap. COST_CAP (\"Cost per result goal\") takes costCap. LOWEST_COST_WITH_MIN_ROAS (\"ROAS goal\") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. When the campaign carries the budget, or shares the ad set budgets, the campaign holds the strategy: give the same bidStrategy here, with the amount for this ad set. Omit it, with every amount, to let Meta apply its default strategy.","type":"string","enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"]},"ageMin":{"type":"integer","minimum":13,"maximum":65,"description":"The youngest age to target, from 13 to 65. This field is required, and it is the one targeting field with no default. Many advertisers must not show ads to people under 18, and a default would spend their money on an audience they cannot legally address. State the age deliberately: send 18 to exclude minors."},"campaignId":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign this ad set belongs to, digits only. Create the campaign first and use the id it returns."},"countries":{"minItems":1,"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"description":"The countries to target, as two-letter ISO 3166-1 alpha-2 codes, for example US or GB. Give at least one."},"customEventType":{"description":"The conversion the pixel reports, for example PURCHASE. Required with a conversion optimization goal, and it must be sent together with pixelId.","type":"string","enum":["PURCHASE","LEAD","COMPLETE_REGISTRATION","ADD_TO_CART","INITIATED_CHECKOUT","ADD_PAYMENT_INFO","VIEW_CONTENT","SEARCH","SUBSCRIBE","START_TRIAL","CONTACT","OTHER"]},"dailyBudget":{"description":"The amount this ad set spends each day. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Omit both budgets when the campaign carries one. When the campaign carries none, give exactly one.","type":"number","minimum":0,"exclusiveMinimum":true},"endTime":{"description":"When delivery stops, as an ISO 8601 timestamp. Required with a lifetime budget.","type":"string"},"genders":{"default":"all","description":"Which genders to target. The default targets everybody.","type":"string","enum":["all","men","women"]},"kind":{"type":"string","enum":["meta_create_adset"]},"lifetimeBudget":{"description":"The amount this ad set spends across its whole schedule. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. It needs an end time.","type":"number","minimum":0,"exclusiveMinimum":true},"name":{"type":"string","minLength":1,"description":"The ad set name. It is visible in Meta Ads Manager."},"optimizationGoal":{"type":"string","enum":["IMPRESSIONS","REACH","LINK_CLICKS","LANDING_PAGE_VIEWS","OFFSITE_CONVERSIONS","POST_ENGAGEMENT","THRUPLAY","LEAD_GENERATION","VALUE"],"description":"What Meta optimizes delivery for. The campaign objective limits which goals are valid. A conversion goal also needs pixelId and customEventType. The billing event follows from this goal, so there is no separate field for it."},"pixelId":{"description":"The Meta pixel that reports conversions. Required with a conversion optimization goal. AdCrunch can list the pixels this ad account can use.","type":"string"},"startTime":{"description":"When delivery starts, as an ISO 8601 timestamp. Omit it to start when the ad set becomes active. The ad set is created paused either way.","type":"string"}},"required":["ageMin","campaignId","countries","genders","kind","name","optimizationGoal"],"additionalProperties":false,"title":"meta_create_adset"},{"type":"object","properties":{"adsetId":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the ad set this ad runs in, digits only. Create the ad set first and use the id it returns."},"creativeId":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the creative this ad shows, digits only. Create the creative first with meta_create_creative and use the id it returns. One creative can be used by more than one ad."},"kind":{"type":"string","enum":["meta_create_ad"]},"name":{"type":"string","minLength":1,"description":"The ad name. It is visible in Meta Ads Manager."}},"required":["adsetId","creativeId","kind","name"],"additionalProperties":false,"title":"meta_create_ad"},{"type":"object","properties":{"assetId":{"type":"string","pattern":"^ast_[\\s\\S]{0,}$","description":"The AdCrunch Asset to build the creative from, as the ast_ id that Asset registration returned. The Asset must already be registered to this advertiser. Do not send a Meta image hash or video id. Whether the creative becomes an image or a video follows from the Asset."},"callToAction":{"type":"string","enum":["LEARN_MORE","SHOP_NOW","SIGN_UP","BOOK_TRAVEL","DOWNLOAD","GET_OFFER","GET_QUOTE","CONTACT_US","SUBSCRIBE","APPLY_NOW","NO_BUTTON"],"description":"The label on the button, for example SHOP_NOW."},"description":{"description":"The text below the headline. Meta can truncate it.","type":"string"},"headline":{"type":"string","minLength":1,"description":"The short bold line next to the button."},"kind":{"type":"string","enum":["meta_create_creative"]},"link":{"type":"string","format":"uri","description":"The page the ad opens when somebody clicks it."},"message":{"type":"string","minLength":1,"description":"The main body text, shown above the media."},"name":{"type":"string","minLength":1,"description":"The creative name. It is visible in Meta Ads Manager, and it is not shown to a person who sees the ad."},"pageId":{"type":"string","description":"The Facebook Page the ad is published from. AdCrunch can list the Pages this ad account can publish from."}},"required":["assetId","callToAction","headline","kind","link","message","name","pageId"],"additionalProperties":false,"title":"meta_create_creative"},{"type":"object","properties":{"bidCap":{"description":"The highest bid Meta places in an auction. Only with LOWEST_COST_WITH_BID_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has.","type":"number","minimum":0,"exclusiveMinimum":true},"costCap":{"description":"The average cost per result that Meta tries to keep. Only with COST_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has. A cost cap can stop the ad set before it spends its full budget.","type":"number","minimum":0,"exclusiveMinimum":true},"roasFloor":{"description":"The lowest return on ad spend that Meta bids for, as a ratio: 1.5 means purchase value of 1.5 times the spend. Only with LOWEST_COST_WITH_MIN_ROAS. From 0.01 to 1000, with at most 4 decimals.","type":"number","minimum":0.01,"maximum":1000,"multipleOf":0.0001},"bidStrategy":{"type":"string","enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"],"description":"How Meta bids. LOWEST_COST_WITHOUT_CAP (\"Highest volume\" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP (\"Bid cap\") takes bidCap. COST_CAP (\"Cost per result goal\") takes costCap. LOWEST_COST_WITH_MIN_ROAS (\"ROAS goal\") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. Send the strategy also to change only an amount. A change of strategy starts the learning phase again, and a large change of amount can too. Meta refuses a change of strategy on a campaign with ad set budget sharing, on a campaign budget with more than 70 ad sets, and on a campaign budget on ROAS goal. Meta advises at most 2 or 3 bid or budget changes a day, and 15 minutes between a budget change and a bid change."},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign or ad set to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found."},"kind":{"type":"string","enum":["meta_update_bidding"]},"level":{"type":"string","enum":["campaign","adset"],"description":"Which object the id refers to. The strategy sits where the budget sits: on the campaign under a campaign budget, otherwise on each ad set. Under a campaign budget, an ad set takes only its amount: send the bidStrategy of the campaign with the amount for this ad set. Another strategy ends with wrong_level. A campaign without a campaign budget ends with wrong_level. On a campaign, a bid cap or a cost cap applies one amount to every ad set of the campaign. A ROAS floor sits on each ad set: on a campaign, LOWEST_COST_WITH_MIN_ROAS is refused."}},"required":["bidStrategy","id","kind","level"],"additionalProperties":false,"title":"meta_update_bidding"},{"type":"object","properties":{"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign to copy: the native id, digits only, not an AdCrunch id. The campaign must be in the ad account of the advertiser, else the copy ends with not_found. The copy stays in that ad account."},"kind":{"type":"string","enum":["meta_copy_campaign"]}},"required":["id","kind"],"additionalProperties":false,"description":"Copy a campaign. The copy has no ad set and no ad: create them with meta_copy_adset or meta_create_adset. A copy is shallow: Meta copies this one object with its settings, and none of the objects under it. The copy starts paused, and Meta adds \" - Copy\" to its name. To give the copy a full name, use `meta_rename`. The result gives the id of the copy and the budget that the copy carries, which is what an activate can spend. A copy starts its own learning phase. Meta refuses some copies: an Advantage+ shopping campaign and an Advantage+ app campaign cannot be copied, and a copy that targets the EU needs the default payor and the default beneficiary of the ad account.","title":"meta_copy_campaign"},{"type":"object","properties":{"campaignId":{"description":"The Meta id of the campaign that receives the copy, digits only. Omit it to keep the copy in the campaign of the source. The copy takes the settings of that campaign: under a campaign budget, it spends the campaign budget, else it keeps the budget of the source. An ad set with no budget of its own needs a campaign with a campaign budget, else the copy ends with invalid_request. Meta decides whether the campaign accepts the copy.","type":"string","pattern":"^\\d+$"},"endTime":{"description":"When delivery of the copy stops, as an ISO 8601 timestamp. Omit it to keep the end time of the source.","type":"string"},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the ad set to copy: the native id, digits only, not an AdCrunch id. The ad set must be in the ad account of the advertiser, else the copy ends with not_found."},"kind":{"type":"string","enum":["meta_copy_adset"]},"startTime":{"description":"When delivery of the copy starts, as an ISO 8601 timestamp. Omit it to keep the start time of the source. A copy of an ad set that has ended starts when Meta makes it, with the duration of the source.","type":"string"}},"required":["id","kind"],"additionalProperties":false,"description":"Copy an ad set. The copy has no ad: create its ads with meta_create_ad, with the creativeId of each source ad. A copy is shallow: Meta copies this one object with its settings, and none of the objects under it. The copy starts paused, and Meta adds \" - Copy\" to its name. To give the copy a full name, use `meta_rename`. The result gives the id of the copy and the budget that the copy carries, which is what an activate can spend. A copy starts its own learning phase. Meta refuses some copies: an Advantage+ shopping campaign and an Advantage+ app campaign cannot be copied, and a copy that targets the EU needs the default payor and the default beneficiary of the ad account.","title":"meta_copy_adset"},{"type":"object","properties":{"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the object to rename: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the rename ends with not_found."},"kind":{"type":"string","enum":["meta_rename"]},"level":{"type":"string","enum":["campaign","adset","ad"],"description":"Which object the id refers to."},"name":{"type":"string","minLength":1,"description":"The new name, which replaces the whole name. It is visible in Meta Ads Manager."}},"required":["id","kind","level","name"],"additionalProperties":false,"description":"Rename a campaign, an ad set or an ad. The rename changes the name and no other field. Meta names a copy with \" - Copy\" at the end: rename the copy to give it a full name.","title":"meta_rename"}],"description":"What to do. The kind field selects the action, and each kind takes its own fields."},"advertiserId":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The advertiser to act on, as an AdCrunch acc_ id. It must belong to your organization and be connected with write access."}},"required":["action","advertiserId"],"additionalProperties":false,"example":{"action":{"id":"120210000000000","kind":"meta_set_status","level":"campaign","status":"PAUSED"},"advertiserId":"acc_1203456789012345"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"action":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the object to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found."},"kind":{"type":"string","enum":["meta_set_status"]},"level":{"type":"string","enum":["campaign","adset","ad"],"description":"Which object the id refers to."},"status":{"type":"string","enum":["ACTIVE","PAUSED","ARCHIVED"],"description":"The status to set. ACTIVE starts delivery and starts spend. PAUSED stops delivery and can be reversed. ARCHIVED retires the object, and on Meta it is close to one-way: you cannot make an archived object active again from this API. Archive is the only way to retire an object, because this API never deletes one."}},"required":["id","kind","level","status"],"additionalProperties":false,"title":"meta_set_status"},{"type":"object","properties":{"dailyBudget":{"description":"The amount to spend each day. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give a daily budget or a lifetime budget, not both. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time.","type":"number","minimum":0,"exclusiveMinimum":true},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign or ad set to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found."},"kind":{"type":"string","enum":["meta_update_budget"]},"level":{"type":"string","enum":["campaign","adset"],"description":"Which object the id refers to. An ad has no budget of its own."},"lifetimeBudget":{"description":"The amount to spend across the whole schedule. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. A lifetime budget needs an end time on the ad set. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time.","type":"number","minimum":0,"exclusiveMinimum":true}},"required":["id","kind","level"],"additionalProperties":false,"description":"Change the daily or the lifetime budget of a campaign with Advantage campaign budget, or of an ad set. Before you change a budget, read its live amount: GET /observe/{advertiserId}/campaigns and GET /observe/{advertiserId}/ad-groups read Meta now. A daily budget is not a hard limit for one day: Meta can spend up to 175% of it on one day, and up to 7 times it in one week.","title":"meta_update_budget"},{"type":"object","properties":{"endTime":{"description":"When delivery stops, as an ISO 8601 timestamp in the future, for example 2026-12-31T23:59:00+01:00. Send null for no end. An ad set with a lifetime budget must keep an end time. A later end time adds days of spend, so the total spend of a daily budget increases. With no end, the ad set spends until you pause it, up to 7 times its daily budget in one week. With a lifetime budget, the total spend stays within the lifetime budget. An ACTIVE ad set that has ended delivers again when it gets a new end time or no end, with no status change. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time. To undo a schedule change, send the times that it replaced (priorValue). An end time that is not in the future cannot go back: Meta refuses it, and AdCrunch refuses it too. To stop an ad set after you extend its end, set its status to PAUSED with meta_set_status.","nullable":true,"type":"string"},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the ad set to change: the native id, digits only, not an AdCrunch id. The ad set must be in the ad account of the advertiser, else the change ends with not_found. A campaign has no schedule that this API can change."},"kind":{"type":"string","enum":["meta_update_schedule"]},"startTime":{"description":"When delivery starts, as an ISO 8601 timestamp. Meta gets it as you send it, also on an ad set that has started, and Meta decides what it does. Give startTime, endTime, or both.","type":"string"}},"required":["id","kind"],"additionalProperties":false,"title":"meta_update_schedule"},{"type":"object","properties":{"adsetBudgetSharing":{"description":"Let the ad sets share their budgets. Only when both campaign budgets are omitted; false when omitted. Each ad set can then share up to 20 % of its daily budget with the other ad sets of the campaign. Sharing works with daily budgets only. It needs a bidStrategy, and it locks that bid strategy for the life of the campaign. Sharing cannot be turned on later.","type":"boolean"},"bidStrategy":{"description":"How Meta bids. LOWEST_COST_WITHOUT_CAP (\"Highest volume\" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP (\"Bid cap\") takes bidCap. COST_CAP (\"Cost per result goal\") takes costCap. LOWEST_COST_WITH_MIN_ROAS (\"ROAS goal\") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. On a campaign, give only the strategy. It applies when the campaign carries a budget, or when adsetBudgetSharing is true, which requires it. Give the amount of a bid cap, a cost cap or a ROAS goal on each ad set, with the same bidStrategy. Omit it to let Meta apply its default strategy.","type":"string","enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"]},"dailyBudget":{"description":"The amount the campaign spends each day, shared across its ad sets. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give one of the two budgets, or neither. When each ad set carries its own budget, for example one ad set for each Line Item, omit both budgets.","type":"number","minimum":0,"exclusiveMinimum":true},"kind":{"type":"string","enum":["meta_create_campaign"]},"lifetimeBudget":{"description":"The amount the campaign spends across its whole schedule, shared across its ad sets. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give one of the two budgets, or neither. When each ad set carries its own budget, for example one ad set for each Line Item, omit both budgets.","type":"number","minimum":0,"exclusiveMinimum":true},"name":{"type":"string","minLength":1,"description":"The campaign name. It is visible in Meta Ads Manager."},"objective":{"type":"string","enum":["OUTCOME_AWARENESS","OUTCOME_ENGAGEMENT","OUTCOME_LEADS","OUTCOME_SALES","OUTCOME_TRAFFIC","OUTCOME_APP_PROMOTION"],"description":"The result the campaign optimizes for. It cannot be changed after the campaign is created, and it limits which optimization goals the ad sets below it can use."},"specialAdCategories":{"default":[],"description":"Declare a regulated category when the campaign advertises one: EMPLOYMENT, HOUSING, CREDIT, ISSUES_ELECTIONS_POLITICS, ONLINE_GAMBLING_AND_GAMING, or FINANCIAL_PRODUCTS_SERVICES. Meta restricts targeting for each of them, and a wrong declaration breaks the advertiser policy agreement. Send an empty array or NONE when none applies. Do not guess: ask the advertiser.","type":"array","items":{"type":"string","enum":["NONE","EMPLOYMENT","HOUSING","CREDIT","ISSUES_ELECTIONS_POLITICS","ONLINE_GAMBLING_AND_GAMING","FINANCIAL_PRODUCTS_SERVICES"]}}},"required":["kind","name","objective","specialAdCategories"],"additionalProperties":false,"title":"meta_create_campaign"},{"type":"object","properties":{"ageMax":{"description":"The oldest age to target, from 13 to 65. Omit it to target every age above the minimum. 65 and above is one group on Meta.","type":"integer","minimum":13,"maximum":65},"bidCap":{"description":"The highest bid Meta places in an auction. Only with LOWEST_COST_WITH_BID_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has.","type":"number","minimum":0,"exclusiveMinimum":true},"costCap":{"description":"The average cost per result that Meta tries to keep. Only with COST_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has. A cost cap can stop the ad set before it spends its full budget.","type":"number","minimum":0,"exclusiveMinimum":true},"roasFloor":{"description":"The lowest return on ad spend that Meta bids for, as a ratio: 1.5 means purchase value of 1.5 times the spend. Only with LOWEST_COST_WITH_MIN_ROAS. From 0.01 to 1000, with at most 4 decimals.","type":"number","minimum":0.01,"maximum":1000,"multipleOf":0.0001},"bidStrategy":{"description":"How Meta bids. LOWEST_COST_WITHOUT_CAP (\"Highest volume\" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP (\"Bid cap\") takes bidCap. COST_CAP (\"Cost per result goal\") takes costCap. LOWEST_COST_WITH_MIN_ROAS (\"ROAS goal\") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. When the campaign carries the budget, or shares the ad set budgets, the campaign holds the strategy: give the same bidStrategy here, with the amount for this ad set. Omit it, with every amount, to let Meta apply its default strategy.","type":"string","enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"]},"ageMin":{"type":"integer","minimum":13,"maximum":65,"description":"The youngest age to target, from 13 to 65. This field is required, and it is the one targeting field with no default. Many advertisers must not show ads to people under 18, and a default would spend their money on an audience they cannot legally address. State the age deliberately: send 18 to exclude minors."},"campaignId":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign this ad set belongs to, digits only. Create the campaign first and use the id it returns."},"countries":{"minItems":1,"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"description":"The countries to target, as two-letter ISO 3166-1 alpha-2 codes, for example US or GB. Give at least one."},"customEventType":{"description":"The conversion the pixel reports, for example PURCHASE. Required with a conversion optimization goal, and it must be sent together with pixelId.","type":"string","enum":["PURCHASE","LEAD","COMPLETE_REGISTRATION","ADD_TO_CART","INITIATED_CHECKOUT","ADD_PAYMENT_INFO","VIEW_CONTENT","SEARCH","SUBSCRIBE","START_TRIAL","CONTACT","OTHER"]},"dailyBudget":{"description":"The amount this ad set spends each day. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Omit both budgets when the campaign carries one. When the campaign carries none, give exactly one.","type":"number","minimum":0,"exclusiveMinimum":true},"endTime":{"description":"When delivery stops, as an ISO 8601 timestamp. Required with a lifetime budget.","type":"string"},"genders":{"default":"all","description":"Which genders to target. The default targets everybody.","type":"string","enum":["all","men","women"]},"kind":{"type":"string","enum":["meta_create_adset"]},"lifetimeBudget":{"description":"The amount this ad set spends across its whole schedule. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. It needs an end time.","type":"number","minimum":0,"exclusiveMinimum":true},"name":{"type":"string","minLength":1,"description":"The ad set name. It is visible in Meta Ads Manager."},"optimizationGoal":{"type":"string","enum":["IMPRESSIONS","REACH","LINK_CLICKS","LANDING_PAGE_VIEWS","OFFSITE_CONVERSIONS","POST_ENGAGEMENT","THRUPLAY","LEAD_GENERATION","VALUE"],"description":"What Meta optimizes delivery for. The campaign objective limits which goals are valid. A conversion goal also needs pixelId and customEventType. The billing event follows from this goal, so there is no separate field for it."},"pixelId":{"description":"The Meta pixel that reports conversions. Required with a conversion optimization goal. AdCrunch can list the pixels this ad account can use.","type":"string"},"startTime":{"description":"When delivery starts, as an ISO 8601 timestamp. Omit it to start when the ad set becomes active. The ad set is created paused either way.","type":"string"}},"required":["ageMin","campaignId","countries","genders","kind","name","optimizationGoal"],"additionalProperties":false,"title":"meta_create_adset"},{"type":"object","properties":{"adsetId":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the ad set this ad runs in, digits only. Create the ad set first and use the id it returns."},"creativeId":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the creative this ad shows, digits only. Create the creative first with meta_create_creative and use the id it returns. One creative can be used by more than one ad."},"kind":{"type":"string","enum":["meta_create_ad"]},"name":{"type":"string","minLength":1,"description":"The ad name. It is visible in Meta Ads Manager."}},"required":["adsetId","creativeId","kind","name"],"additionalProperties":false,"title":"meta_create_ad"},{"type":"object","properties":{"assetId":{"type":"string","pattern":"^ast_[\\s\\S]{0,}$","description":"The AdCrunch Asset to build the creative from, as the ast_ id that Asset registration returned. The Asset must already be registered to this advertiser. Do not send a Meta image hash or video id. Whether the creative becomes an image or a video follows from the Asset."},"callToAction":{"type":"string","enum":["LEARN_MORE","SHOP_NOW","SIGN_UP","BOOK_TRAVEL","DOWNLOAD","GET_OFFER","GET_QUOTE","CONTACT_US","SUBSCRIBE","APPLY_NOW","NO_BUTTON"],"description":"The label on the button, for example SHOP_NOW."},"description":{"description":"The text below the headline. Meta can truncate it.","type":"string"},"headline":{"type":"string","minLength":1,"description":"The short bold line next to the button."},"kind":{"type":"string","enum":["meta_create_creative"]},"link":{"type":"string","format":"uri","description":"The page the ad opens when somebody clicks it."},"message":{"type":"string","minLength":1,"description":"The main body text, shown above the media."},"name":{"type":"string","minLength":1,"description":"The creative name. It is visible in Meta Ads Manager, and it is not shown to a person who sees the ad."},"pageId":{"type":"string","description":"The Facebook Page the ad is published from. AdCrunch can list the Pages this ad account can publish from."}},"required":["assetId","callToAction","headline","kind","link","message","name","pageId"],"additionalProperties":false,"title":"meta_create_creative"},{"type":"object","properties":{"bidCap":{"description":"The highest bid Meta places in an auction. Only with LOWEST_COST_WITH_BID_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has.","type":"number","minimum":0,"exclusiveMinimum":true},"costCap":{"description":"The average cost per result that Meta tries to keep. Only with COST_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has. A cost cap can stop the ad set before it spends its full budget.","type":"number","minimum":0,"exclusiveMinimum":true},"roasFloor":{"description":"The lowest return on ad spend that Meta bids for, as a ratio: 1.5 means purchase value of 1.5 times the spend. Only with LOWEST_COST_WITH_MIN_ROAS. From 0.01 to 1000, with at most 4 decimals.","type":"number","minimum":0.01,"maximum":1000,"multipleOf":0.0001},"bidStrategy":{"type":"string","enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"],"description":"How Meta bids. LOWEST_COST_WITHOUT_CAP (\"Highest volume\" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP (\"Bid cap\") takes bidCap. COST_CAP (\"Cost per result goal\") takes costCap. LOWEST_COST_WITH_MIN_ROAS (\"ROAS goal\") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. Send the strategy also to change only an amount. A change of strategy starts the learning phase again, and a large change of amount can too. Meta refuses a change of strategy on a campaign with ad set budget sharing, on a campaign budget with more than 70 ad sets, and on a campaign budget on ROAS goal. Meta advises at most 2 or 3 bid or budget changes a day, and 15 minutes between a budget change and a bid change."},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign or ad set to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found."},"kind":{"type":"string","enum":["meta_update_bidding"]},"level":{"type":"string","enum":["campaign","adset"],"description":"Which object the id refers to. The strategy sits where the budget sits: on the campaign under a campaign budget, otherwise on each ad set. Under a campaign budget, an ad set takes only its amount: send the bidStrategy of the campaign with the amount for this ad set. Another strategy ends with wrong_level. A campaign without a campaign budget ends with wrong_level. On a campaign, a bid cap or a cost cap applies one amount to every ad set of the campaign. A ROAS floor sits on each ad set: on a campaign, LOWEST_COST_WITH_MIN_ROAS is refused."}},"required":["bidStrategy","id","kind","level"],"additionalProperties":false,"title":"meta_update_bidding"},{"type":"object","properties":{"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign to copy: the native id, digits only, not an AdCrunch id. The campaign must be in the ad account of the advertiser, else the copy ends with not_found. The copy stays in that ad account."},"kind":{"type":"string","enum":["meta_copy_campaign"]}},"required":["id","kind"],"additionalProperties":false,"description":"Copy a campaign. The copy has no ad set and no ad: create them with meta_copy_adset or meta_create_adset. A copy is shallow: Meta copies this one object with its settings, and none of the objects under it. The copy starts paused, and Meta adds \" - Copy\" to its name. To give the copy a full name, use `meta_rename`. The result gives the id of the copy and the budget that the copy carries, which is what an activate can spend. A copy starts its own learning phase. Meta refuses some copies: an Advantage+ shopping campaign and an Advantage+ app campaign cannot be copied, and a copy that targets the EU needs the default payor and the default beneficiary of the ad account.","title":"meta_copy_campaign"},{"type":"object","properties":{"campaignId":{"description":"The Meta id of the campaign that receives the copy, digits only. Omit it to keep the copy in the campaign of the source. The copy takes the settings of that campaign: under a campaign budget, it spends the campaign budget, else it keeps the budget of the source. An ad set with no budget of its own needs a campaign with a campaign budget, else the copy ends with invalid_request. Meta decides whether the campaign accepts the copy.","type":"string","pattern":"^\\d+$"},"endTime":{"description":"When delivery of the copy stops, as an ISO 8601 timestamp. Omit it to keep the end time of the source.","type":"string"},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the ad set to copy: the native id, digits only, not an AdCrunch id. The ad set must be in the ad account of the advertiser, else the copy ends with not_found."},"kind":{"type":"string","enum":["meta_copy_adset"]},"startTime":{"description":"When delivery of the copy starts, as an ISO 8601 timestamp. Omit it to keep the start time of the source. A copy of an ad set that has ended starts when Meta makes it, with the duration of the source.","type":"string"}},"required":["id","kind"],"additionalProperties":false,"description":"Copy an ad set. The copy has no ad: create its ads with meta_create_ad, with the creativeId of each source ad. A copy is shallow: Meta copies this one object with its settings, and none of the objects under it. The copy starts paused, and Meta adds \" - Copy\" to its name. To give the copy a full name, use `meta_rename`. The result gives the id of the copy and the budget that the copy carries, which is what an activate can spend. A copy starts its own learning phase. Meta refuses some copies: an Advantage+ shopping campaign and an Advantage+ app campaign cannot be copied, and a copy that targets the EU needs the default payor and the default beneficiary of the ad account.","title":"meta_copy_adset"},{"type":"object","properties":{"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the object to rename: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the rename ends with not_found."},"kind":{"type":"string","enum":["meta_rename"]},"level":{"type":"string","enum":["campaign","adset","ad"],"description":"Which object the id refers to."},"name":{"type":"string","minLength":1,"description":"The new name, which replaces the whole name. It is visible in Meta Ads Manager."}},"required":["id","kind","level","name"],"additionalProperties":false,"description":"Rename a campaign, an ad set or an ad. The rename changes the name and no other field. Meta names a copy with \" - Copy\" at the end: rename the copy to give it a full name.","title":"meta_rename"}],"description":"What to do. The kind field selects the action, and each kind takes its own fields."},"advertiserId":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The advertiser to act on, as an AdCrunch acc_ id. It must belong to your organization and be connected with write access."}},"required":["action","advertiserId"],"additionalProperties":false,"example":{"action":{"id":"120210000000000","kind":"meta_set_status","level":"campaign","status":"PAUSED"},"advertiserId":"acc_1203456789012345"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"action":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the object to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found."},"kind":{"type":"string","enum":["meta_set_status"]},"level":{"type":"string","enum":["campaign","adset","ad"],"description":"Which object the id refers to."},"status":{"type":"string","enum":["ACTIVE","PAUSED","ARCHIVED"],"description":"The status to set. ACTIVE starts delivery and starts spend. PAUSED stops delivery and can be reversed. ARCHIVED retires the object, and on Meta it is close to one-way: you cannot make an archived object active again from this API. Archive is the only way to retire an object, because this API never deletes one."}},"required":["id","kind","level","status"],"additionalProperties":false,"title":"meta_set_status"},{"type":"object","properties":{"dailyBudget":{"description":"The amount to spend each day. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give a daily budget or a lifetime budget, not both. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time.","type":"number","minimum":0,"exclusiveMinimum":true},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign or ad set to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found."},"kind":{"type":"string","enum":["meta_update_budget"]},"level":{"type":"string","enum":["campaign","adset"],"description":"Which object the id refers to. An ad has no budget of its own."},"lifetimeBudget":{"description":"The amount to spend across the whole schedule. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. A lifetime budget needs an end time on the ad set. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time.","type":"number","minimum":0,"exclusiveMinimum":true}},"required":["id","kind","level"],"additionalProperties":false,"description":"Change the daily or the lifetime budget of a campaign with Advantage campaign budget, or of an ad set. Before you change a budget, read its live amount: GET /observe/{advertiserId}/campaigns and GET /observe/{advertiserId}/ad-groups read Meta now. A daily budget is not a hard limit for one day: Meta can spend up to 175% of it on one day, and up to 7 times it in one week.","title":"meta_update_budget"},{"type":"object","properties":{"endTime":{"description":"When delivery stops, as an ISO 8601 timestamp in the future, for example 2026-12-31T23:59:00+01:00. Send null for no end. An ad set with a lifetime budget must keep an end time. A later end time adds days of spend, so the total spend of a daily budget increases. With no end, the ad set spends until you pause it, up to 7 times its daily budget in one week. With a lifetime budget, the total spend stays within the lifetime budget. An ACTIVE ad set that has ended delivers again when it gets a new end time or no end, with no status change. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time. To undo a schedule change, send the times that it replaced (priorValue). An end time that is not in the future cannot go back: Meta refuses it, and AdCrunch refuses it too. To stop an ad set after you extend its end, set its status to PAUSED with meta_set_status.","nullable":true,"type":"string"},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the ad set to change: the native id, digits only, not an AdCrunch id. The ad set must be in the ad account of the advertiser, else the change ends with not_found. A campaign has no schedule that this API can change."},"kind":{"type":"string","enum":["meta_update_schedule"]},"startTime":{"description":"When delivery starts, as an ISO 8601 timestamp. Meta gets it as you send it, also on an ad set that has started, and Meta decides what it does. Give startTime, endTime, or both.","type":"string"}},"required":["id","kind"],"additionalProperties":false,"title":"meta_update_schedule"},{"type":"object","properties":{"adsetBudgetSharing":{"description":"Let the ad sets share their budgets. Only when both campaign budgets are omitted; false when omitted. Each ad set can then share up to 20 % of its daily budget with the other ad sets of the campaign. Sharing works with daily budgets only. It needs a bidStrategy, and it locks that bid strategy for the life of the campaign. Sharing cannot be turned on later.","type":"boolean"},"bidStrategy":{"description":"How Meta bids. LOWEST_COST_WITHOUT_CAP (\"Highest volume\" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP (\"Bid cap\") takes bidCap. COST_CAP (\"Cost per result goal\") takes costCap. LOWEST_COST_WITH_MIN_ROAS (\"ROAS goal\") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. On a campaign, give only the strategy. It applies when the campaign carries a budget, or when adsetBudgetSharing is true, which requires it. Give the amount of a bid cap, a cost cap or a ROAS goal on each ad set, with the same bidStrategy. Omit it to let Meta apply its default strategy.","type":"string","enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"]},"dailyBudget":{"description":"The amount the campaign spends each day, shared across its ad sets. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give one of the two budgets, or neither. When each ad set carries its own budget, for example one ad set for each Line Item, omit both budgets.","type":"number","minimum":0,"exclusiveMinimum":true},"kind":{"type":"string","enum":["meta_create_campaign"]},"lifetimeBudget":{"description":"The amount the campaign spends across its whole schedule, shared across its ad sets. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give one of the two budgets, or neither. When each ad set carries its own budget, for example one ad set for each Line Item, omit both budgets.","type":"number","minimum":0,"exclusiveMinimum":true},"name":{"type":"string","minLength":1,"description":"The campaign name. It is visible in Meta Ads Manager."},"objective":{"type":"string","enum":["OUTCOME_AWARENESS","OUTCOME_ENGAGEMENT","OUTCOME_LEADS","OUTCOME_SALES","OUTCOME_TRAFFIC","OUTCOME_APP_PROMOTION"],"description":"The result the campaign optimizes for. It cannot be changed after the campaign is created, and it limits which optimization goals the ad sets below it can use."},"specialAdCategories":{"default":[],"description":"Declare a regulated category when the campaign advertises one: EMPLOYMENT, HOUSING, CREDIT, ISSUES_ELECTIONS_POLITICS, ONLINE_GAMBLING_AND_GAMING, or FINANCIAL_PRODUCTS_SERVICES. Meta restricts targeting for each of them, and a wrong declaration breaks the advertiser policy agreement. Send an empty array or NONE when none applies. Do not guess: ask the advertiser.","type":"array","items":{"type":"string","enum":["NONE","EMPLOYMENT","HOUSING","CREDIT","ISSUES_ELECTIONS_POLITICS","ONLINE_GAMBLING_AND_GAMING","FINANCIAL_PRODUCTS_SERVICES"]}}},"required":["kind","name","objective","specialAdCategories"],"additionalProperties":false,"title":"meta_create_campaign"},{"type":"object","properties":{"ageMax":{"description":"The oldest age to target, from 13 to 65. Omit it to target every age above the minimum. 65 and above is one group on Meta.","type":"integer","minimum":13,"maximum":65},"bidCap":{"description":"The highest bid Meta places in an auction. Only with LOWEST_COST_WITH_BID_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has.","type":"number","minimum":0,"exclusiveMinimum":true},"costCap":{"description":"The average cost per result that Meta tries to keep. Only with COST_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has. A cost cap can stop the ad set before it spends its full budget.","type":"number","minimum":0,"exclusiveMinimum":true},"roasFloor":{"description":"The lowest return on ad spend that Meta bids for, as a ratio: 1.5 means purchase value of 1.5 times the spend. Only with LOWEST_COST_WITH_MIN_ROAS. From 0.01 to 1000, with at most 4 decimals.","type":"number","minimum":0.01,"maximum":1000,"multipleOf":0.0001},"bidStrategy":{"description":"How Meta bids. LOWEST_COST_WITHOUT_CAP (\"Highest volume\" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP (\"Bid cap\") takes bidCap. COST_CAP (\"Cost per result goal\") takes costCap. LOWEST_COST_WITH_MIN_ROAS (\"ROAS goal\") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. When the campaign carries the budget, or shares the ad set budgets, the campaign holds the strategy: give the same bidStrategy here, with the amount for this ad set. Omit it, with every amount, to let Meta apply its default strategy.","type":"string","enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"]},"ageMin":{"type":"integer","minimum":13,"maximum":65,"description":"The youngest age to target, from 13 to 65. This field is required, and it is the one targeting field with no default. Many advertisers must not show ads to people under 18, and a default would spend their money on an audience they cannot legally address. State the age deliberately: send 18 to exclude minors."},"campaignId":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign this ad set belongs to, digits only. Create the campaign first and use the id it returns."},"countries":{"minItems":1,"type":"array","items":{"type":"string","minLength":2,"maxLength":2},"description":"The countries to target, as two-letter ISO 3166-1 alpha-2 codes, for example US or GB. Give at least one."},"customEventType":{"description":"The conversion the pixel reports, for example PURCHASE. Required with a conversion optimization goal, and it must be sent together with pixelId.","type":"string","enum":["PURCHASE","LEAD","COMPLETE_REGISTRATION","ADD_TO_CART","INITIATED_CHECKOUT","ADD_PAYMENT_INFO","VIEW_CONTENT","SEARCH","SUBSCRIBE","START_TRIAL","CONTACT","OTHER"]},"dailyBudget":{"description":"The amount this ad set spends each day. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Omit both budgets when the campaign carries one. When the campaign carries none, give exactly one.","type":"number","minimum":0,"exclusiveMinimum":true},"endTime":{"description":"When delivery stops, as an ISO 8601 timestamp. Required with a lifetime budget.","type":"string"},"genders":{"default":"all","description":"Which genders to target. The default targets everybody.","type":"string","enum":["all","men","women"]},"kind":{"type":"string","enum":["meta_create_adset"]},"lifetimeBudget":{"description":"The amount this ad set spends across its whole schedule. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. It needs an end time.","type":"number","minimum":0,"exclusiveMinimum":true},"name":{"type":"string","minLength":1,"description":"The ad set name. It is visible in Meta Ads Manager."},"optimizationGoal":{"type":"string","enum":["IMPRESSIONS","REACH","LINK_CLICKS","LANDING_PAGE_VIEWS","OFFSITE_CONVERSIONS","POST_ENGAGEMENT","THRUPLAY","LEAD_GENERATION","VALUE"],"description":"What Meta optimizes delivery for. The campaign objective limits which goals are valid. A conversion goal also needs pixelId and customEventType. The billing event follows from this goal, so there is no separate field for it."},"pixelId":{"description":"The Meta pixel that reports conversions. Required with a conversion optimization goal. AdCrunch can list the pixels this ad account can use.","type":"string"},"startTime":{"description":"When delivery starts, as an ISO 8601 timestamp. Omit it to start when the ad set becomes active. The ad set is created paused either way.","type":"string"}},"required":["ageMin","campaignId","countries","genders","kind","name","optimizationGoal"],"additionalProperties":false,"title":"meta_create_adset"},{"type":"object","properties":{"adsetId":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the ad set this ad runs in, digits only. Create the ad set first and use the id it returns."},"creativeId":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the creative this ad shows, digits only. Create the creative first with meta_create_creative and use the id it returns. One creative can be used by more than one ad."},"kind":{"type":"string","enum":["meta_create_ad"]},"name":{"type":"string","minLength":1,"description":"The ad name. It is visible in Meta Ads Manager."}},"required":["adsetId","creativeId","kind","name"],"additionalProperties":false,"title":"meta_create_ad"},{"type":"object","properties":{"assetId":{"type":"string","pattern":"^ast_[\\s\\S]{0,}$","description":"The AdCrunch Asset to build the creative from, as the ast_ id that Asset registration returned. The Asset must already be registered to this advertiser. Do not send a Meta image hash or video id. Whether the creative becomes an image or a video follows from the Asset."},"callToAction":{"type":"string","enum":["LEARN_MORE","SHOP_NOW","SIGN_UP","BOOK_TRAVEL","DOWNLOAD","GET_OFFER","GET_QUOTE","CONTACT_US","SUBSCRIBE","APPLY_NOW","NO_BUTTON"],"description":"The label on the button, for example SHOP_NOW."},"description":{"description":"The text below the headline. Meta can truncate it.","type":"string"},"headline":{"type":"string","minLength":1,"description":"The short bold line next to the button."},"kind":{"type":"string","enum":["meta_create_creative"]},"link":{"type":"string","format":"uri","description":"The page the ad opens when somebody clicks it."},"message":{"type":"string","minLength":1,"description":"The main body text, shown above the media."},"name":{"type":"string","minLength":1,"description":"The creative name. It is visible in Meta Ads Manager, and it is not shown to a person who sees the ad."},"pageId":{"type":"string","description":"The Facebook Page the ad is published from. AdCrunch can list the Pages this ad account can publish from."}},"required":["assetId","callToAction","headline","kind","link","message","name","pageId"],"additionalProperties":false,"title":"meta_create_creative"},{"type":"object","properties":{"bidCap":{"description":"The highest bid Meta places in an auction. Only with LOWEST_COST_WITH_BID_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has.","type":"number","minimum":0,"exclusiveMinimum":true},"costCap":{"description":"The average cost per result that Meta tries to keep. Only with COST_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has. A cost cap can stop the ad set before it spends its full budget.","type":"number","minimum":0,"exclusiveMinimum":true},"roasFloor":{"description":"The lowest return on ad spend that Meta bids for, as a ratio: 1.5 means purchase value of 1.5 times the spend. Only with LOWEST_COST_WITH_MIN_ROAS. From 0.01 to 1000, with at most 4 decimals.","type":"number","minimum":0.01,"maximum":1000,"multipleOf":0.0001},"bidStrategy":{"type":"string","enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"],"description":"How Meta bids. LOWEST_COST_WITHOUT_CAP (\"Highest volume\" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP (\"Bid cap\") takes bidCap. COST_CAP (\"Cost per result goal\") takes costCap. LOWEST_COST_WITH_MIN_ROAS (\"ROAS goal\") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. Send the strategy also to change only an amount. A change of strategy starts the learning phase again, and a large change of amount can too. Meta refuses a change of strategy on a campaign with ad set budget sharing, on a campaign budget with more than 70 ad sets, and on a campaign budget on ROAS goal. Meta advises at most 2 or 3 bid or budget changes a day, and 15 minutes between a budget change and a bid change."},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign or ad set to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found."},"kind":{"type":"string","enum":["meta_update_bidding"]},"level":{"type":"string","enum":["campaign","adset"],"description":"Which object the id refers to. The strategy sits where the budget sits: on the campaign under a campaign budget, otherwise on each ad set. Under a campaign budget, an ad set takes only its amount: send the bidStrategy of the campaign with the amount for this ad set. Another strategy ends with wrong_level. A campaign without a campaign budget ends with wrong_level. On a campaign, a bid cap or a cost cap applies one amount to every ad set of the campaign. A ROAS floor sits on each ad set: on a campaign, LOWEST_COST_WITH_MIN_ROAS is refused."}},"required":["bidStrategy","id","kind","level"],"additionalProperties":false,"title":"meta_update_bidding"},{"type":"object","properties":{"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the campaign to copy: the native id, digits only, not an AdCrunch id. The campaign must be in the ad account of the advertiser, else the copy ends with not_found. The copy stays in that ad account."},"kind":{"type":"string","enum":["meta_copy_campaign"]}},"required":["id","kind"],"additionalProperties":false,"description":"Copy a campaign. The copy has no ad set and no ad: create them with meta_copy_adset or meta_create_adset. A copy is shallow: Meta copies this one object with its settings, and none of the objects under it. The copy starts paused, and Meta adds \" - Copy\" to its name. To give the copy a full name, use `meta_rename`. The result gives the id of the copy and the budget that the copy carries, which is what an activate can spend. A copy starts its own learning phase. Meta refuses some copies: an Advantage+ shopping campaign and an Advantage+ app campaign cannot be copied, and a copy that targets the EU needs the default payor and the default beneficiary of the ad account.","title":"meta_copy_campaign"},{"type":"object","properties":{"campaignId":{"description":"The Meta id of the campaign that receives the copy, digits only. Omit it to keep the copy in the campaign of the source. The copy takes the settings of that campaign: under a campaign budget, it spends the campaign budget, else it keeps the budget of the source. An ad set with no budget of its own needs a campaign with a campaign budget, else the copy ends with invalid_request. Meta decides whether the campaign accepts the copy.","type":"string","pattern":"^\\d+$"},"endTime":{"description":"When delivery of the copy stops, as an ISO 8601 timestamp. Omit it to keep the end time of the source.","type":"string"},"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the ad set to copy: the native id, digits only, not an AdCrunch id. The ad set must be in the ad account of the advertiser, else the copy ends with not_found."},"kind":{"type":"string","enum":["meta_copy_adset"]},"startTime":{"description":"When delivery of the copy starts, as an ISO 8601 timestamp. Omit it to keep the start time of the source. A copy of an ad set that has ended starts when Meta makes it, with the duration of the source.","type":"string"}},"required":["id","kind"],"additionalProperties":false,"description":"Copy an ad set. The copy has no ad: create its ads with meta_create_ad, with the creativeId of each source ad. A copy is shallow: Meta copies this one object with its settings, and none of the objects under it. The copy starts paused, and Meta adds \" - Copy\" to its name. To give the copy a full name, use `meta_rename`. The result gives the id of the copy and the budget that the copy carries, which is what an activate can spend. A copy starts its own learning phase. Meta refuses some copies: an Advantage+ shopping campaign and an Advantage+ app campaign cannot be copied, and a copy that targets the EU needs the default payor and the default beneficiary of the ad account.","title":"meta_copy_adset"},{"type":"object","properties":{"id":{"type":"string","pattern":"^\\d+$","description":"The Meta id of the object to rename: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the rename ends with not_found."},"kind":{"type":"string","enum":["meta_rename"]},"level":{"type":"string","enum":["campaign","adset","ad"],"description":"Which object the id refers to."},"name":{"type":"string","minLength":1,"description":"The new name, which replaces the whole name. It is visible in Meta Ads Manager."}},"required":["id","kind","level","name"],"additionalProperties":false,"description":"Rename a campaign, an ad set or an ad. The rename changes the name and no other field. Meta names a copy with \" - Copy\" at the end: rename the copy to give it a full name.","title":"meta_rename"}],"description":"What to do. The kind field selects the action, and each kind takes its own fields."},"advertiserId":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The advertiser to act on, as an AdCrunch acc_ id. It must belong to your organization and be connected with write access."}},"required":["action","advertiserId"],"additionalProperties":false,"example":{"action":{"id":"120210000000000","kind":"meta_set_status","level":"campaign","status":"PAUSED"},"advertiserId":"acc_1203456789012345"}}}}}},"get":{"description":"Lists your organization's Mutations, newest first.\n\nOne page at a time: `limit` rows at most, 100 by default. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page. When it is absent, you have the last page. The cursor is opaque: send it back with no change, and do not build one.\n\nA row that is still `running` is checked against the change itself as the page is read, so a Mutation that finished after its row was written shows its true outcome here.\n\nThis is the history read, and it needs `mutation:read`. Starting a Mutation and polling the one you started need `mutation:write`.","operationId":"listMutations","responses":{"200":{"content":{"application/json":{"example":{"data":[{"action":{"dailyBudget":50,"kind":"meta_create_campaign","name":"Spring sale prospecting","objective":"OUTCOME_SALES","specialAdCategories":[]},"advertiserId":"acc_1203456789012345","completedAt":null,"createdAt":1790000060000,"organizationId":"org_3a7f10","priorValue":null,"provider":"meta","result":null,"status":"running","tool":"meta_create_campaign","userId":"usr_4e8b23","workflowId":"7e5a2d19-0c4b-4e6a-8f31-5b9d7a02c6e8"},{"action":{"id":"120210000000000","kind":"meta_set_status","level":"campaign","status":"PAUSED"},"advertiserId":"acc_1203456789012345","completedAt":1790000004000,"createdAt":1790000000000,"organizationId":"org_3a7f10","priorValue":{"status":"ACTIVE"},"provider":"meta","result":{"ok":true,"result":{"success":true}},"status":"complete","tool":"meta_set_status","userId":"usr_4e8b23","workflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94"}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"action":{"description":"What was asked for, in the shape POST /mutations accepts."},"advertiserId":{"type":"string","description":"The advertiser that was changed."},"completedAt":{"nullable":true,"description":"When it finished, as a Unix timestamp in milliseconds. Null while it runs.","type":"number"},"createdAt":{"type":"number","description":"When it started, as a Unix timestamp in milliseconds."},"organizationId":{"type":"string","description":"The organization it belongs to."},"priorValue":{"nullable":true,"type":"object","additionalProperties":{},"description":"The values that the change replaced, read from the provider just before the change. It uses the field names and the units of action, for example {\"dailyBudget\": 800}, {\"status\": \"ACTIVE\"}, {\"bidStrategy\": \"COST_CAP\", \"costCap\": 4}, {\"startTime\": \"2026-10-01T00:00:00+0200\", \"endTime\": null} or {\"name\": \"Spring sale\"}. To undo the change, send the same kind again with these values. A bid change on a campaign also gives the amount of each ad set in adsets: undo it on each ad set. A create and a copy have none."},"provider":{"type":"string","description":"The ad platform the advertiser belongs to. It is meta today."},"result":{"nullable":true,"description":"How the change ended, in the shape GET /mutations/{id} answers. Null while the Mutation runs."},"status":{"type":"string","enum":["running","complete","errored"],"description":"How it ended, or that it has not."},"tool":{"type":"string","description":"Which action was asked for, for example meta_set_status. It is the kind of the action above."},"userId":{"type":"string","description":"Who asked for it."},"workflowId":{"type":"string","description":"The Mutation id. The same id POST /mutations returned."}},"required":["action","advertiserId","completedAt","createdAt","organizationId","priorValue","provider","result","status","tool","userId","workflowId"],"additionalProperties":false}},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"action":{"dailyBudget":50,"kind":"meta_create_campaign","name":"Spring sale prospecting","objective":"OUTCOME_SALES","specialAdCategories":[]},"advertiserId":"acc_1203456789012345","completedAt":null,"createdAt":1790000060000,"organizationId":"org_3a7f10","priorValue":null,"provider":"meta","result":null,"status":"running","tool":"meta_create_campaign","userId":"usr_4e8b23","workflowId":"7e5a2d19-0c4b-4e6a-8f31-5b9d7a02c6e8"},{"action":{"id":"120210000000000","kind":"meta_set_status","level":"campaign","status":"PAUSED"},"advertiserId":"acc_1203456789012345","completedAt":1790000004000,"createdAt":1790000000000,"organizationId":"org_3a7f10","priorValue":{"status":"ACTIVE"},"provider":"meta","result":{"ok":true,"result":{"success":true}},"status":"complete","tool":"meta_set_status","userId":"usr_4e8b23","workflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94"}]}}}},"description":"One page of Mutations, newest first. This one is the last page, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `mutation:read`. `error` is `forbidden`."}},"summary":"List mutations","tags":["Mutations"],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":true,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}}]}},"/mutations/{id}":{"get":{"description":"Reports how a Mutation ended, or that it has not ended.\n\n`running` means the change has not finished. Poll again.\n\n`complete` means the change finished, and `result` says how. `result.ok` of `true` carries what the provider gave back: a create carries the new object id, a copy carries the id of the copy and the `budget` that the copy carries, and every other action carries `success`. `result.ok` of `false` is a provider refusal, with a stable `error` code and a sentence. A refusal is `complete`, not `errored`.\n\n`errored` means the Mutation itself broke down. AdCrunch cannot say whether the provider applied the change, so read the object at the provider before you send the action again.\n\n`priorValue` is present for a change to an object that exists, in each status. It holds the values that the change replaced, read from the provider just before the change, in the field names and units of the action: `{\"dailyBudget\": 800}` for a budget, `{\"status\": \"ACTIVE\"}` for a status. To undo the change, send the same kind again with these values. A create and a copy have none.\n\nA Mutation belongs to one organization. An id from another organization and an id that never existed both answer `404`, so a caller cannot learn which of the two it sent.\n\nThis needs `mutation:write`, not `mutation:read`. Polling is the second half of the write that started the Mutation, and the caller polling is the caller that started it. `mutation:read` reads the organization history through `GET /mutations`.","operationId":"getMutationStatus","responses":{"200":{"content":{"application/json":{"example":{"priorValue":{"status":"ACTIVE"},"result":{"ok":true,"result":{"success":true}},"status":"complete"},"schema":{"type":"object","properties":{"priorValue":{"type":"object","additionalProperties":{},"description":"The values that the change replaced, read from the provider just before the change. It uses the field names and the units of action, for example {\"dailyBudget\": 800}, {\"status\": \"ACTIVE\"}, {\"bidStrategy\": \"COST_CAP\", \"costCap\": 4}, {\"startTime\": \"2026-10-01T00:00:00+0200\", \"endTime\": null} or {\"name\": \"Spring sale\"}. To undo the change, send the same kind again with these values. A bid change on a campaign also gives the amount of each ad set in adsets: undo it on each ad set. A create and a copy have none."},"result":{"description":"How the change ended. Present only when status is complete. ok of true carries what the provider gave back: a create carries the new object id, a copy carries the id of the copy and the budget that the copy carries, and every other action carries success. The budget is {\"dailyBudget\": 50} or {\"lifetimeBudget\": 500} in whole units of the account currency, \"campaign\" for an ad set copy that spends the budget of its campaign, or \"adset\" for a campaign copy whose ad sets each carry a budget. A schedule change also carries deliveryStartsAgain of true when it gave an ACTIVE ad set that had ended a new end: the ad set then delivers again. ok of false is a provider refusal, with a stable error code and a sentence."},"status":{"type":"string","enum":["running","complete","errored"],"description":"running means poll again. complete means the change finished, and result says how it ended. errored means the Mutation itself broke down, and AdCrunch cannot say whether the change happened."}},"required":["status"],"additionalProperties":false,"example":{"priorValue":{"status":"ACTIVE"},"result":{"ok":true,"result":{"success":true}},"status":"complete"}}}},"description":"The Mutation is running, complete, or errored."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `mutation:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No such Mutation for your organization. `error` is `not_found`."}},"summary":"Read a mutation","tags":["Mutations"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The workflowId that POST /mutations returned."}}]}},"/context/skills":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the Skills of your organization, newest first. Each row gives the slug, the name, the description and the source. It does not give the body. Deleted Skills are not in the list.\n\nOne page at a time: `limit` rows at most, 100 by default. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page.","operationId":"listSkills","responses":{"200":{"content":{"application/json":{"example":{"data":[{"description":"Use this every Monday to move budget towards the campaigns that reach the target cost per action.","name":"Weekly budget review","slug":"weekly-budget-review","source":null},{"description":"Use this before you write a new ad, to keep the wording inside the guidelines of the brand.","name":"Write ad copy","slug":"write-ad-copy","source":null},{"description":"Use it every Monday to review the spend and the results of last week.","name":"Weekly performance review","slug":"weekly-performance-review","source":{"slug":"weekly-performance-review","version":"2026-10-09"}}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","description":"What this playbook is for. This is what an agent chooses on."},"name":{"type":"string"},"slug":{"type":"string","description":"The per-organization handle used to fetch it."},"source":{"nullable":true,"description":"The Catalog Skill and the version that this Skill was imported from. It is `null` for a Skill that your organization wrote. An edit keeps it.","type":"object","properties":{"slug":{"type":"string","description":"The slug of the Catalog Skill that the Skill was imported from."},"version":{"type":"string","description":"The version of the Catalog Skill that the Skill was last imported from, `YYYY-MM-DD`."}},"required":["slug","version"],"additionalProperties":false}},"required":["description","name","slug","source"],"additionalProperties":false}},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"description":"Use this every Monday to move budget towards the campaigns that reach the target cost per action.","name":"Weekly budget review","slug":"weekly-budget-review","source":null},{"description":"Use this before you write a new ad, to keep the wording inside the guidelines of the brand.","name":"Write ad copy","slug":"write-ad-copy","source":null},{"description":"Use it every Monday to review the spend and the results of last week.","name":"Weekly performance review","slug":"weekly-performance-review","source":{"slug":"weekly-performance-review","version":"2026-10-09"}}]}}}},"description":"One page of the Skills of the organization, as slug, name and description, newest first. Bodies are never returned here — fetch one Skill for its instructions. This page is the last one, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `skill:read`. `error` is `forbidden`."}},"summary":"List the Skills","tags":["Skills"],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":true,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}}]},"post":{"security":[{"bearerAuth":[]}],"description":"Creates a Skill in your organization. The body is the playbook itself, and the description is what an agent reads to choose it. AdCrunch makes the slug from the name. To set a different slug, send one. The answer is `409` if the slug already exists in your organization.","operationId":"createSkill","responses":{"201":{"content":{"application/json":{"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Read the last seven days of spend for each live campaign.\n2. Move budget to the campaigns under the target cost per action.\n3. Write down what you changed, and why.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use this every Monday to move budget towards the campaigns that reach the target cost per action.","id":"skl_2d7e14","name":"Weekly budget review","revision":1,"slug":"weekly-budget-review","source":null,"updatedAt":null},"schema":{"type":"object","properties":{"body":{"type":"string","description":"The agent instructions themselves — the `SKILL.md`-equivalent, in markdown."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"source":{"nullable":true,"description":"The Catalog Skill and the version that this Skill was imported from. It is `null` for a Skill that your organization wrote. An edit keeps it.","type":"object","properties":{"slug":{"type":"string","description":"The slug of the Catalog Skill that the Skill was imported from."},"version":{"type":"string","description":"The version of the Catalog Skill that the Skill was last imported from, `YYYY-MM-DD`."}},"required":["slug","version"],"additionalProperties":false},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["body","createdAt","createdBy","description","id","name","revision","slug","source","updatedAt"],"additionalProperties":false,"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Read the last seven days of spend for each live campaign.\n2. Move budget to the campaigns under the target cost per action.\n3. Write down what you changed, and why.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use this every Monday to move budget towards the campaigns that reach the target cost per action.","id":"skl_2d7e14","name":"Weekly budget review","revision":1,"slug":"weekly-budget-review","source":null,"updatedAt":null}}}},"description":"The Skill as created, at `revision` 1. Send that revision back as `base_revision` on your first edit."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `skill:write`. `error` is `forbidden`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"The slug that is already taken."},"error":{"type":"string","enum":["slug_conflict"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false}}},"description":"Another Skill in this organization already holds that slug. `error` is `slug_conflict`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"The name and slug together contain no letter or digit, so no handle can be derived. `error` is `invalid_slug`. A body that fails the schema answers with the framework shape instead."}},"summary":"Create a Skill","tags":["Skills"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"description":"The playbook instructions (markdown)."},"description":{"type":"string","minLength":1,"maxLength":500,"description":"When to use this Skill — what agents read to discover it."},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.","type":"string","maxLength":120}},"required":["body","description","name"],"additionalProperties":false,"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Read the last seven days of spend for each live campaign.\n2. Move budget to the campaigns under the target cost per action.\n3. Write down what you changed, and why.","description":"Use this every Monday to move budget towards the campaigns that reach the target cost per action.","name":"Weekly budget review","slug":"weekly-budget-review"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"description":"The playbook instructions (markdown)."},"description":{"type":"string","minLength":1,"maxLength":500,"description":"When to use this Skill — what agents read to discover it."},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.","type":"string","maxLength":120}},"required":["body","description","name"],"additionalProperties":false,"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Read the last seven days of spend for each live campaign.\n2. Move budget to the campaigns under the target cost per action.\n3. Write down what you changed, and why.","description":"Use this every Monday to move budget towards the campaigns that reach the target cost per action.","name":"Weekly budget review","slug":"weekly-budget-review"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"description":"The playbook instructions (markdown)."},"description":{"type":"string","minLength":1,"maxLength":500,"description":"When to use this Skill — what agents read to discover it."},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.","type":"string","maxLength":120}},"required":["body","description","name"],"additionalProperties":false,"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Read the last seven days of spend for each live campaign.\n2. Move budget to the campaigns under the target cost per action.\n3. Write down what you changed, and why.","description":"Use this every Monday to move budget towards the campaigns that reach the target cost per action.","name":"Weekly budget review","slug":"weekly-budget-review"}}}}}}},"/context/skills/{slug}":{"get":{"security":[{"bearerAuth":[]}],"description":"Reads one Skill by its slug. The answer has the full body and the current revision. Send that revision back as `base_revision` when you update the Skill. `source` names the Catalog Skill that the Skill was imported from, or is `null`.","operationId":"getSkill","responses":{"200":{"content":{"application/json":{"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Read the last seven days of spend for each live campaign.\n2. Move budget to the campaigns under the target cost per action.\n3. Write down what you changed, and why.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use this every Monday to move budget towards the campaigns that reach the target cost per action.","id":"skl_2d7e14","name":"Weekly budget review","revision":3,"slug":"weekly-budget-review","source":null,"updatedAt":1790086400000},"schema":{"type":"object","properties":{"body":{"type":"string","description":"The agent instructions themselves — the `SKILL.md`-equivalent, in markdown."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"source":{"nullable":true,"description":"The Catalog Skill and the version that this Skill was imported from. It is `null` for a Skill that your organization wrote. An edit keeps it.","type":"object","properties":{"slug":{"type":"string","description":"The slug of the Catalog Skill that the Skill was imported from."},"version":{"type":"string","description":"The version of the Catalog Skill that the Skill was last imported from, `YYYY-MM-DD`."}},"required":["slug","version"],"additionalProperties":false},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["body","createdAt","createdBy","description","id","name","revision","slug","source","updatedAt"],"additionalProperties":false,"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Read the last seven days of spend for each live campaign.\n2. Move budget to the campaigns under the target cost per action.\n3. Write down what you changed, and why.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use this every Monday to move budget towards the campaigns that reach the target cost per action.","id":"skl_2d7e14","name":"Weekly budget review","revision":3,"slug":"weekly-budget-review","source":null,"updatedAt":1790086400000}}}},"description":"The Skill, with its instruction body."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `skill:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Skill with that slug in this organization. `error` is `not_found`."}},"summary":"Get a Skill by slug","tags":["Skills"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","description":"The Skill slug (its handle in your organization)."}}]},"patch":{"security":[{"bearerAuth":[]}],"description":"Updates a Skill. Send only the fields you change, with the `base_revision` you read. A field you omit keeps its value. The slug changes only when you send a `slug`: a new `name` alone never moves the handle. The answer is `409` with `error=revision_mismatch` if the Skill changed after you read it. It is `409` with `error=slug_conflict` if another Skill already holds the `slug` you send. An update keeps the `source` of an imported Skill.","operationId":"updateSkill","responses":{"200":{"content":{"application/json":{"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Read the last seven days of spend for each live campaign.\n2. Move budget to the campaigns under the target cost per action.\n3. Write down what you changed, and why.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use this every Monday, and after any change of budget, to move spend towards the campaigns that reach the target cost per action.","id":"skl_2d7e14","name":"Weekly budget review","revision":4,"slug":"weekly-budget-review","source":null,"updatedAt":1790086400000},"schema":{"type":"object","properties":{"body":{"type":"string","description":"The agent instructions themselves — the `SKILL.md`-equivalent, in markdown."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"source":{"nullable":true,"description":"The Catalog Skill and the version that this Skill was imported from. It is `null` for a Skill that your organization wrote. An edit keeps it.","type":"object","properties":{"slug":{"type":"string","description":"The slug of the Catalog Skill that the Skill was imported from."},"version":{"type":"string","description":"The version of the Catalog Skill that the Skill was last imported from, `YYYY-MM-DD`."}},"required":["slug","version"],"additionalProperties":false},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["body","createdAt","createdBy","description","id","name","revision","slug","source","updatedAt"],"additionalProperties":false,"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Read the last seven days of spend for each live campaign.\n2. Move budget to the campaigns under the target cost per action.\n3. Write down what you changed, and why.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use this every Monday, and after any change of budget, to move spend towards the campaigns that reach the target cost per action.","id":"skl_2d7e14","name":"Weekly budget review","revision":4,"slug":"weekly-budget-review","source":null,"updatedAt":1790086400000}}}},"description":"The Skill as it now stands, at the next `revision`. Send that one back on your next edit."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `skill:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Skill with that slug. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false},{"type":"object","properties":{"slug":{"type":"string","description":"The slug that is already taken."},"error":{"type":"string","enum":["slug_conflict"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false}]}}},"description":"Two different conflicts share this status, and `error` tells them apart. `revision_mismatch` means somebody else wrote first — re-read, re-apply your edit, and send the `currentRevision` back. `slug_conflict` means the rename collides with another Skill."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"The new slug contains no letter or digit. `error` is `invalid_slug`."}},"summary":"Update a Skill","tags":["Skills"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"body":{"type":"string","minLength":1},"description":{"type":"string","minLength":1,"maxLength":500},"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":3,"description":"Use this every Monday, and after any change of budget, to move spend towards the campaigns that reach the target cost per action."}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"body":{"type":"string","minLength":1},"description":{"type":"string","minLength":1,"maxLength":500},"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":3,"description":"Use this every Monday, and after any change of budget, to move spend towards the campaigns that reach the target cost per action."}}},"multipart/form-data":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"body":{"type":"string","minLength":1},"description":{"type":"string","minLength":1,"maxLength":500},"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":3,"description":"Use this every Monday, and after any change of budget, to move spend towards the campaigns that reach the target cost per action."}}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a Skill. Send the `base_revision` you read, in the query string. The answer is `409` with `error=revision_mismatch` if the Skill changed after you read it. The row is soft-deleted: the Skill leaves every listing and every read, and only an operator can bring it back.","operationId":"deleteSkill","responses":{"204":{"description":"Deleted. The row is soft-deleted, and the slug is free for re-use."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `skill:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Skill with that slug. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false}}},"description":"The Skill moved since you read it. `error` is `revision_mismatch`."}},"summary":"Delete a Skill","tags":["Skills"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"base_revision","in":"query","required":true,"schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."}}]}},"/context/skills/{slug}/re-import":{"post":{"security":[{"bearerAuth":[]}],"description":"Re-imports a Skill that your organization imported from the Skill Catalog. The re-import replaces the name, the description and the body with the current version of the Catalog Skill, and sets `source.version`. The Skill keeps its slug. **Edits that your organization made to the Skill are lost.** Send the `baseRevision` you read. The answer is `409` when the re-import is not possible, and `error` tells why: `not_imported` for a Skill that your organization wrote, `not_in_skill_catalog` when the Catalog Skill left the Skill Catalog, `no_later_version` when the Skill Catalog has no later version, and `revision_mismatch` when the Skill changed after you read it.","operationId":"reimportSkill","responses":{"200":{"content":{"application/json":{"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Call `list_campaigns` for each advertiser.\n2. Call `query_insights` for the last seven days.\n3. Write down the campaigns that missed their target.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use it every Monday to review the spend and the results of last week.","id":"skl_7c1f90","name":"Weekly performance review","revision":3,"slug":"weekly-performance-review","source":{"slug":"weekly-performance-review","version":"2026-11-02"},"updatedAt":1791000000000},"schema":{"type":"object","properties":{"body":{"type":"string","description":"The agent instructions themselves — the `SKILL.md`-equivalent, in markdown."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"source":{"nullable":true,"description":"The Catalog Skill and the version that this Skill was imported from. It is `null` for a Skill that your organization wrote. An edit keeps it.","type":"object","properties":{"slug":{"type":"string","description":"The slug of the Catalog Skill that the Skill was imported from."},"version":{"type":"string","description":"The version of the Catalog Skill that the Skill was last imported from, `YYYY-MM-DD`."}},"required":["slug","version"],"additionalProperties":false},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["body","createdAt","createdBy","description","id","name","revision","slug","source","updatedAt"],"additionalProperties":false,"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Call `list_campaigns` for each advertiser.\n2. Call `query_insights` for the last seven days.\n3. Write down the campaigns that missed their target.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use it every Monday to review the spend and the results of last week.","id":"skl_7c1f90","name":"Weekly performance review","revision":3,"slug":"weekly-performance-review","source":{"slug":"weekly-performance-review","version":"2026-11-02"},"updatedAt":1791000000000}}}},"description":"The Skill with the content of the later version, at the next `revision`. This shape was not observed against a deployed service, because no stage held a later version of a Catalog Skill: it is written from the Context Layer’s view type."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `skill:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Skill with that slug. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"error":{"type":"string","enum":["not_imported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["not_in_skill_catalog"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["no_later_version"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false},{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false}]}}},"description":"The re-import is not possible, and `error` tells why. `not_imported`: your organization wrote the Skill, so it has no source. `not_in_skill_catalog`: the Catalog Skill left the Skill Catalog; the Skill keeps its source. `no_later_version`: the version of the Catalog Skill is not later than `source.version`. `revision_mismatch`: somebody else wrote first; read the Skill again, and send the `currentRevision` back."}},"summary":"Re-import a Skill","tags":["Skills"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","description":"The slug of the imported Skill in your organization."}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"baseRevision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET. A re-import replaces the content of the Skill, so it guards the content that you read."}},"required":["baseRevision"],"additionalProperties":false,"example":{"baseRevision":2}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"baseRevision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET. A re-import replaces the content of the Skill, so it guards the content that you read."}},"required":["baseRevision"],"additionalProperties":false,"example":{"baseRevision":2}}},"multipart/form-data":{"schema":{"type":"object","properties":{"baseRevision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET. A re-import replaces the content of the Skill, so it guards the content that you read."}},"required":["baseRevision"],"additionalProperties":false,"example":{"baseRevision":2}}}}}}},"/context/skill-catalog":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the Skill Catalog: the playbooks that AdCrunch writes, and that your organization can import as Skills. Each row gives the slug, the name, the description and the version of the Catalog Skill. It does not give the body. `importedSkill` names the Skill that your organization imported from it, or is `null`.\n\nThe list is in slug order. One page at a time: `limit` rows at most, 100 by default. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page.","operationId":"listSkillCatalog","responses":{"200":{"content":{"application/json":{"example":{"data":[{"description":"Use it to pause the ads that spend and do not convert.","importedSkill":null,"name":"Pause low performers","slug":"pause-low-performers","version":"2026-10-09"},{"description":"Use it every Monday to review the spend and the results of last week.","name":"Weekly performance review","slug":"weekly-performance-review","importedSkill":{"slug":"weekly-performance-review"},"version":"2026-11-02"}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","description":"What this playbook is for. An import copies it."},"importedSkill":{"nullable":true,"description":"The live import of your organization, or `null` when your organization holds none. Read the Skill for its `source.version`: a re-import is possible when `version` is later.","type":"object","properties":{"slug":{"type":"string","description":"The slug of the Skill that the import made in your organization."}},"required":["slug"],"additionalProperties":false},"name":{"type":"string"},"slug":{"type":"string","description":"The slug of the Catalog Skill. It never changes."},"version":{"type":"string","description":"The date of the current edition, `YYYY-MM-DD`. A later date is a newer edition."}},"required":["description","importedSkill","name","slug","version"],"additionalProperties":false}},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"description":"Use it to pause the ads that spend and do not convert.","importedSkill":null,"name":"Pause low performers","slug":"pause-low-performers","version":"2026-10-09"},{"description":"Use it every Monday to review the spend and the results of last week.","name":"Weekly performance review","slug":"weekly-performance-review","importedSkill":{"slug":"weekly-performance-review"},"version":"2026-11-02"}]}}}},"description":"One page of the Skill Catalog, in slug order, with the live import of your organization. This page is the last one, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `skill:read`. `error` is `forbidden`."}},"summary":"List the Skill Catalog","tags":["Skills"],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":true,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}}]}},"/context/skill-catalog/{catalogSlug}/import":{"post":{"security":[{"bearerAuth":[]}],"description":"Imports a Catalog Skill into your organization. The import makes a Skill from the name, the description and the body of the Catalog Skill, and sets `source`. Your organization owns the Skill, and can edit it like any other Skill. Send `{}` to keep the slug of the Catalog Skill. Your organization holds at most one import of each Catalog Skill: a second import answers `409` with `error=already_imported`. If a Skill of your organization holds the slug, the answer is `409` with `error=slug_conflict`: send another `slug`.","operationId":"importCatalogSkill","responses":{"201":{"content":{"application/json":{"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Call `list_campaigns` for each advertiser.\n2. Call `query_insights` for the last seven days.\n3. Write down the campaigns that missed their target.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use it every Monday to review the spend and the results of last week.","id":"skl_7c1f90","name":"Weekly performance review","revision":1,"slug":"weekly-performance-review","source":{"slug":"weekly-performance-review","version":"2026-10-09"},"updatedAt":null},"schema":{"type":"object","properties":{"body":{"type":"string","description":"The agent instructions themselves — the `SKILL.md`-equivalent, in markdown."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"source":{"nullable":true,"description":"The Catalog Skill and the version that this Skill was imported from. It is `null` for a Skill that your organization wrote. An edit keeps it.","type":"object","properties":{"slug":{"type":"string","description":"The slug of the Catalog Skill that the Skill was imported from."},"version":{"type":"string","description":"The version of the Catalog Skill that the Skill was last imported from, `YYYY-MM-DD`."}},"required":["slug","version"],"additionalProperties":false},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["body","createdAt","createdBy","description","id","name","revision","slug","source","updatedAt"],"additionalProperties":false,"example":{"body":"## When to run\n\nEvery Monday, before the week starts.\n\n## Steps\n\n1. Call `list_campaigns` for each advertiser.\n2. Call `query_insights` for the last seven days.\n3. Write down the campaigns that missed their target.","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Use it every Monday to review the spend and the results of last week.","id":"skl_7c1f90","name":"Weekly performance review","revision":1,"slug":"weekly-performance-review","source":{"slug":"weekly-performance-review","version":"2026-10-09"},"updatedAt":null}}}},"description":"The Skill as the import made it, at `revision` 1, with its `source`."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `skill:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Catalog Skill with that slug. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"slug":{"type":"string","description":"The slug of the Skill that holds the live import."},"error":{"type":"string","enum":["already_imported"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false},{"type":"object","properties":{"slug":{"type":"string","description":"The slug that is already taken."},"error":{"type":"string","enum":["slug_conflict"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false}]}}},"description":"Two different conflicts share this status, and `error` tells them apart. `already_imported` means that your organization holds an import of this Catalog Skill: `slug` names that Skill. `slug_conflict` means that another Skill holds the slug: send another `slug`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"The slug contains no letter or digit. `error` is `invalid_slug`."}},"summary":"Import a Catalog Skill","tags":["Skills"],"parameters":[{"name":"catalogSlug","in":"path","required":true,"schema":{"type":"string","description":"The slug of the Catalog Skill to import."}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"description":"Optional slug for the new Skill, normalized to kebab-case. It defaults to the slug of the Catalog Skill. Send one when a Skill of your organization holds that slug.","type":"string","maxLength":120}},"additionalProperties":false,"example":{"slug":"monday-review"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"slug":{"description":"Optional slug for the new Skill, normalized to kebab-case. It defaults to the slug of the Catalog Skill. Send one when a Skill of your organization holds that slug.","type":"string","maxLength":120}},"additionalProperties":false,"example":{"slug":"monday-review"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"slug":{"description":"Optional slug for the new Skill, normalized to kebab-case. It defaults to the slug of the Catalog Skill. Send one when a Skill of your organization holds that slug.","type":"string","maxLength":120}},"additionalProperties":false,"example":{"slug":"monday-review"}}}}}}},"/context/brands":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the Brands of your organization, newest first. Each row gives the slug, the name, the description, the logo URL, and which narrative sections have content. This call does not give the sections. To read them, read one Brand. Deleted Brands are not in the list.\n\nOne page at a time: `limit` rows at most, 100 by default. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page.","operationId":"listBrands","responses":{"200":{"content":{"application/json":{"example":{"data":[{"description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","logoUrl":"https://documents.adcrunch.dev/org_3a7f10/doc_5e2b41","name":"Acme","sections":{"guidelines":true,"identity":true,"messaging":true,"voice":true},"slug":"acme"},{"description":"The trail range of Acme, sold to runners who leave the road.","logoUrl":null,"name":"Acme Trail","sections":{"guidelines":false,"identity":true,"messaging":false,"voice":false},"slug":"acme-trail"}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string"},"logoUrl":{"nullable":true,"description":"The Brand’s nominated logo, when it has one.","type":"string"},"name":{"type":"string"},"sections":{"type":"object","properties":{"guidelines":{"type":"boolean"},"identity":{"type":"boolean"},"messaging":{"type":"boolean"},"voice":{"type":"boolean"}},"required":["guidelines","identity","messaging","voice"],"additionalProperties":false,"description":"Which sections carry prose. An unwritten section is a normal state, not a deficit."},"slug":{"type":"string"}},"required":["description","logoUrl","name","sections","slug"],"additionalProperties":false}},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","logoUrl":"https://documents.adcrunch.dev/org_3a7f10/doc_5e2b41","name":"Acme","sections":{"guidelines":true,"identity":true,"messaging":true,"voice":true},"slug":"acme"},{"description":"The trail range of Acme, sold to runners who leave the road.","logoUrl":null,"name":"Acme Trail","sections":{"guidelines":false,"identity":true,"messaging":false,"voice":false},"slug":"acme-trail"}]}}}},"description":"One page of the Brands of the organization, newest first. `sections` says which of the four carry prose, so a chooser can rank Brands without the listing becoming the brief. `logoUrl` is the public address of the Document the Brand nominates, and it is `null` when it nominates none. This page is the last one, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:read`. `error` is `forbidden`."}},"summary":"List the Brands","tags":["Brands"],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":true,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}}]},"post":{"security":[{"bearerAuth":[]}],"description":"Creates a Brand in your organization. Every narrative section is optional, because you write a Brand one part at a time. AdCrunch makes the slug from the name. To set a different slug, send one. The answer is `409` if the slug already exists in your organization.","operationId":"createBrand","responses":{"201":{"content":{"application/json":{"example":{"createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","guidelines":null,"id":"brd_6a9c33","identity":"Acme makes one running shoe and sells it direct. The promise is a thousand kilometres on the same pair, and a resole rather than a replacement.","logoDocumentId":null,"messaging":null,"name":"Acme","revision":1,"slug":"acme","updatedAt":null,"voice":"Plain and measured. Short sentences. No superlative the shoe cannot earn.","websiteUrl":"https://acme.example"},"schema":{"type":"object","properties":{"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"guidelines":{"nullable":true,"description":"Do’s and don’ts, claims to avoid.","type":"string"},"id":{"type":"string"},"identity":{"nullable":true,"description":"Positioning, mission, what is on offer.","type":"string"},"logoDocumentId":{"nullable":true,"description":"The Document this Brand nominates as its logo.","type":"string"},"messaging":{"nullable":true,"description":"Key messages, value propositions, proof points.","type":"string"},"name":{"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"voice":{"nullable":true,"description":"Tone and personality.","type":"string"},"websiteUrl":{"nullable":true,"description":"The brand’s public site. A locator only — its contents are not stored.","type":"string"}},"required":["createdAt","createdBy","description","guidelines","id","identity","logoDocumentId","messaging","name","revision","slug","updatedAt","voice","websiteUrl"],"additionalProperties":false,"example":{"createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","guidelines":null,"id":"brd_6a9c33","identity":"Acme makes one running shoe and sells it direct. The promise is a thousand kilometres on the same pair, and a resole rather than a replacement.","logoDocumentId":null,"messaging":null,"name":"Acme","revision":1,"slug":"acme","updatedAt":null,"voice":"Plain and measured. Short sentences. No superlative the shoe cannot earn.","websiteUrl":"https://acme.example"}}}},"description":"The Brand as created, at `revision` 1. Every unwritten section comes back `null`, which is a normal state rather than an error."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"The slug that is already taken."},"error":{"type":"string","enum":["slug_conflict"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false}}},"description":"Another Brand in this organization already holds that slug. `error` is `slug_conflict`. A Skill may share it — each aggregate is addressed through its own surface."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"The name and slug together contain no letter or digit. `error` is `invalid_slug`."}},"summary":"Create a Brand","tags":["Brands"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":500,"description":"One-line summary — what agents read to pick this Brand."},"guidelines":{"description":"Do's and don'ts, guardrails, claims to avoid (markdown).","type":"string","minLength":1},"identity":{"description":"Who the brand is: positioning, mission, offering (markdown).","type":"string","minLength":1},"messaging":{"description":"Key messages, value propositions, proof points (markdown).","type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.","type":"string","maxLength":120},"voice":{"description":"Tone of voice and personality (markdown).","type":"string","minLength":1},"website_url":{"description":"The Brand's public site — a locator like the slug, not a section. Setting it does nothing on its own; reading it is a separate, explicit call.","type":"string","format":"uri"}},"required":["description","name"],"additionalProperties":false,"example":{"description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","identity":"Acme makes one running shoe and sells it direct. The promise is a thousand kilometres on the same pair, and a resole rather than a replacement.","name":"Acme","slug":"acme","voice":"Plain and measured. Short sentences. No superlative the shoe cannot earn.","website_url":"https://acme.example"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":500,"description":"One-line summary — what agents read to pick this Brand."},"guidelines":{"description":"Do's and don'ts, guardrails, claims to avoid (markdown).","type":"string","minLength":1},"identity":{"description":"Who the brand is: positioning, mission, offering (markdown).","type":"string","minLength":1},"messaging":{"description":"Key messages, value propositions, proof points (markdown).","type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.","type":"string","maxLength":120},"voice":{"description":"Tone of voice and personality (markdown).","type":"string","minLength":1},"website_url":{"description":"The Brand's public site — a locator like the slug, not a section. Setting it does nothing on its own; reading it is a separate, explicit call.","type":"string","format":"uri"}},"required":["description","name"],"additionalProperties":false,"example":{"description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","identity":"Acme makes one running shoe and sells it direct. The promise is a thousand kilometres on the same pair, and a resole rather than a replacement.","name":"Acme","slug":"acme","voice":"Plain and measured. Short sentences. No superlative the shoe cannot earn.","website_url":"https://acme.example"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":500,"description":"One-line summary — what agents read to pick this Brand."},"guidelines":{"description":"Do's and don'ts, guardrails, claims to avoid (markdown).","type":"string","minLength":1},"identity":{"description":"Who the brand is: positioning, mission, offering (markdown).","type":"string","minLength":1},"messaging":{"description":"Key messages, value propositions, proof points (markdown).","type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.","type":"string","maxLength":120},"voice":{"description":"Tone of voice and personality (markdown).","type":"string","minLength":1},"website_url":{"description":"The Brand's public site — a locator like the slug, not a section. Setting it does nothing on its own; reading it is a separate, explicit call.","type":"string","format":"uri"}},"required":["description","name"],"additionalProperties":false,"example":{"description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","identity":"Acme makes one running shoe and sells it direct. The promise is a thousand kilometres on the same pair, and a resole rather than a replacement.","name":"Acme","slug":"acme","voice":"Plain and measured. Short sentences. No superlative the shoe cannot earn.","website_url":"https://acme.example"}}}}}}},"/context/brands/{slug}":{"get":{"security":[{"bearerAuth":[]}],"description":"Reads one Brand by its slug. The answer has every narrative section and the current revision. Send that revision back as `base_revision` when you update the Brand. A section that nobody wrote is `null`.","operationId":"getBrand","responses":{"200":{"content":{"application/json":{"example":{"createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","guidelines":"- Name the distance, never the \"journey\".\n- Claim the kilometres the shoe is tested to, and no more.\n- Do not compare the shoe to a named competitor.","id":"brd_6a9c33","identity":"Acme makes one running shoe and sells it direct. The promise is a thousand kilometres on the same pair, and a resole rather than a replacement.","logoDocumentId":"doc_5e2b41","messaging":"- One shoe, a thousand kilometres.\n- Resole it twice, and keep running on it.\n- Sold direct, so the price is the cost of the shoe.","name":"Acme","revision":4,"slug":"acme","updatedAt":1790086400000,"voice":"Plain and measured. Short sentences. No superlative the shoe cannot earn.","websiteUrl":"https://acme.example"},"schema":{"type":"object","properties":{"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"guidelines":{"nullable":true,"description":"Do’s and don’ts, claims to avoid.","type":"string"},"id":{"type":"string"},"identity":{"nullable":true,"description":"Positioning, mission, what is on offer.","type":"string"},"logoDocumentId":{"nullable":true,"description":"The Document this Brand nominates as its logo.","type":"string"},"messaging":{"nullable":true,"description":"Key messages, value propositions, proof points.","type":"string"},"name":{"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"voice":{"nullable":true,"description":"Tone and personality.","type":"string"},"websiteUrl":{"nullable":true,"description":"The brand’s public site. A locator only — its contents are not stored.","type":"string"}},"required":["createdAt","createdBy","description","guidelines","id","identity","logoDocumentId","messaging","name","revision","slug","updatedAt","voice","websiteUrl"],"additionalProperties":false,"example":{"createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","guidelines":"- Name the distance, never the \"journey\".\n- Claim the kilometres the shoe is tested to, and no more.\n- Do not compare the shoe to a named competitor.","id":"brd_6a9c33","identity":"Acme makes one running shoe and sells it direct. The promise is a thousand kilometres on the same pair, and a resole rather than a replacement.","logoDocumentId":"doc_5e2b41","messaging":"- One shoe, a thousand kilometres.\n- Resole it twice, and keep running on it.\n- Sold direct, so the price is the cost of the shoe.","name":"Acme","revision":4,"slug":"acme","updatedAt":1790086400000,"voice":"Plain and measured. Short sentences. No superlative the shoe cannot earn.","websiteUrl":"https://acme.example"}}}},"description":"The Brand with all four sections. It does **not** carry its Personas — ask `GET /context/brands/{slug}/personas` for those."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Brand with that slug in this organization. `error` is `not_found`."}},"summary":"Get a Brand by slug","tags":["Brands"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","description":"The Brand slug (its handle in your organization)."}}]},"patch":{"security":[{"bearerAuth":[]}],"description":"Updates a Brand. Send the `base_revision` you read. Omit a narrative section to keep it. Send `null` to clear it. The slug changes only when you send a `slug`: a new `name` alone never moves the handle. The answer is `409` with `error=revision_mismatch` if the Brand changed after you read it. It is `409` with `error=slug_conflict` if another Brand already holds the `slug` you send.","operationId":"updateBrand","responses":{"200":{"content":{"application/json":{"example":{"createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","guidelines":"- Name the distance, never the \"journey\".\n- Claim the kilometres the shoe is tested to, and no more.\n- Do not compare the shoe to a named competitor.","id":"brd_6a9c33","identity":"Acme makes one running shoe and sells it direct. The promise is a thousand kilometres on the same pair, and a resole rather than a replacement.","logoDocumentId":"doc_5e2b41","messaging":"- One shoe, a thousand kilometres.\n- Resole it twice, and keep running on it.\n- Sold direct, so the price is the cost of the shoe.","name":"Acme","revision":5,"slug":"acme","updatedAt":1790086400000,"voice":"Plain and measured. Short sentences. No superlative the shoe cannot earn.","websiteUrl":"https://acme.example"},"schema":{"type":"object","properties":{"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"guidelines":{"nullable":true,"description":"Do’s and don’ts, claims to avoid.","type":"string"},"id":{"type":"string"},"identity":{"nullable":true,"description":"Positioning, mission, what is on offer.","type":"string"},"logoDocumentId":{"nullable":true,"description":"The Document this Brand nominates as its logo.","type":"string"},"messaging":{"nullable":true,"description":"Key messages, value propositions, proof points.","type":"string"},"name":{"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"voice":{"nullable":true,"description":"Tone and personality.","type":"string"},"websiteUrl":{"nullable":true,"description":"The brand’s public site. A locator only — its contents are not stored.","type":"string"}},"required":["createdAt","createdBy","description","guidelines","id","identity","logoDocumentId","messaging","name","revision","slug","updatedAt","voice","websiteUrl"],"additionalProperties":false,"example":{"createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Running shoes sold direct to the runner, made to last a thousand kilometres.","guidelines":"- Name the distance, never the \"journey\".\n- Claim the kilometres the shoe is tested to, and no more.\n- Do not compare the shoe to a named competitor.","id":"brd_6a9c33","identity":"Acme makes one running shoe and sells it direct. The promise is a thousand kilometres on the same pair, and a resole rather than a replacement.","logoDocumentId":"doc_5e2b41","messaging":"- One shoe, a thousand kilometres.\n- Resole it twice, and keep running on it.\n- Sold direct, so the price is the cost of the shoe.","name":"Acme","revision":5,"slug":"acme","updatedAt":1790086400000,"voice":"Plain and measured. Short sentences. No superlative the shoe cannot earn.","websiteUrl":"https://acme.example"}}}},"description":"The Brand as it now stands, at the next `revision`. Send that one back on your next edit."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Brand with that slug. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false},{"type":"object","properties":{"slug":{"type":"string","description":"The slug that is already taken."},"error":{"type":"string","enum":["slug_conflict"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false}]}}},"description":"`error` tells the two conflicts apart. `revision_mismatch` means somebody wrote first — re-read, re-apply, and send `currentRevision` back. `slug_conflict` means the rename collides."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"Two codes share this status. `invalid_slug`: the new slug has no letter or digit. `invalid_logo`: the nominated logo is not one of this Brand’s own finished documents."}},"summary":"Update a Brand","tags":["Brands"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"description":{"type":"string","minLength":1,"maxLength":500},"guidelines":{"description":"Do's and don'ts; null clears the section.","nullable":true,"type":"string","minLength":1},"identity":{"description":"Positioning and mission; null clears it.","nullable":true,"type":"string","minLength":1},"logo_document_id":{"description":"Nominate one of the Brand's documents as its logo; null clears the nomination.","nullable":true,"type":"string","pattern":"^doc_[\\s\\S]{0,}$"},"messaging":{"description":"Key messages; null clears the section.","nullable":true,"type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120},"voice":{"description":"Tone of voice; null clears the section.","nullable":true,"type":"string","minLength":1},"website_url":{"description":"The Brand's public site; null clears it. Changing it starts no read.","nullable":true,"type":"string","format":"uri"}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":4,"guidelines":"- Name the distance, never the \"journey\".\n- Claim the kilometres the shoe is tested to, and no more.\n- Do not compare the shoe to a named competitor.","logo_document_id":"doc_5e2b41","messaging":"- One shoe, a thousand kilometres.\n- Resole it twice, and keep running on it.\n- Sold direct, so the price is the cost of the shoe."}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"description":{"type":"string","minLength":1,"maxLength":500},"guidelines":{"description":"Do's and don'ts; null clears the section.","nullable":true,"type":"string","minLength":1},"identity":{"description":"Positioning and mission; null clears it.","nullable":true,"type":"string","minLength":1},"logo_document_id":{"description":"Nominate one of the Brand's documents as its logo; null clears the nomination.","nullable":true,"type":"string","pattern":"^doc_[\\s\\S]{0,}$"},"messaging":{"description":"Key messages; null clears the section.","nullable":true,"type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120},"voice":{"description":"Tone of voice; null clears the section.","nullable":true,"type":"string","minLength":1},"website_url":{"description":"The Brand's public site; null clears it. Changing it starts no read.","nullable":true,"type":"string","format":"uri"}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":4,"guidelines":"- Name the distance, never the \"journey\".\n- Claim the kilometres the shoe is tested to, and no more.\n- Do not compare the shoe to a named competitor.","logo_document_id":"doc_5e2b41","messaging":"- One shoe, a thousand kilometres.\n- Resole it twice, and keep running on it.\n- Sold direct, so the price is the cost of the shoe."}}},"multipart/form-data":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"description":{"type":"string","minLength":1,"maxLength":500},"guidelines":{"description":"Do's and don'ts; null clears the section.","nullable":true,"type":"string","minLength":1},"identity":{"description":"Positioning and mission; null clears it.","nullable":true,"type":"string","minLength":1},"logo_document_id":{"description":"Nominate one of the Brand's documents as its logo; null clears the nomination.","nullable":true,"type":"string","pattern":"^doc_[\\s\\S]{0,}$"},"messaging":{"description":"Key messages; null clears the section.","nullable":true,"type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120},"voice":{"description":"Tone of voice; null clears the section.","nullable":true,"type":"string","minLength":1},"website_url":{"description":"The Brand's public site; null clears it. Changing it starts no read.","nullable":true,"type":"string","format":"uri"}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":4,"guidelines":"- Name the distance, never the \"journey\".\n- Claim the kilometres the shoe is tested to, and no more.\n- Do not compare the shoe to a named competitor.","logo_document_id":"doc_5e2b41","messaging":"- One shoe, a thousand kilometres.\n- Resole it twice, and keep running on it.\n- Sold direct, so the price is the cost of the shoe."}}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a Brand. Send the `base_revision` you read, in the query string. The delete carries its Personas, its Documents and its advertiser attachments with it. The slug becomes free again. The answer is `409` with `error=revision_mismatch` if the Brand changed after you read it.","operationId":"deleteBrand","responses":{"204":{"description":"Deleted, with its Personas, its Documents and its advertiser attachments. The document bytes are dropped from storage too, so a deleted Brand’s guidelines PDF stops being readable at its URL. Its **Campaign Plans stay**, pointing at a tombstone the read path handles."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Brand with that slug. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false}}},"description":"The Brand moved since you read it. `error` is `revision_mismatch`."}},"summary":"Delete a Brand","tags":["Brands"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"base_revision","in":"query","required":true,"schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."}}]}},"/context/brands/{slug}/personas":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists a Brand's Personas, newest first, as slug + name + description + age range, with which sections are authored. The sections themselves are never returned here — fetch a single Persona for those. Soft-deleted Personas are excluded.\n\nOne page at a time: `limit` rows at most, 100 by default. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page.","operationId":"listPersonas","responses":{"200":{"content":{"application/json":{"example":{"data":[{"ageMax":null,"ageMin":18,"description":"Runners who have never bought direct, and who buy the shoe a shop puts in front of them.","name":"First pair","sections":{"frictions":false,"language":false,"motivations":true,"profile":true},"slug":"first-pair"},{"ageMax":45,"ageMin":28,"description":"Runners who already own a pair, and who buy the next one without looking at another brand.","name":"Loyalists","sections":{"frictions":true,"language":true,"motivations":true,"profile":true},"slug":"loyalists"}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"description":{"type":"string"},"name":{"type":"string"},"sections":{"type":"object","properties":{"frictions":{"type":"boolean"},"language":{"type":"boolean"},"motivations":{"type":"boolean"},"profile":{"type":"boolean"}},"required":["frictions","language","motivations","profile"],"additionalProperties":false,"description":"Which of the Persona’s four sections carry prose."},"slug":{"type":"string","description":"Unique within its Brand, not within the organization — two Brands may each brief a `loyalists`."}},"required":["ageMax","ageMin","description","name","sections","slug"],"additionalProperties":false}},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"ageMax":null,"ageMin":18,"description":"Runners who have never bought direct, and who buy the shoe a shop puts in front of them.","name":"First pair","sections":{"frictions":false,"language":false,"motivations":true,"profile":true},"slug":"first-pair"},{"ageMax":45,"ageMin":28,"description":"Runners who already own a pair, and who buy the next one without looking at another brand.","name":"Loyalists","sections":{"frictions":true,"language":true,"motivations":true,"profile":true},"slug":"loyalists"}]}}}},"description":"One page of this Brand's Personas, newest first, with which of their four sections carry prose. The prose itself is never returned here. There is deliberately no organization-wide Persona listing — a Persona is addressed through its Brand. This page is the last one, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Brand with that slug. `error` is `not_found`."}},"summary":"List a Brand's Personas","tags":["Personas"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":true,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}}]},"post":{"security":[{"bearerAuth":[]}],"description":"Creates a Persona on a Brand. A Persona describes one audience that the brand speaks to. Every section and both ends of the age range are optional, because you write a Persona one part at a time. The slug is unique in the Brand, not in your organization. The answer is `409` if the slug already exists on this Brand.","operationId":"createPersona","responses":{"201":{"content":{"application/json":{"example":{"ageMax":45,"ageMin":28,"brandId":"brd_6a9c33","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Runners who already own a pair, and who buy the next one without looking at another brand.","frictions":null,"id":"psn_8c4a20","language":null,"motivations":"They want the same shoe again, and proof that it is the same. A resole keeps them running on a pair they trust.","name":"Loyalists","profile":"Runners of 40 to 80 kilometres a week, ten years in, with one pair in rotation and one on the shelf.","revision":1,"slug":"loyalists","updatedAt":null},"schema":{"type":"object","properties":{"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"brandId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"frictions":{"nullable":true,"description":"Objections, doubts, what stops them.","type":"string"},"id":{"type":"string"},"language":{"nullable":true,"description":"The words *they* use for the problem, in their own register. Not a locale, despite the name.","type":"string"},"motivations":{"nullable":true,"description":"What they want, and what triggers them.","type":"string"},"name":{"type":"string"},"profile":{"nullable":true,"description":"Who they are, and their situation.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["ageMax","ageMin","brandId","createdAt","createdBy","description","frictions","id","language","motivations","name","profile","revision","slug","updatedAt"],"additionalProperties":false,"example":{"ageMax":45,"ageMin":28,"brandId":"brd_6a9c33","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Runners who already own a pair, and who buy the next one without looking at another brand.","frictions":null,"id":"psn_8c4a20","language":null,"motivations":"They want the same shoe again, and proof that it is the same. A resole keeps them running on a pair they trust.","name":"Loyalists","profile":"Runners of 40 to 80 kilometres a week, ten years in, with one pair in rotation and one on the shelf.","revision":1,"slug":"loyalists","updatedAt":null}}}},"description":"The Persona as created, at `revision` 1. Every unwritten section comes back `null`, which is a normal state rather than an error."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Brand with that slug. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"The slug that is already taken."},"error":{"type":"string","enum":["slug_conflict"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false}}},"description":"This Brand already has a Persona with that slug. `error` is `slug_conflict`. Another Brand may hold the same one — the scope of the uniqueness is the Brand, not the organization."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"Two codes share this status. `invalid_slug`: the name and slug carry no letter or digit. `invalid_age_range`: the minimum is above the maximum."}},"summary":"Create a Persona","tags":["Personas"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"age_max":{"description":"Upper age bound. Omit for \"and older\".","type":"integer","minimum":0,"maximum":120},"age_min":{"description":"Lower age bound. Omit for \"and younger\".","type":"integer","minimum":0,"maximum":120},"description":{"type":"string","minLength":1,"maxLength":500,"description":"One-line summary — what agents read to pick this Persona."},"frictions":{"description":"What stops them: objections, doubts, inertia (markdown).","type":"string","minLength":1},"language":{"description":"The words the audience itself uses for the problem — verbatims and vocabulary, in their own register (markdown). Not a locale.","type":"string","minLength":1},"motivations":{"description":"What they want: jobs to be done, triggers, outcomes (markdown).","type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"profile":{"description":"Who they are: life stage, situation, role, context (markdown).","type":"string","minLength":1},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Unique within this Brand, so two Brands may each hold the same one. Defaults to one derived from the name.","type":"string","maxLength":120}},"required":["description","name"],"additionalProperties":false,"example":{"age_max":45,"age_min":28,"description":"Runners who already own a pair, and who buy the next one without looking at another brand.","motivations":"They want the same shoe again, and proof that it is the same. A resole keeps them running on a pair they trust.","name":"Loyalists","profile":"Runners of 40 to 80 kilometres a week, ten years in, with one pair in rotation and one on the shelf.","slug":"loyalists"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"age_max":{"description":"Upper age bound. Omit for \"and older\".","type":"integer","minimum":0,"maximum":120},"age_min":{"description":"Lower age bound. Omit for \"and younger\".","type":"integer","minimum":0,"maximum":120},"description":{"type":"string","minLength":1,"maxLength":500,"description":"One-line summary — what agents read to pick this Persona."},"frictions":{"description":"What stops them: objections, doubts, inertia (markdown).","type":"string","minLength":1},"language":{"description":"The words the audience itself uses for the problem — verbatims and vocabulary, in their own register (markdown). Not a locale.","type":"string","minLength":1},"motivations":{"description":"What they want: jobs to be done, triggers, outcomes (markdown).","type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"profile":{"description":"Who they are: life stage, situation, role, context (markdown).","type":"string","minLength":1},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Unique within this Brand, so two Brands may each hold the same one. Defaults to one derived from the name.","type":"string","maxLength":120}},"required":["description","name"],"additionalProperties":false,"example":{"age_max":45,"age_min":28,"description":"Runners who already own a pair, and who buy the next one without looking at another brand.","motivations":"They want the same shoe again, and proof that it is the same. A resole keeps them running on a pair they trust.","name":"Loyalists","profile":"Runners of 40 to 80 kilometres a week, ten years in, with one pair in rotation and one on the shelf.","slug":"loyalists"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"age_max":{"description":"Upper age bound. Omit for \"and older\".","type":"integer","minimum":0,"maximum":120},"age_min":{"description":"Lower age bound. Omit for \"and younger\".","type":"integer","minimum":0,"maximum":120},"description":{"type":"string","minLength":1,"maxLength":500,"description":"One-line summary — what agents read to pick this Persona."},"frictions":{"description":"What stops them: objections, doubts, inertia (markdown).","type":"string","minLength":1},"language":{"description":"The words the audience itself uses for the problem — verbatims and vocabulary, in their own register (markdown). Not a locale.","type":"string","minLength":1},"motivations":{"description":"What they want: jobs to be done, triggers, outcomes (markdown).","type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"profile":{"description":"Who they are: life stage, situation, role, context (markdown).","type":"string","minLength":1},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Unique within this Brand, so two Brands may each hold the same one. Defaults to one derived from the name.","type":"string","maxLength":120}},"required":["description","name"],"additionalProperties":false,"example":{"age_max":45,"age_min":28,"description":"Runners who already own a pair, and who buy the next one without looking at another brand.","motivations":"They want the same shoe again, and proof that it is the same. A resole keeps them running on a pair they trust.","name":"Loyalists","profile":"Runners of 40 to 80 kilometres a week, ten years in, with one pair in rotation and one on the shelf.","slug":"loyalists"}}}}}}},"/context/brands/{slug}/personas/{personaSlug}":{"get":{"security":[{"bearerAuth":[]}],"description":"Reads one Persona by its Brand slug and its own slug. The answer has every section, the age range, and the current revision. Send that revision back as `base_revision` when you update the Persona. A section that nobody wrote is `null`.","operationId":"getPersona","responses":{"200":{"content":{"application/json":{"example":{"ageMax":45,"ageMin":28,"brandId":"brd_6a9c33","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Runners who already own a pair, and who buy the next one without looking at another brand.","frictions":"They fear the new model is worse than the one they know. A change of sole or of last reads to them as a risk, not as progress.","id":"psn_8c4a20","language":"They say \"my daily trainer\" and \"I got 900 out of the last pair\". They ask about the drop and the stack, never about the technology.","motivations":"They want the same shoe again, and proof that it is the same. A resole keeps them running on a pair they trust.","name":"Loyalists","profile":"Runners of 40 to 80 kilometres a week, ten years in, with one pair in rotation and one on the shelf.","revision":2,"slug":"loyalists","updatedAt":1790086400000},"schema":{"type":"object","properties":{"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"brandId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"frictions":{"nullable":true,"description":"Objections, doubts, what stops them.","type":"string"},"id":{"type":"string"},"language":{"nullable":true,"description":"The words *they* use for the problem, in their own register. Not a locale, despite the name.","type":"string"},"motivations":{"nullable":true,"description":"What they want, and what triggers them.","type":"string"},"name":{"type":"string"},"profile":{"nullable":true,"description":"Who they are, and their situation.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["ageMax","ageMin","brandId","createdAt","createdBy","description","frictions","id","language","motivations","name","profile","revision","slug","updatedAt"],"additionalProperties":false,"example":{"ageMax":45,"ageMin":28,"brandId":"brd_6a9c33","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Runners who already own a pair, and who buy the next one without looking at another brand.","frictions":"They fear the new model is worse than the one they know. A change of sole or of last reads to them as a risk, not as progress.","id":"psn_8c4a20","language":"They say \"my daily trainer\" and \"I got 900 out of the last pair\". They ask about the drop and the stack, never about the technology.","motivations":"They want the same shoe again, and proof that it is the same. A resole keeps them running on a pair they trust.","name":"Loyalists","profile":"Runners of 40 to 80 kilometres a week, ten years in, with one pair in rotation and one on the shelf.","revision":2,"slug":"loyalists","updatedAt":1790086400000}}}},"description":"The Persona with all four sections. `brandId` is the Brand it belongs to; the pair of slugs in the path is its handle."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such Brand, or no Persona with that slug inside it. `error` is `not_found` either way — the two are deliberately not told apart."}},"summary":"Get a Persona","tags":["Personas"],"parameters":[{"name":"personaSlug","in":"path","required":true,"schema":{"type":"string","description":"The Persona slug — its handle within the Brand."}},{"name":"slug","in":"path","required":true,"schema":{"type":"string","description":"The Brand slug."}}]},"patch":{"security":[{"bearerAuth":[]}],"description":"Updates a Persona. Send the `base_revision` you read. Omit a section or an age bound to keep it. Send `null` to clear it. AdCrunch checks the age range on the result of the merge. Therefore a call that sends only `age_min` can still fail, if the result is inverted. The slug changes only when you send a `slug`: a new `name` alone never moves the handle. The answer is `409` with `error=revision_mismatch` if the Persona changed after you read it. It is `409` with `error=slug_conflict` if another Persona of this Brand already holds the `slug` you send.","operationId":"updatePersona","responses":{"200":{"content":{"application/json":{"example":{"ageMax":45,"ageMin":28,"brandId":"brd_6a9c33","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Runners who already own a pair, and who buy the next one without looking at another brand.","frictions":"They fear the new model is worse than the one they know. A change of sole or of last reads to them as a risk, not as progress.","id":"psn_8c4a20","language":"They say \"my daily trainer\" and \"I got 900 out of the last pair\". They ask about the drop and the stack, never about the technology.","motivations":"They want the same shoe again, and proof that it is the same. A resole keeps them running on a pair they trust.","name":"Loyalists","profile":"Runners of 40 to 80 kilometres a week, ten years in, with one pair in rotation and one on the shelf.","revision":3,"slug":"loyalists","updatedAt":1790086400000},"schema":{"type":"object","properties":{"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"brandId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"description":{"type":"string"},"frictions":{"nullable":true,"description":"Objections, doubts, what stops them.","type":"string"},"id":{"type":"string"},"language":{"nullable":true,"description":"The words *they* use for the problem, in their own register. Not a locale, despite the name.","type":"string"},"motivations":{"nullable":true,"description":"What they want, and what triggers them.","type":"string"},"name":{"type":"string"},"profile":{"nullable":true,"description":"Who they are, and their situation.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["ageMax","ageMin","brandId","createdAt","createdBy","description","frictions","id","language","motivations","name","profile","revision","slug","updatedAt"],"additionalProperties":false,"example":{"ageMax":45,"ageMin":28,"brandId":"brd_6a9c33","createdAt":1790000000000,"createdBy":"usr_4d8b12","description":"Runners who already own a pair, and who buy the next one without looking at another brand.","frictions":"They fear the new model is worse than the one they know. A change of sole or of last reads to them as a risk, not as progress.","id":"psn_8c4a20","language":"They say \"my daily trainer\" and \"I got 900 out of the last pair\". They ask about the drop and the stack, never about the technology.","motivations":"They want the same shoe again, and proof that it is the same. A resole keeps them running on a pair they trust.","name":"Loyalists","profile":"Runners of 40 to 80 kilometres a week, ten years in, with one pair in rotation and one on the shelf.","revision":3,"slug":"loyalists","updatedAt":1790086400000}}}},"description":"The Persona as it now stands, at the next `revision`. Send that one back on your next edit."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such Brand, or no Persona with that slug inside it. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false},{"type":"object","properties":{"slug":{"type":"string","description":"The slug that is already taken."},"error":{"type":"string","enum":["slug_conflict"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false}]}}},"description":"`error` tells the two conflicts apart: `revision_mismatch` and `slug_conflict`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"`invalid_slug`, or `invalid_age_range`. The age range is checked against the **merged** result, so sending only `age_min` can still be rejected as inverted."}},"summary":"Update a Persona","tags":["Personas"],"parameters":[{"name":"personaSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"age_max":{"description":"Upper age bound; null clears it (\"and older\").","nullable":true,"type":"integer","minimum":0,"maximum":120},"age_min":{"description":"Lower age bound; null clears it (\"and younger\").","nullable":true,"type":"integer","minimum":0,"maximum":120},"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"description":{"type":"string","minLength":1,"maxLength":500},"frictions":{"description":"Objections and barriers; null clears it.","nullable":true,"type":"string","minLength":1},"language":{"description":"The audience's own words — not a locale; null clears it.","nullable":true,"type":"string","minLength":1},"motivations":{"description":"Jobs, triggers, outcomes; null clears it.","nullable":true,"type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120},"profile":{"description":"Who they are; null clears it.","nullable":true,"type":"string","minLength":1},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":2,"frictions":"They fear the new model is worse than the one they know. A change of sole or of last reads to them as a risk, not as progress.","language":"They say \"my daily trainer\" and \"I got 900 out of the last pair\". They ask about the drop and the stack, never about the technology."}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"age_max":{"description":"Upper age bound; null clears it (\"and older\").","nullable":true,"type":"integer","minimum":0,"maximum":120},"age_min":{"description":"Lower age bound; null clears it (\"and younger\").","nullable":true,"type":"integer","minimum":0,"maximum":120},"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"description":{"type":"string","minLength":1,"maxLength":500},"frictions":{"description":"Objections and barriers; null clears it.","nullable":true,"type":"string","minLength":1},"language":{"description":"The audience's own words — not a locale; null clears it.","nullable":true,"type":"string","minLength":1},"motivations":{"description":"Jobs, triggers, outcomes; null clears it.","nullable":true,"type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120},"profile":{"description":"Who they are; null clears it.","nullable":true,"type":"string","minLength":1},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":2,"frictions":"They fear the new model is worse than the one they know. A change of sole or of last reads to them as a risk, not as progress.","language":"They say \"my daily trainer\" and \"I got 900 out of the last pair\". They ask about the drop and the stack, never about the technology."}}},"multipart/form-data":{"schema":{"type":"object","properties":{"age_max":{"description":"Upper age bound; null clears it (\"and older\").","nullable":true,"type":"integer","minimum":0,"maximum":120},"age_min":{"description":"Lower age bound; null clears it (\"and younger\").","nullable":true,"type":"integer","minimum":0,"maximum":120},"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"description":{"type":"string","minLength":1,"maxLength":500},"frictions":{"description":"Objections and barriers; null clears it.","nullable":true,"type":"string","minLength":1},"language":{"description":"The audience's own words — not a locale; null clears it.","nullable":true,"type":"string","minLength":1},"motivations":{"description":"Jobs, triggers, outcomes; null clears it.","nullable":true,"type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120},"profile":{"description":"Who they are; null clears it.","nullable":true,"type":"string","minLength":1},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":2,"frictions":"They fear the new model is worse than the one they know. A change of sole or of last reads to them as a risk, not as progress.","language":"They say \"my daily trainer\" and \"I got 900 out of the last pair\". They ask about the drop and the stack, never about the technology."}}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a Persona. Send the `base_revision` you read, in the query string. The slug becomes free again in its Brand. The answer is `409` with `error=revision_mismatch` if the Persona changed after you read it. The row is soft-deleted: the Persona leaves every listing and every read, and only an operator can bring it back.","operationId":"deletePersona","responses":{"204":{"description":"Deleted. The slug is free to re-use within this Brand. Nothing hangs off a Persona, so nothing cascades — a Line Item that named it keeps its `personaId`."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such Brand, or no Persona with that slug inside it. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false}}},"description":"The Persona moved since you read it. `error` is `revision_mismatch`."}},"summary":"Delete a Persona","tags":["Personas"],"parameters":[{"name":"personaSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"base_revision","in":"query","required":true,"schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."}}]}},"/context/brands/{slug}/documents":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the documents of a Brand, newest first. Each row has a URL to read the file from. A reservation with no bytes is not in the list, because it has no file.\n\nOne page at a time: `limit` rows at most, 100 by default. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page.","operationId":"listBrandDocuments","responses":{"200":{"content":{"application/json":{"example":{"data":[{"createdAt":1790000000000,"filename":"acme-brand-guidelines.pdf","id":"doc_5e2b41","mimeType":"application/pdf","name":"acme-brand-guidelines.pdf","sizeBytes":1482064,"targetId":"brd_6a9c33","targetType":"brand","url":"https://documents.adcrunch.dev/org_3a7f10/doc_5e2b41"},{"createdAt":1789900000000,"filename":"acme-logo.png","id":"doc_3f7a08","mimeType":"image/png","name":"Acme logo","sizeBytes":24118,"targetId":"brd_6a9c33","targetType":"brand","url":"https://documents.adcrunch.dev/org_3a7f10/doc_3f7a08"}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"filename":{"type":"string"},"id":{"type":"string"},"mimeType":{"type":"string"},"name":{"type":"string","description":"The display name. Falls back to the filename."},"sizeBytes":{"type":"number","description":"Read off the stored object at finalize, never from the client."},"targetId":{"type":"string"},"targetType":{"type":"string","description":"What the Document is attached to. `brand` today."},"url":{"type":"string"}},"required":["createdAt","filename","id","mimeType","name","sizeBytes","targetId","targetType","url"],"additionalProperties":false}},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"createdAt":1790000000000,"filename":"acme-brand-guidelines.pdf","id":"doc_5e2b41","mimeType":"application/pdf","name":"acme-brand-guidelines.pdf","sizeBytes":1482064,"targetId":"brd_6a9c33","targetType":"brand","url":"https://documents.adcrunch.dev/org_3a7f10/doc_5e2b41"},{"createdAt":1789900000000,"filename":"acme-logo.png","id":"doc_3f7a08","mimeType":"image/png","name":"Acme logo","sizeBytes":24118,"targetId":"brd_6a9c33","targetType":"brand","url":"https://documents.adcrunch.dev/org_3a7f10/doc_3f7a08"}]}}}},"description":"One page of the Brand’s finalized Documents, newest first. A reservation whose bytes never arrived is never listed, because there is no readable object behind it. This page is the last one, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Brand with that slug. `error` is `not_found`."}},"summary":"List a Brand's documents","tags":["Documents"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":true,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}}]},"post":{"security":[{"bearerAuth":[]}],"description":"Reserves a document on a Brand, and is the first of the three steps of an upload. The answer gives a URL with a short life. `PUT` the bytes to that URL with their `Content-Type`, then call finalize. The bytes do not go through this API. AdCrunch accepts PDF, GIF, JPEG, PNG and WebP. It refuses SVG, because a stored document is served from an adcrunch.dev host and an SVG carries script.","operationId":"createBrandDocumentUpload","responses":{"201":{"content":{"application/json":{"example":{"documentId":"doc_5e2b41","expiresInSeconds":900,"uploadUrl":"https://a1b2c3d4e5f6.r2.cloudflarestorage.com/documents/org_3a7f10/doc_5e2b41?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=8f1c…"},"schema":{"type":"object","properties":{"documentId":{"type":"string","description":"Pass this to the finalize call."},"expiresInSeconds":{"type":"number","description":"How long `uploadUrl` stays valid. Reserve again if it lapses."},"uploadUrl":{"type":"string","description":"A short-lived presigned URL. PUT the bytes here."}},"required":["documentId","expiresInSeconds","uploadUrl"],"additionalProperties":false,"example":{"documentId":"doc_5e2b41","expiresInSeconds":900,"uploadUrl":"https://a1b2c3d4e5f6.r2.cloudflarestorage.com/documents/org_3a7f10/doc_5e2b41?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&X-Amz-Signature=8f1c…"}}}},"description":"A reserved destination, valid for `expiresInSeconds`. PUT the bytes to `uploadUrl`, then call finalize with `documentId`. The URL itself gives write access: anybody holding it can write that one object until it lapses. The Document does not exist for any reader until finalize succeeds."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Brand with that slug. `error` is `not_found`."},"415":{"content":{"application/json":{"schema":{"type":"object","properties":{"allowed":{"type":"array","items":{"type":"string"},"description":"Every media type this API accepts as a Document."},"error":{"type":"string","enum":["unsupported_type"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["allowed","error","message"],"additionalProperties":false}}},"description":"That media type is not accepted. `error` is `unsupported_type`, and `allowed` names every type that is. This is the declared type; the stored object is checked again at finalize."}},"summary":"Start a document upload","tags":["Documents"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content_type":{"type":"string","description":"The type you are about to upload. Re-checked against the stored object at finalize — this is only a declaration."},"filename":{"type":"string","minLength":1,"maxLength":255}},"required":["content_type","filename"],"additionalProperties":false,"example":{"content_type":"application/pdf","filename":"acme-brand-guidelines.pdf"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"content_type":{"type":"string","description":"The type you are about to upload. Re-checked against the stored object at finalize — this is only a declaration."},"filename":{"type":"string","minLength":1,"maxLength":255}},"required":["content_type","filename"],"additionalProperties":false,"example":{"content_type":"application/pdf","filename":"acme-brand-guidelines.pdf"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"content_type":{"type":"string","description":"The type you are about to upload. Re-checked against the stored object at finalize — this is only a declaration."},"filename":{"type":"string","minLength":1,"maxLength":255}},"required":["content_type","filename"],"additionalProperties":false,"example":{"content_type":"application/pdf","filename":"acme-brand-guidelines.pdf"}}}}}}},"/context/brands/{slug}/documents/{documentId}/finalize":{"post":{"security":[{"bearerAuth":[]}],"description":"Completes a reserved document after you send the bytes, and is the third of the three steps of an upload. AdCrunch reads the size and the content type from the stored file. It does not take them from you, because the upload URL applies no rules of its own. This call is the only point of validation: a file over 25 MB, or of a type this API does not accept, is refused here and discarded. Until it succeeds, the Document is in no listing and in no read.","operationId":"finalizeBrandDocument","responses":{"200":{"content":{"application/json":{"example":{"createdAt":1790000000000,"filename":"acme-brand-guidelines.pdf","id":"doc_5e2b41","mimeType":"application/pdf","name":"acme-brand-guidelines.pdf","sizeBytes":1482064,"targetId":"brd_6a9c33","targetType":"brand","url":"https://documents.adcrunch.dev/org_3a7f10/doc_5e2b41"},"schema":{"type":"object","properties":{"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"filename":{"type":"string"},"id":{"type":"string"},"mimeType":{"type":"string"},"name":{"type":"string","description":"The display name. Falls back to the filename."},"sizeBytes":{"type":"number","description":"Read off the stored object at finalize, never from the client."},"targetId":{"type":"string"},"targetType":{"type":"string","description":"What the Document is attached to. `brand` today."},"url":{"type":"string"}},"required":["createdAt","filename","id","mimeType","name","sizeBytes","targetId","targetType","url"],"additionalProperties":false,"example":{"createdAt":1790000000000,"filename":"acme-brand-guidelines.pdf","id":"doc_5e2b41","mimeType":"application/pdf","name":"acme-brand-guidelines.pdf","sizeBytes":1482064,"targetId":"brd_6a9c33","targetType":"brand","url":"https://documents.adcrunch.dev/org_3a7f10/doc_5e2b41"}}}},"description":"The Document, now readable. `sizeBytes` and `mimeType` are read off the stored object rather than taken from your declaration. `name` is the display name, and it falls back to the filename. `url` is public: anybody holding it can read the file."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No reservation with that id on this Brand. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"The reservation exists but no bytes have arrived. `error` is `no_object`. PUT to the `uploadUrl` first."},"413":{"content":{"application/json":{"schema":{"type":"object","properties":{"limitBytes":{"type":"number"},"sizeBytes":{"type":"number","description":"What the stored object actually measured."},"error":{"type":"string","enum":["too_large"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["limitBytes","sizeBytes","error","message"],"additionalProperties":false}}},"description":"The stored object is over the limit. `error` is `too_large`."},"415":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"The **stored** object is not an accepted type, whatever was declared at reservation. `error` is `unsupported_type`, the `message` names what the object turned out to be, and the object has been discarded. Reserve again to retry."}},"summary":"Finalize a document upload","tags":["Documents"],"parameters":[{"name":"documentId","in":"path","required":true,"schema":{"type":"string","pattern":"^doc_[\\s\\S]{0,}$"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}]}},"/context/brands/{slug}/documents/{documentId}":{"get":{"security":[{"bearerAuth":[]}],"description":"Reads one document of a Brand. The answer has the document data and the URL to read the file from. The bytes never come back in the body: fetch `url` for them. A reservation whose bytes never arrived answers `404`, because there is no file behind it.","operationId":"getBrandDocument","responses":{"200":{"content":{"application/json":{"example":{"createdAt":1790000000000,"filename":"acme-brand-guidelines.pdf","id":"doc_5e2b41","mimeType":"application/pdf","name":"acme-brand-guidelines.pdf","sizeBytes":1482064,"targetId":"brd_6a9c33","targetType":"brand","url":"https://documents.adcrunch.dev/org_3a7f10/doc_5e2b41"},"schema":{"type":"object","properties":{"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"filename":{"type":"string"},"id":{"type":"string"},"mimeType":{"type":"string"},"name":{"type":"string","description":"The display name. Falls back to the filename."},"sizeBytes":{"type":"number","description":"Read off the stored object at finalize, never from the client."},"targetId":{"type":"string"},"targetType":{"type":"string","description":"What the Document is attached to. `brand` today."},"url":{"type":"string"}},"required":["createdAt","filename","id","mimeType","name","sizeBytes","targetId","targetType","url"],"additionalProperties":false,"example":{"createdAt":1790000000000,"filename":"acme-brand-guidelines.pdf","id":"doc_5e2b41","mimeType":"application/pdf","name":"acme-brand-guidelines.pdf","sizeBytes":1482064,"targetId":"brd_6a9c33","targetType":"brand","url":"https://documents.adcrunch.dev/org_3a7f10/doc_5e2b41"}}}},"description":"The Document. `url` is public: anybody holding it can read the file."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such Document on this Brand, or it has not been finalized. `error` is `not_found`."}},"summary":"Get a document","tags":["Documents"],"parameters":[{"name":"documentId","in":"path","required":true,"schema":{"type":"string","pattern":"^doc_[\\s\\S]{0,}$"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}]},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a document and its bytes. This operation takes no `base_revision`: a Document holds no prose for two people to write over. AdCrunch deletes the record first. Therefore a failure in the middle leaves bytes with no record, and never a record with no bytes.","operationId":"deleteBrandDocument","responses":{"204":{"description":"Deleted. If this Document was the Brand’s nominated logo, AdCrunch clears that nomination — in the application rather than in the database, because the row is soft-deleted and a delete action would never fire. **Clearing it moves the `revision` of the Brand**, so a `base_revision` you read before this call now answers `409`."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such Document on this Brand. `error` is `not_found`."}},"summary":"Delete a document","tags":["Documents"],"parameters":[{"name":"documentId","in":"path","required":true,"schema":{"type":"string","pattern":"^doc_[\\s\\S]{0,}$"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}]}},"/context/brands/{slug}/advertisers":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the advertisers attached to a Brand, newest first. Each row names the advertiser, the provider AdCrunch resolved for it when it was attached, and the Brand it points at. The reverse question — which Brands apply to one advertiser — is what the `brand_resolve` MCP tool answers; this API has no route for it.\n\nOne page at a time: `limit` rows at most, 100 by default. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page.","operationId":"listBrandAdvertisers","responses":{"200":{"content":{"application/json":{"example":{"data":[{"advertiserId":"acc_1203456789012345","createdAt":1790000000000,"provider":"meta","targetId":"brd_6a9c33","targetType":"brand"}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The ad account. A soft pointer into the integration database — there is no foreign key across databases."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"provider":{"type":"string"},"targetId":{"type":"string"},"targetType":{"type":"string"}},"required":["advertiserId","createdAt","provider","targetId","targetType"],"additionalProperties":false}},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"advertiserId":"acc_1203456789012345","createdAt":1790000000000,"provider":"meta","targetId":"brd_6a9c33","targetType":"brand"}]}}}},"description":"One page of the advertisers attached to this Brand, newest first. `targetType` is `brand` and `targetId` is this Brand, because the attachment is polymorphic in the store and only Brands are attachable today. This page is the last one, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Brand with that slug. `error` is `not_found`."}},"summary":"List a Brand's advertisers","tags":["Brands"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":true,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}}]},"post":{"security":[{"bearerAuth":[]}],"description":"Attaches an advertiser to a Brand. An agent that works on that advertiser can then find this brand context. Send the same call again for the same result: the pair is the key, so a second attach writes nothing and still answers `201`. AdCrunch reads the provider from the advertiser. It does not take the provider from you. A Brand takes several advertisers, and an advertiser takes several Brands.","operationId":"attachBrandAdvertiser","responses":{"201":{"content":{"application/json":{"example":{"advertiserId":"acc_1203456789012345","provider":"meta"},"schema":{"type":"object","properties":{"advertiserId":{"type":"string"},"provider":{"type":"string","description":"Resolved from the ad account, never taken from the caller. That resolution is also the ownership check."}},"required":["advertiserId","provider"],"additionalProperties":false,"example":{"advertiserId":"acc_1203456789012345","provider":"meta"}}}},"description":"Attached. Idempotent: attaching an account that is already attached answers the same way. `provider` is what AdCrunch resolved from the advertiser, never what you sent."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Brand with that slug. `error` is `not_found`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"That advertiser is not connected to this organization. `error` is `advertiser_not_owned`. Ownership is checked against the integration database at write time, because there is no foreign key across databases."}},"summary":"Attach an advertiser","tags":["Brands"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"advertiser_id":{"type":"string","pattern":"^acc_[\\s\\S]{0,}$"}},"required":["advertiser_id"],"additionalProperties":false,"example":{"advertiser_id":"acc_1203456789012345"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"advertiser_id":{"type":"string","pattern":"^acc_[\\s\\S]{0,}$"}},"required":["advertiser_id"],"additionalProperties":false,"example":{"advertiser_id":"acc_1203456789012345"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"advertiser_id":{"type":"string","pattern":"^acc_[\\s\\S]{0,}$"}},"required":["advertiser_id"],"additionalProperties":false,"example":{"advertiser_id":"acc_1203456789012345"}}}}}}},"/context/brands/{slug}/advertisers/{advertiserId}":{"delete":{"security":[{"bearerAuth":[]}],"description":"Detaches an advertiser from a Brand. This brand context stops applying to that advertiser, so an agent working on it no longer reads this Brand. Nothing else changes: the advertiser stays connected to your organization, the Brand keeps every section, and a Campaign Plan that names the advertiser keeps naming it. AdCrunch resolves the provider from the advertiser, as the attach does, so the ownership check runs here too.","operationId":"detachBrandAdvertiser","responses":{"204":{"description":"Detached. Unlike everything else here the link is **hard-deleted**: it has no content to preserve, and a tombstone would permanently occupy the only key that pair could use."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `brand:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"`error` tells the two apart. `not_found`: no Brand with that slug. `not_attached`: the Brand exists and that advertiser was never attached to it."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"That advertiser is not connected to this organization. `error` is `advertiser_not_owned`."}},"summary":"Detach an advertiser","tags":["Brands"],"parameters":[{"name":"advertiserId","in":"path","required":true,"schema":{"type":"string","pattern":"^acc_[\\s\\S]{0,}$"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}]}},"/context/campaign-plans":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the Campaign Plans of your organization, newest first. Each row gives the figures you compare plans by: the budget envelope, the allocated sum and whether that sum is comparable, the window, the number of lines, and the channels the Campaign Plan uses. This call does not give the Line Items. To read them, read one plan. Deleted plans are not in the list.\n\nOne page at a time: `limit` rows at most, 100 by default. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page.","operationId":"listCampaignPlans","responses":{"200":{"content":{"application/json":{"example":{"data":[{"allocation":{"allocated":2500,"comparable":true,"unallocated":2500,"unit":"total"},"brandId":"brd_6a9c33","channels":["meta"],"currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-05-31","lineItemCount":1,"name":"Spring Sale 2026","revision":3,"slug":"spring-sale","startDate":"2026-03-01","status":"approved","totalBudget":5000}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"allocation":{"type":"object","properties":{"allocated":{"nullable":true,"description":"The sum of the Line Item budgets. `null` when the lines mix units, because that sum would be meaningless.","type":"number"},"comparable":{"type":"boolean","description":"False when the lines mix totals and daily rates. A surface must then print \"not comparable\" rather than a plausible-looking wrong total."},"unallocated":{"nullable":true,"description":"The envelope less the allocated, where both are known.","type":"number"},"unit":{"nullable":true,"description":"The shared unit, when the lines agree on one.","type":"string","enum":["total","daily"]}},"required":["allocated","comparable","unallocated","unit"],"additionalProperties":false,"description":"Derived on every read. Never stored."},"brandId":{"nullable":true,"type":"string"},"channels":{"type":"array","items":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"]},"description":"Every channel its Line Items name, deduplicated."},"currency":{"type":"string"},"description":{"type":"string"},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"lineItemCount":{"type":"number"},"name":{"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","approved"]},"totalBudget":{"nullable":true,"description":"The envelope. Whole units of `currency`.","type":"number"}},"required":["allocation","brandId","channels","currency","description","endDate","lineItemCount","name","revision","slug","startDate","status","totalBudget"],"additionalProperties":false}},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"allocation":{"allocated":2500,"comparable":true,"unallocated":2500,"unit":"total"},"brandId":"brd_6a9c33","channels":["meta"],"currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-05-31","lineItemCount":1,"name":"Spring Sale 2026","revision":3,"slug":"spring-sale","startDate":"2026-03-01","status":"approved","totalBudget":5000}]}}}},"description":"One page of the Campaign Plans of the organization, newest first, with the money of each summarized and its channels named, but without its rows. Where the rows mix totals and daily rates, `allocation.comparable` is false and `allocation.allocated` is null — there is no meaningful sum to show. This page is the last one, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:read`. `error` is `forbidden`."}},"summary":"List the Campaign Plans","tags":["Campaign Plans"],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":true,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}}]},"post":{"security":[{"bearerAuth":[]}],"description":"Creates a Campaign Plan. A Campaign Plan is the AdCrunch planning document. It is **not** a provider campaign. For provider campaigns, see the entities in the Observe API.\n\nOnly a name, a description and a currency are necessary. You write the window, the budget envelope and the Brand one part at a time. Write each amount in whole units of the currency: `40.5` is €40.50.\n\nThe new plan starts in draft. The answer is `409` if the slug already exists in your organization.","operationId":"createCampaignPlan","responses":{"201":{"content":{"application/json":{"example":{"allocation":{"allocated":0,"comparable":true,"unallocated":5000,"unit":null},"brandId":"brd_6a9c33","createdAt":1768435200000,"createdBy":"usr_5d2b71","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-05-31","id":"cpl_4e1a08","lineItems":[],"measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","revision":1,"slug":"spring-sale","startDate":"2026-03-01","status":"draft","totalBudget":5000,"updatedAt":null},"schema":{"type":"object","properties":{"allocation":{"type":"object","properties":{"allocated":{"nullable":true,"description":"The sum of the Line Item budgets. `null` when the lines mix units, because that sum would be meaningless.","type":"number"},"comparable":{"type":"boolean","description":"False when the lines mix totals and daily rates. A surface must then print \"not comparable\" rather than a plausible-looking wrong total."},"unallocated":{"nullable":true,"description":"The envelope less the allocated, where both are known.","type":"number"},"unit":{"nullable":true,"description":"The shared unit, when the lines agree on one.","type":"string","enum":["total","daily"]}},"required":["allocated","comparable","unallocated","unit"],"additionalProperties":false,"description":"Derived on every read. Never stored."},"brandId":{"nullable":true,"description":"Nullable and mutable: a plan may precede its brand, cover a portfolio, or be re-pointed. Deleting a Brand leaves its plans standing.","type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"currency":{"type":"string","description":"Editable until the first Line Item budget, and frozen thereafter."},"description":{"type":"string"},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"id":{"type":"string"},"lineItems":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"nullable":true,"type":"string"},"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"budget":{"nullable":true,"description":"Whole units of the **plan’s** currency.","type":"number"},"campaignPlanId":{"type":"string"},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"The planner’s word for what is being bought, not a provider name. A plan may name a channel we cannot execute."},"countries":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"effectiveEndDate":{"nullable":true,"description":"The line’s own bound where it has one, else the plan’s.","type":"string"},"effectiveStartDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"executable":{"type":"boolean","description":"Whether this channel could be executed at all today. Derived from our capabilities, never a promise that it will be."},"executions":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"entityId":{"type":"string","description":"The provider’s own id, unprefixed, so it joins to `entities`."},"entityType":{"type":"string","description":"The provider’s word for the level — `campaign`, `adset`, `ad`."},"id":{"type":"string"},"mutationWorkflowId":{"nullable":true,"description":"The durable execution that performed the write, when known.","type":"string"},"provider":{"type":"string"}},"required":["advertiserId","createdAt","entityId","entityType","id","mutationWorkflowId","provider"],"additionalProperties":false},"description":"What this line has spawned. Never a claim that it is *finished* — nobody declared how many objects a line should produce."},"gender":{"nullable":true,"type":"string","enum":["all","men","women"]},"id":{"type":"string"},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"]},"personaId":{"nullable":true,"description":"Explains the targeting; never supplies its values.","type":"string"},"provider":{"nullable":true,"description":"Who sells this channel, or null when nobody we integrate with does.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","validated"],"description":"`validated` is the gate on execution."},"unit":{"type":"string","enum":["total","daily"],"description":"How `budget` reads, derived from the effective end date. Present → a total for the period; absent → a daily rate. A surface must print this beside the amount."},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["advertiserId","ageMax","ageMin","budget","campaignPlanId","channel","countries","createdAt","createdBy","effectiveEndDate","effectiveStartDate","endDate","executable","executions","gender","id","objective","personaId","provider","revision","startDate","status","unit","updatedAt"],"additionalProperties":false}},"measurement":{"nullable":true,"type":"string"},"name":{"type":"string"},"rationale":{"nullable":true,"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","approved"],"description":"The only two stored states. `active`, `closed` and \"executed\" are derived, never stored."},"totalBudget":{"nullable":true,"type":"number"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["allocation","brandId","createdAt","createdBy","currency","description","endDate","id","lineItems","measurement","name","rationale","revision","slug","startDate","status","totalBudget","updatedAt"],"additionalProperties":false,"example":{"allocation":{"allocated":0,"comparable":true,"unallocated":5000,"unit":null},"brandId":"brd_6a9c33","createdAt":1768435200000,"createdBy":"usr_5d2b71","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-05-31","id":"cpl_4e1a08","lineItems":[],"measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","revision":1,"slug":"spring-sale","startDate":"2026-03-01","status":"draft","totalBudget":5000,"updatedAt":null}}}},"description":"The Campaign Plan as created, in `draft`, at `revision` 1, with no rows. `allocation.allocated` is 0 and `unallocated` is the whole envelope."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"`brand_name` names no Brand in this organization. `error` is `not_found`. Despite its name the field takes a Brand **slug**."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"The slug that is already taken."},"error":{"type":"string","enum":["slug_conflict"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false}}},"description":"Another Campaign Plan already holds that slug. `error` is `slug_conflict`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"Four codes share this status. `invalid_slug`; `invalid_currency` (it must be a three-letter ISO 4217 code); `invalid_amount` (an amount has no more decimals than its currency, and is never negative); `invalid_window` (the end precedes the start)."}},"summary":"Create a Campaign Plan","tags":["Campaign Plans"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"brand_name":{"description":"The Brand's slug. Optional and re-pointable — a Campaign Plan may precede its brand or cover a portfolio. Addressed by slug because that is the handle every Context surface uses; the Campaign Plan stores the id.","type":"string"},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"ISO 4217. Editable while no Line Item carries a budget, frozen thereafter."},"description":{"type":"string","minLength":1,"maxLength":500,"description":"One-line summary — what a reader picks this Campaign Plan by."},"end_date":{"description":"Omit for an always-on plan. Line Items inherit this.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"measurement":{"description":"How success will be judged (markdown).","type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"rationale":{"description":"The argument for the split — what a reviewer approves (markdown).","type":"string","minLength":1},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.","type":"string","maxLength":120},"start_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"total_budget":{"description":"The envelope: what we have. Omit for \"no declared constraint\" — which is not the same as zero.","type":"number","minimum":0}},"required":["currency","description","name"],"additionalProperties":false,"example":{"brand_name":"acme","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","end_date":"2026-05-31","measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","slug":"spring-sale","start_date":"2026-03-01","total_budget":5000}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"brand_name":{"description":"The Brand's slug. Optional and re-pointable — a Campaign Plan may precede its brand or cover a portfolio. Addressed by slug because that is the handle every Context surface uses; the Campaign Plan stores the id.","type":"string"},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"ISO 4217. Editable while no Line Item carries a budget, frozen thereafter."},"description":{"type":"string","minLength":1,"maxLength":500,"description":"One-line summary — what a reader picks this Campaign Plan by."},"end_date":{"description":"Omit for an always-on plan. Line Items inherit this.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"measurement":{"description":"How success will be judged (markdown).","type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"rationale":{"description":"The argument for the split — what a reviewer approves (markdown).","type":"string","minLength":1},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.","type":"string","maxLength":120},"start_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"total_budget":{"description":"The envelope: what we have. Omit for \"no declared constraint\" — which is not the same as zero.","type":"number","minimum":0}},"required":["currency","description","name"],"additionalProperties":false,"example":{"brand_name":"acme","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","end_date":"2026-05-31","measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","slug":"spring-sale","start_date":"2026-03-01","total_budget":5000}}},"multipart/form-data":{"schema":{"type":"object","properties":{"brand_name":{"description":"The Brand's slug. Optional and re-pointable — a Campaign Plan may precede its brand or cover a portfolio. Addressed by slug because that is the handle every Context surface uses; the Campaign Plan stores the id.","type":"string"},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"ISO 4217. Editable while no Line Item carries a budget, frozen thereafter."},"description":{"type":"string","minLength":1,"maxLength":500,"description":"One-line summary — what a reader picks this Campaign Plan by."},"end_date":{"description":"Omit for an always-on plan. Line Items inherit this.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"measurement":{"description":"How success will be judged (markdown).","type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120,"description":"Display name."},"rationale":{"description":"The argument for the split — what a reviewer approves (markdown).","type":"string","minLength":1},"slug":{"description":"Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.","type":"string","maxLength":120},"start_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"total_budget":{"description":"The envelope: what we have. Omit for \"no declared constraint\" — which is not the same as zero.","type":"number","minimum":0}},"required":["currency","description","name"],"additionalProperties":false,"example":{"brand_name":"acme","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","end_date":"2026-05-31","measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","slug":"spring-sale","start_date":"2026-03-01","total_budget":5000}}}}}}},"/context/campaign-plans/{slug}":{"get":{"security":[{"bearerAuth":[]}],"description":"Reads one Campaign Plan with its Line Items **in full**, and with the provider objects each line created.\n\nEach line has four calculated fields: the provider that sells its channel, whether AdCrunch can execute the line, the window the line resolves to from its own dates and the Campaign Plan dates, and whether its amount is a total or a daily rate.\n\nThe plan has the three money figures. If `allocation.comparable` is `false`, the lines do not share one unit. Show \"not comparable\" in that condition. Do not show a total.\n\nSend `revision` back as `base_revision` when you write to the Campaign Plan.","operationId":"getCampaignPlan","responses":{"200":{"content":{"application/json":{"example":{"allocation":{"allocated":2500,"comparable":true,"unallocated":2500,"unit":"total"},"brandId":"brd_6a9c33","createdAt":1768435200000,"createdBy":"usr_5d2b71","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-05-31","id":"cpl_4e1a08","lineItems":[{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":2,"startDate":null,"status":"validated","unit":"total","updatedAt":1769040000000}],"measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","revision":3,"slug":"spring-sale","startDate":"2026-03-01","status":"approved","totalBudget":5000,"updatedAt":1769040000000},"schema":{"type":"object","properties":{"allocation":{"type":"object","properties":{"allocated":{"nullable":true,"description":"The sum of the Line Item budgets. `null` when the lines mix units, because that sum would be meaningless.","type":"number"},"comparable":{"type":"boolean","description":"False when the lines mix totals and daily rates. A surface must then print \"not comparable\" rather than a plausible-looking wrong total."},"unallocated":{"nullable":true,"description":"The envelope less the allocated, where both are known.","type":"number"},"unit":{"nullable":true,"description":"The shared unit, when the lines agree on one.","type":"string","enum":["total","daily"]}},"required":["allocated","comparable","unallocated","unit"],"additionalProperties":false,"description":"Derived on every read. Never stored."},"brandId":{"nullable":true,"description":"Nullable and mutable: a plan may precede its brand, cover a portfolio, or be re-pointed. Deleting a Brand leaves its plans standing.","type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"currency":{"type":"string","description":"Editable until the first Line Item budget, and frozen thereafter."},"description":{"type":"string"},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"id":{"type":"string"},"lineItems":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"nullable":true,"type":"string"},"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"budget":{"nullable":true,"description":"Whole units of the **plan’s** currency.","type":"number"},"campaignPlanId":{"type":"string"},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"The planner’s word for what is being bought, not a provider name. A plan may name a channel we cannot execute."},"countries":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"effectiveEndDate":{"nullable":true,"description":"The line’s own bound where it has one, else the plan’s.","type":"string"},"effectiveStartDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"executable":{"type":"boolean","description":"Whether this channel could be executed at all today. Derived from our capabilities, never a promise that it will be."},"executions":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"entityId":{"type":"string","description":"The provider’s own id, unprefixed, so it joins to `entities`."},"entityType":{"type":"string","description":"The provider’s word for the level — `campaign`, `adset`, `ad`."},"id":{"type":"string"},"mutationWorkflowId":{"nullable":true,"description":"The durable execution that performed the write, when known.","type":"string"},"provider":{"type":"string"}},"required":["advertiserId","createdAt","entityId","entityType","id","mutationWorkflowId","provider"],"additionalProperties":false},"description":"What this line has spawned. Never a claim that it is *finished* — nobody declared how many objects a line should produce."},"gender":{"nullable":true,"type":"string","enum":["all","men","women"]},"id":{"type":"string"},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"]},"personaId":{"nullable":true,"description":"Explains the targeting; never supplies its values.","type":"string"},"provider":{"nullable":true,"description":"Who sells this channel, or null when nobody we integrate with does.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","validated"],"description":"`validated` is the gate on execution."},"unit":{"type":"string","enum":["total","daily"],"description":"How `budget` reads, derived from the effective end date. Present → a total for the period; absent → a daily rate. A surface must print this beside the amount."},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["advertiserId","ageMax","ageMin","budget","campaignPlanId","channel","countries","createdAt","createdBy","effectiveEndDate","effectiveStartDate","endDate","executable","executions","gender","id","objective","personaId","provider","revision","startDate","status","unit","updatedAt"],"additionalProperties":false}},"measurement":{"nullable":true,"type":"string"},"name":{"type":"string"},"rationale":{"nullable":true,"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","approved"],"description":"The only two stored states. `active`, `closed` and \"executed\" are derived, never stored."},"totalBudget":{"nullable":true,"type":"number"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["allocation","brandId","createdAt","createdBy","currency","description","endDate","id","lineItems","measurement","name","rationale","revision","slug","startDate","status","totalBudget","updatedAt"],"additionalProperties":false,"example":{"allocation":{"allocated":2500,"comparable":true,"unallocated":2500,"unit":"total"},"brandId":"brd_6a9c33","createdAt":1768435200000,"createdBy":"usr_5d2b71","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-05-31","id":"cpl_4e1a08","lineItems":[{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":2,"startDate":null,"status":"validated","unit":"total","updatedAt":1769040000000}],"measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","revision":3,"slug":"spring-sale","startDate":"2026-03-01","status":"approved","totalBudget":5000,"updatedAt":1769040000000}}}},"description":"The Campaign Plan with its Line Items **in full**. That is why there is no single-Line-Item fetch: progressive disclosure exists to keep unbounded prose out of a bundle, and a Line Item has none. This is the one answer that carries what each line created."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Campaign Plan with that slug. `error` is `not_found`."}},"summary":"Get a Campaign Plan","tags":["Campaign Plans"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}]},"patch":{"security":[{"bearerAuth":[]}],"description":"Updates a Campaign Plan. Send the `base_revision` you read. Omit a field to keep it. Send `null` to clear it.\n\n**Any change puts an approved plan back into draft.** An approval speaks about content, and it cannot survive a change to that content. A write that changes nothing keeps the approval, and it does not move the `revision`.\n\nThe Line Items keep the state they hold. The revert is local to the Campaign Plan, so a `validated` line stays validated.\n\nThe answer is `409` with `error=currency_frozen` if a Line Item already has a budget, with `error=revision_mismatch` if the Campaign Plan changed after you read it, and with `error=slug_conflict` if the new name gives a slug that already exists.","operationId":"updateCampaignPlan","responses":{"200":{"content":{"application/json":{"example":{"allocation":{"allocated":2500,"comparable":true,"unallocated":5000,"unit":"total"},"brandId":"brd_6a9c33","createdAt":1768435200000,"createdBy":"usr_5d2b71","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-06-30","id":"cpl_4e1a08","lineItems":[{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-06-30","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":2,"startDate":null,"status":"validated","unit":"total","updatedAt":1769040000000}],"measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","revision":4,"slug":"spring-sale","startDate":"2026-03-01","status":"draft","totalBudget":7500,"updatedAt":1769644800000},"schema":{"type":"object","properties":{"allocation":{"type":"object","properties":{"allocated":{"nullable":true,"description":"The sum of the Line Item budgets. `null` when the lines mix units, because that sum would be meaningless.","type":"number"},"comparable":{"type":"boolean","description":"False when the lines mix totals and daily rates. A surface must then print \"not comparable\" rather than a plausible-looking wrong total."},"unallocated":{"nullable":true,"description":"The envelope less the allocated, where both are known.","type":"number"},"unit":{"nullable":true,"description":"The shared unit, when the lines agree on one.","type":"string","enum":["total","daily"]}},"required":["allocated","comparable","unallocated","unit"],"additionalProperties":false,"description":"Derived on every read. Never stored."},"brandId":{"nullable":true,"description":"Nullable and mutable: a plan may precede its brand, cover a portfolio, or be re-pointed. Deleting a Brand leaves its plans standing.","type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"currency":{"type":"string","description":"Editable until the first Line Item budget, and frozen thereafter."},"description":{"type":"string"},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"id":{"type":"string"},"lineItems":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"nullable":true,"type":"string"},"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"budget":{"nullable":true,"description":"Whole units of the **plan’s** currency.","type":"number"},"campaignPlanId":{"type":"string"},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"The planner’s word for what is being bought, not a provider name. A plan may name a channel we cannot execute."},"countries":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"effectiveEndDate":{"nullable":true,"description":"The line’s own bound where it has one, else the plan’s.","type":"string"},"effectiveStartDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"executable":{"type":"boolean","description":"Whether this channel could be executed at all today. Derived from our capabilities, never a promise that it will be."},"executions":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"entityId":{"type":"string","description":"The provider’s own id, unprefixed, so it joins to `entities`."},"entityType":{"type":"string","description":"The provider’s word for the level — `campaign`, `adset`, `ad`."},"id":{"type":"string"},"mutationWorkflowId":{"nullable":true,"description":"The durable execution that performed the write, when known.","type":"string"},"provider":{"type":"string"}},"required":["advertiserId","createdAt","entityId","entityType","id","mutationWorkflowId","provider"],"additionalProperties":false},"description":"What this line has spawned. Never a claim that it is *finished* — nobody declared how many objects a line should produce."},"gender":{"nullable":true,"type":"string","enum":["all","men","women"]},"id":{"type":"string"},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"]},"personaId":{"nullable":true,"description":"Explains the targeting; never supplies its values.","type":"string"},"provider":{"nullable":true,"description":"Who sells this channel, or null when nobody we integrate with does.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","validated"],"description":"`validated` is the gate on execution."},"unit":{"type":"string","enum":["total","daily"],"description":"How `budget` reads, derived from the effective end date. Present → a total for the period; absent → a daily rate. A surface must print this beside the amount."},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["advertiserId","ageMax","ageMin","budget","campaignPlanId","channel","countries","createdAt","createdBy","effectiveEndDate","effectiveStartDate","endDate","executable","executions","gender","id","objective","personaId","provider","revision","startDate","status","unit","updatedAt"],"additionalProperties":false}},"measurement":{"nullable":true,"type":"string"},"name":{"type":"string"},"rationale":{"nullable":true,"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","approved"],"description":"The only two stored states. `active`, `closed` and \"executed\" are derived, never stored."},"totalBudget":{"nullable":true,"type":"number"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["allocation","brandId","createdAt","createdBy","currency","description","endDate","id","lineItems","measurement","name","rationale","revision","slug","startDate","status","totalBudget","updatedAt"],"additionalProperties":false,"example":{"allocation":{"allocated":2500,"comparable":true,"unallocated":5000,"unit":"total"},"brandId":"brd_6a9c33","createdAt":1768435200000,"createdBy":"usr_5d2b71","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-06-30","id":"cpl_4e1a08","lineItems":[{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-06-30","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":2,"startDate":null,"status":"validated","unit":"total","updatedAt":1769040000000}],"measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","revision":4,"slug":"spring-sale","startDate":"2026-03-01","status":"draft","totalBudget":7500,"updatedAt":1769644800000}}}},"description":"The Campaign Plan as it now stands, with its Line Items. A change takes the approval away and moves the `revision`; the Line Items keep the state they hold."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Campaign Plan with that slug, or `brand_name` names no Brand. `error` is `not_found` for both."},"409":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false},{"type":"object","properties":{"slug":{"type":"string","description":"The slug that is already taken."},"error":{"type":"string","enum":["slug_conflict"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["slug","error","message"],"additionalProperties":false}]}}},"description":"Four codes share this status. `revision_mismatch`: somebody wrote first. `slug_conflict`: the rename collides. `persona_not_in_brand`: re-pointing the Campaign Plan would orphan Line Items that name a Persona of the current Brand, and `lineItemIds` names them — they are not cleared for you. `currency_frozen`: a Line Item already carries a budget, so the denomination is fixed; nothing is ever converted."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"`invalid_slug`, `invalid_currency`, `invalid_amount` or `invalid_window`."}},"summary":"Update a Campaign Plan","tags":["Campaign Plans"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"brand_name":{"description":"Re-point the Campaign Plan at another Brand by slug; null unlinks it. Refused with 409 error=persona_not_in_brand while any Line Item still names a Persona of the current Brand.","nullable":true,"type":"string"},"currency":{"description":"ISO 4217. Refused with 409 error=currency_frozen once any Line Item carries a budget — re-denominating a costed plan would silently reinterpret every number in it.","type":"string","minLength":3,"maxLength":3},"description":{"type":"string","minLength":1,"maxLength":500},"end_date":{"description":"null makes the Campaign Plan always-on — which also flips every inheriting line’s amount from a total to a daily rate.","nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"measurement":{"nullable":true,"type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120},"rationale":{"nullable":true,"type":"string","minLength":1},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120},"start_date":{"nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"total_budget":{"description":"null means \"no declared constraint\", not zero.","nullable":true,"type":"number","minimum":0}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":3,"end_date":"2026-06-30","total_budget":7500}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"brand_name":{"description":"Re-point the Campaign Plan at another Brand by slug; null unlinks it. Refused with 409 error=persona_not_in_brand while any Line Item still names a Persona of the current Brand.","nullable":true,"type":"string"},"currency":{"description":"ISO 4217. Refused with 409 error=currency_frozen once any Line Item carries a budget — re-denominating a costed plan would silently reinterpret every number in it.","type":"string","minLength":3,"maxLength":3},"description":{"type":"string","minLength":1,"maxLength":500},"end_date":{"description":"null makes the Campaign Plan always-on — which also flips every inheriting line’s amount from a total to a daily rate.","nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"measurement":{"nullable":true,"type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120},"rationale":{"nullable":true,"type":"string","minLength":1},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120},"start_date":{"nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"total_budget":{"description":"null means \"no declared constraint\", not zero.","nullable":true,"type":"number","minimum":0}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":3,"end_date":"2026-06-30","total_budget":7500}}},"multipart/form-data":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"brand_name":{"description":"Re-point the Campaign Plan at another Brand by slug; null unlinks it. Refused with 409 error=persona_not_in_brand while any Line Item still names a Persona of the current Brand.","nullable":true,"type":"string"},"currency":{"description":"ISO 4217. Refused with 409 error=currency_frozen once any Line Item carries a budget — re-denominating a costed plan would silently reinterpret every number in it.","type":"string","minLength":3,"maxLength":3},"description":{"type":"string","minLength":1,"maxLength":500},"end_date":{"description":"null makes the Campaign Plan always-on — which also flips every inheriting line’s amount from a total to a daily rate.","nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"measurement":{"nullable":true,"type":"string","minLength":1},"name":{"type":"string","minLength":1,"maxLength":120},"rationale":{"nullable":true,"type":"string","minLength":1},"slug":{"description":"Optional new slug handle (a deliberate rename).","type":"string","maxLength":120},"start_date":{"nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"total_budget":{"description":"null means \"no declared constraint\", not zero.","nullable":true,"type":"number","minimum":0}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":3,"end_date":"2026-06-30","total_budget":7500}}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a Campaign Plan and its Line Items. Send the `base_revision` you read. The slug becomes free again.\n\nAdCrunch **keeps** the record of what those lines already created. Those provider objects are live in an ad account, and the deletion of a planning row does not remove them.\n\nThe answer is `409` with `error=revision_mismatch` if the Campaign Plan changed after you read it.","operationId":"deleteCampaignPlan","responses":{"204":{"description":"Deleted, with its Line Items. What those lines already created is **kept**: those provider objects are live in somebody’s ad account and were never ours to withdraw."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Campaign Plan with that slug. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false}}},"description":"The plan moved since you read it. `error` is `revision_mismatch`. Adding or deleting a Line Item moves an **approved** plan, because it takes the approval away. Re-read the Campaign Plan before you delete it."}},"summary":"Delete a Campaign Plan","tags":["Campaign Plans"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"base_revision","in":"query","required":true,"schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."}}]}},"/context/campaign-plans/{slug}/approve":{"post":{"security":[{"bearerAuth":[]}],"description":"Approves a Campaign Plan and validates its draft Line Items. This opens the gate for execution: AdCrunch executes a line only after somebody validates it.\n\n**This call blocks on nothing.** You can approve a Campaign Plan with no budget, with more allocated than the envelope, or with parts missing. Approval is the human act, and execution makes its own checks.\n\nThis call creates nothing and spends nothing. To execute a line, use the mutation tools with the `mutation:write` permission.\n\nThis call moves the `revision` of the Campaign Plan, and the `revision` of each Line Item it validates. Read the Campaign Plan again before your next guarded write.\n\nA line that you add after the approval starts in draft, and it puts the Campaign Plan back into draft. The answer is `409` with `error=revision_mismatch` if the Campaign Plan changed after you read it.","operationId":"approveCampaignPlan","responses":{"200":{"content":{"application/json":{"example":{"campaignPlan":{"allocation":{"allocated":2500,"comparable":true,"unallocated":2500,"unit":"total"},"brandId":"brd_6a9c33","createdAt":1768435200000,"createdBy":"usr_5d2b71","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-05-31","id":"cpl_4e1a08","lineItems":[{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":2,"startDate":null,"status":"validated","unit":"total","updatedAt":1769040000000}],"measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","revision":3,"slug":"spring-sale","startDate":"2026-03-01","status":"approved","totalBudget":5000,"updatedAt":1769040000000},"lineItemsValidated":1},"schema":{"type":"object","properties":{"campaignPlan":{"type":"object","properties":{"allocation":{"type":"object","properties":{"allocated":{"nullable":true,"description":"The sum of the Line Item budgets. `null` when the lines mix units, because that sum would be meaningless.","type":"number"},"comparable":{"type":"boolean","description":"False when the lines mix totals and daily rates. A surface must then print \"not comparable\" rather than a plausible-looking wrong total."},"unallocated":{"nullable":true,"description":"The envelope less the allocated, where both are known.","type":"number"},"unit":{"nullable":true,"description":"The shared unit, when the lines agree on one.","type":"string","enum":["total","daily"]}},"required":["allocated","comparable","unallocated","unit"],"additionalProperties":false,"description":"Derived on every read. Never stored."},"brandId":{"nullable":true,"description":"Nullable and mutable: a plan may precede its brand, cover a portfolio, or be re-pointed. Deleting a Brand leaves its plans standing.","type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"currency":{"type":"string","description":"Editable until the first Line Item budget, and frozen thereafter."},"description":{"type":"string"},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"id":{"type":"string"},"lineItems":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"nullable":true,"type":"string"},"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"budget":{"nullable":true,"description":"Whole units of the **plan’s** currency.","type":"number"},"campaignPlanId":{"type":"string"},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"The planner’s word for what is being bought, not a provider name. A plan may name a channel we cannot execute."},"countries":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"effectiveEndDate":{"nullable":true,"description":"The line’s own bound where it has one, else the plan’s.","type":"string"},"effectiveStartDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"executable":{"type":"boolean","description":"Whether this channel could be executed at all today. Derived from our capabilities, never a promise that it will be."},"executions":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"entityId":{"type":"string","description":"The provider’s own id, unprefixed, so it joins to `entities`."},"entityType":{"type":"string","description":"The provider’s word for the level — `campaign`, `adset`, `ad`."},"id":{"type":"string"},"mutationWorkflowId":{"nullable":true,"description":"The durable execution that performed the write, when known.","type":"string"},"provider":{"type":"string"}},"required":["advertiserId","createdAt","entityId","entityType","id","mutationWorkflowId","provider"],"additionalProperties":false},"description":"What this line has spawned. Never a claim that it is *finished* — nobody declared how many objects a line should produce."},"gender":{"nullable":true,"type":"string","enum":["all","men","women"]},"id":{"type":"string"},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"]},"personaId":{"nullable":true,"description":"Explains the targeting; never supplies its values.","type":"string"},"provider":{"nullable":true,"description":"Who sells this channel, or null when nobody we integrate with does.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","validated"],"description":"`validated` is the gate on execution."},"unit":{"type":"string","enum":["total","daily"],"description":"How `budget` reads, derived from the effective end date. Present → a total for the period; absent → a daily rate. A surface must print this beside the amount."},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["advertiserId","ageMax","ageMin","budget","campaignPlanId","channel","countries","createdAt","createdBy","effectiveEndDate","effectiveStartDate","endDate","executable","executions","gender","id","objective","personaId","provider","revision","startDate","status","unit","updatedAt"],"additionalProperties":false}},"measurement":{"nullable":true,"type":"string"},"name":{"type":"string"},"rationale":{"nullable":true,"type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"slug":{"type":"string"},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","approved"],"description":"The only two stored states. `active`, `closed` and \"executed\" are derived, never stored."},"totalBudget":{"nullable":true,"type":"number"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["allocation","brandId","createdAt","createdBy","currency","description","endDate","id","lineItems","measurement","name","rationale","revision","slug","startDate","status","totalBudget","updatedAt"],"additionalProperties":false},"lineItemsValidated":{"type":"number","description":"How many draft Line Items this approval moved to `validated`. Zero when they were all validated already."}},"required":["campaignPlan","lineItemsValidated"],"additionalProperties":false,"example":{"campaignPlan":{"allocation":{"allocated":2500,"comparable":true,"unallocated":2500,"unit":"total"},"brandId":"brd_6a9c33","createdAt":1768435200000,"createdBy":"usr_5d2b71","currency":"EUR","description":"The spring sale of Acme, on Meta, in France and Belgium.","endDate":"2026-05-31","id":"cpl_4e1a08","lineItems":[{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":2,"startDate":null,"status":"validated","unit":"total","updatedAt":1769040000000}],"measurement":"Revenue in the window, against the spring of 2025.","name":"Spring Sale 2026","rationale":"Meta carries the reach. One Line Item keeps the budget in one place.","revision":3,"slug":"spring-sale","startDate":"2026-03-01","status":"approved","totalBudget":5000,"updatedAt":1769040000000},"lineItemsValidated":1}}}},"description":"The approved plan, and how many Line Items this call validated. A line that was already validated is not counted. **Approving spends nothing and creates nothing** — it opens the execution gate, and executing goes through the mutation tools on `mutation:write`."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Campaign Plan with that slug. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false}}},"description":"The plan moved since you read it. `error` is `revision_mismatch`. This is what makes the call an approval **of the content you read**, rather than of whatever the row holds now."}},"summary":"Approve a Campaign Plan","tags":["Campaign Plans"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET. This is what makes it an approval **of the content that was read**, rather than of whatever the row holds now."}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":2}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET. This is what makes it an approval **of the content that was read**, rather than of whatever the row holds now."}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":2}}},"multipart/form-data":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET. This is what makes it an approval **of the content that was read**, rather than of whatever the row holds now."}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":2}}}}}}},"/context/campaign-plans/{slug}/line-items":{"post":{"security":[{"bearerAuth":[]}],"description":"Adds a Line Item to a Campaign Plan. One Line Item is one thing you buy. It is coarser than an ad set, and it is not one campaign.\n\nA Line Item holds the **envelope** of an intent, not a script for execution. One line can produce more than one provider object. Therefore it holds only what is the same for all of them. This is why a line has no optimization goal.\n\nOnly the channel and the objective are necessary. The new line starts in draft. If the Campaign Plan was approved, this call puts the Campaign Plan back into draft, and the answer tells you so.","operationId":"createLineItem","responses":{"201":{"content":{"application/json":{"example":{"campaignPlanStatus":"draft","lineItem":{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":1,"startDate":null,"status":"draft","unit":"total","updatedAt":null}},"schema":{"type":"object","properties":{"campaignPlanStatus":{"type":"string","enum":["draft","approved"],"description":"The plan’s status after the insert. Adding a row to an approved plan returns it to draft."},"lineItem":{"type":"object","properties":{"advertiserId":{"nullable":true,"type":"string"},"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"budget":{"nullable":true,"description":"Whole units of the **plan’s** currency.","type":"number"},"campaignPlanId":{"type":"string"},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"The planner’s word for what is being bought, not a provider name. A plan may name a channel we cannot execute."},"countries":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"effectiveEndDate":{"nullable":true,"description":"The line’s own bound where it has one, else the plan’s.","type":"string"},"effectiveStartDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"executable":{"type":"boolean","description":"Whether this channel could be executed at all today. Derived from our capabilities, never a promise that it will be."},"executions":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"entityId":{"type":"string","description":"The provider’s own id, unprefixed, so it joins to `entities`."},"entityType":{"type":"string","description":"The provider’s word for the level — `campaign`, `adset`, `ad`."},"id":{"type":"string"},"mutationWorkflowId":{"nullable":true,"description":"The durable execution that performed the write, when known.","type":"string"},"provider":{"type":"string"}},"required":["advertiserId","createdAt","entityId","entityType","id","mutationWorkflowId","provider"],"additionalProperties":false},"description":"What this line has spawned. Never a claim that it is *finished* — nobody declared how many objects a line should produce."},"gender":{"nullable":true,"type":"string","enum":["all","men","women"]},"id":{"type":"string"},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"]},"personaId":{"nullable":true,"description":"Explains the targeting; never supplies its values.","type":"string"},"provider":{"nullable":true,"description":"Who sells this channel, or null when nobody we integrate with does.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","validated"],"description":"`validated` is the gate on execution."},"unit":{"type":"string","enum":["total","daily"],"description":"How `budget` reads, derived from the effective end date. Present → a total for the period; absent → a daily rate. A surface must print this beside the amount."},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["advertiserId","ageMax","ageMin","budget","campaignPlanId","channel","countries","createdAt","createdBy","effectiveEndDate","effectiveStartDate","endDate","executable","executions","gender","id","objective","personaId","provider","revision","startDate","status","unit","updatedAt"],"additionalProperties":false}},"required":["campaignPlanStatus","lineItem"],"additionalProperties":false,"example":{"campaignPlanStatus":"draft","lineItem":{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":1,"startDate":null,"status":"draft","unit":"total","updatedAt":null}}}}},"description":"The Line Item, and the Campaign Plan’s status after the insert. This is the one create here that wraps its row: adding a line to an approved plan returns that Campaign Plan to `draft`, and you have to be told. The line carries the dates it resolves to, the provider that sells its channel, and the unit its amount reads in."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Campaign Plan with that slug. `error` is `not_found`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"Six codes share this status. `invalid_countries` (each entry must be a two-letter ISO 3166-1 alpha-2 code; AdCrunch puts it in upper case for you and removes a duplicate); `invalid_age_range` (each bound is a whole number from 0 to 120, and the minimum may not exceed the maximum); `invalid_amount` (whole units of the Campaign Plan’s currency, with no more decimals than the currency has, never negative); `invalid_window`; `persona_not_in_brand` (a Persona must belong to the Campaign Plan’s Brand, so a brandless plan can name none); `advertiser_not_owned`."}},"summary":"Add a Line Item","tags":["Line Items"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"advertiser_id":{"description":"The ad account this line runs through. On the line rather than the Campaign Plan, because an account belongs to one provider and a Campaign Plan spans several channels.","type":"string","pattern":"^acc_[\\s\\S]{0,}$"},"age_max":{"type":"integer","minimum":0,"maximum":120},"age_min":{"type":"integer","minimum":0,"maximum":120},"budget":{"description":"Whole units of the **plan’s** currency: `40.5` is €40.50. Whether it reads as a total or a daily rate is derived from the effective end date, never stored.","type":"number","minimum":0},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"What is being bought, in a planner’s vocabulary — not a provider. Curated and deliberately incomplete."},"countries":{"description":"ISO 3166-1 alpha-2. Empty is a drafting state, not an error.","type":"array","items":{"type":"string","minLength":2,"maxLength":2}},"end_date":{"description":"Omit to inherit the Campaign Plan’s. Present (here or inherited) makes the budget a total for the period; absent makes it a daily rate.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"gender":{"type":"string","enum":["all","men","women"],"description":"Omit for undecided, which executes the same as \"all\"."},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"],"description":"Provider-neutral outcome, translated at execution. There is deliberately no optimization goal here."},"persona_id":{"description":"Must belong to the Campaign Plan’s Brand. It **explains** the targeting and never supplies its values — the countries and age range above are what execute.","type":"string","pattern":"^psn_[\\s\\S]{0,}$"},"start_date":{"description":"Omit to inherit the Campaign Plan’s.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["channel","objective"],"additionalProperties":false,"example":{"advertiser_id":"acc_1203456789012345","age_max":44,"age_min":25,"budget":2500,"channel":"meta","countries":["FR","BE"],"gender":"all","objective":"sales","persona_id":"psn_8c4a20"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"advertiser_id":{"description":"The ad account this line runs through. On the line rather than the Campaign Plan, because an account belongs to one provider and a Campaign Plan spans several channels.","type":"string","pattern":"^acc_[\\s\\S]{0,}$"},"age_max":{"type":"integer","minimum":0,"maximum":120},"age_min":{"type":"integer","minimum":0,"maximum":120},"budget":{"description":"Whole units of the **plan’s** currency: `40.5` is €40.50. Whether it reads as a total or a daily rate is derived from the effective end date, never stored.","type":"number","minimum":0},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"What is being bought, in a planner’s vocabulary — not a provider. Curated and deliberately incomplete."},"countries":{"description":"ISO 3166-1 alpha-2. Empty is a drafting state, not an error.","type":"array","items":{"type":"string","minLength":2,"maxLength":2}},"end_date":{"description":"Omit to inherit the Campaign Plan’s. Present (here or inherited) makes the budget a total for the period; absent makes it a daily rate.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"gender":{"type":"string","enum":["all","men","women"],"description":"Omit for undecided, which executes the same as \"all\"."},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"],"description":"Provider-neutral outcome, translated at execution. There is deliberately no optimization goal here."},"persona_id":{"description":"Must belong to the Campaign Plan’s Brand. It **explains** the targeting and never supplies its values — the countries and age range above are what execute.","type":"string","pattern":"^psn_[\\s\\S]{0,}$"},"start_date":{"description":"Omit to inherit the Campaign Plan’s.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["channel","objective"],"additionalProperties":false,"example":{"advertiser_id":"acc_1203456789012345","age_max":44,"age_min":25,"budget":2500,"channel":"meta","countries":["FR","BE"],"gender":"all","objective":"sales","persona_id":"psn_8c4a20"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"advertiser_id":{"description":"The ad account this line runs through. On the line rather than the Campaign Plan, because an account belongs to one provider and a Campaign Plan spans several channels.","type":"string","pattern":"^acc_[\\s\\S]{0,}$"},"age_max":{"type":"integer","minimum":0,"maximum":120},"age_min":{"type":"integer","minimum":0,"maximum":120},"budget":{"description":"Whole units of the **plan’s** currency: `40.5` is €40.50. Whether it reads as a total or a daily rate is derived from the effective end date, never stored.","type":"number","minimum":0},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"What is being bought, in a planner’s vocabulary — not a provider. Curated and deliberately incomplete."},"countries":{"description":"ISO 3166-1 alpha-2. Empty is a drafting state, not an error.","type":"array","items":{"type":"string","minLength":2,"maxLength":2}},"end_date":{"description":"Omit to inherit the Campaign Plan’s. Present (here or inherited) makes the budget a total for the period; absent makes it a daily rate.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"gender":{"type":"string","enum":["all","men","women"],"description":"Omit for undecided, which executes the same as \"all\"."},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"],"description":"Provider-neutral outcome, translated at execution. There is deliberately no optimization goal here."},"persona_id":{"description":"Must belong to the Campaign Plan’s Brand. It **explains** the targeting and never supplies its values — the countries and age range above are what execute.","type":"string","pattern":"^psn_[\\s\\S]{0,}$"},"start_date":{"description":"Omit to inherit the Campaign Plan’s.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["channel","objective"],"additionalProperties":false,"example":{"advertiser_id":"acc_1203456789012345","age_max":44,"age_min":25,"budget":2500,"channel":"meta","countries":["FR","BE"],"gender":"all","objective":"sales","persona_id":"psn_8c4a20"}}}}}}},"/context/campaign-plans/{slug}/line-items/{lineItemId}":{"patch":{"security":[{"bearerAuth":[]}],"description":"Updates a Line Item. Send the `base_revision` you read. Omit a field to keep it. Send `null` to clear it.\n\n**Any change puts a validated line back into draft.** Only that line changes. The Campaign Plan keeps its approval, because that approval speaks about the whole allocation. To open the gate again, validate the line again.\n\nA write that changes nothing keeps the validation, and it does not move the `revision`.\n\nThe provider objects that the line already created do not change. This call never alters a live campaign.","operationId":"updateLineItem","responses":{"200":{"content":{"application/json":{"example":{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":3000,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE","NL"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":3,"startDate":null,"status":"draft","unit":"total","updatedAt":1769644800000},"schema":{"type":"object","properties":{"advertiserId":{"nullable":true,"type":"string"},"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"budget":{"nullable":true,"description":"Whole units of the **plan’s** currency.","type":"number"},"campaignPlanId":{"type":"string"},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"The planner’s word for what is being bought, not a provider name. A plan may name a channel we cannot execute."},"countries":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"effectiveEndDate":{"nullable":true,"description":"The line’s own bound where it has one, else the plan’s.","type":"string"},"effectiveStartDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"executable":{"type":"boolean","description":"Whether this channel could be executed at all today. Derived from our capabilities, never a promise that it will be."},"executions":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"entityId":{"type":"string","description":"The provider’s own id, unprefixed, so it joins to `entities`."},"entityType":{"type":"string","description":"The provider’s word for the level — `campaign`, `adset`, `ad`."},"id":{"type":"string"},"mutationWorkflowId":{"nullable":true,"description":"The durable execution that performed the write, when known.","type":"string"},"provider":{"type":"string"}},"required":["advertiserId","createdAt","entityId","entityType","id","mutationWorkflowId","provider"],"additionalProperties":false},"description":"What this line has spawned. Never a claim that it is *finished* — nobody declared how many objects a line should produce."},"gender":{"nullable":true,"type":"string","enum":["all","men","women"]},"id":{"type":"string"},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"]},"personaId":{"nullable":true,"description":"Explains the targeting; never supplies its values.","type":"string"},"provider":{"nullable":true,"description":"Who sells this channel, or null when nobody we integrate with does.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","validated"],"description":"`validated` is the gate on execution."},"unit":{"type":"string","enum":["total","daily"],"description":"How `budget` reads, derived from the effective end date. Present → a total for the period; absent → a daily rate. A surface must print this beside the amount."},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["advertiserId","ageMax","ageMin","budget","campaignPlanId","channel","countries","createdAt","createdBy","effectiveEndDate","effectiveStartDate","endDate","executable","executions","gender","id","objective","personaId","provider","revision","startDate","status","unit","updatedAt"],"additionalProperties":false,"example":{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":3000,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE","NL"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":3,"startDate":null,"status":"draft","unit":"total","updatedAt":1769644800000}}}},"description":"The Line Item, bare rather than wrapped — unlike the create, which also reports the Campaign Plan status. Any change returns a `validated` line to `draft`. `executions` lists what the line created, the same as a read of its Campaign Plan."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such plan, or no Line Item with that id on it. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false}}},"description":"The Line Item moved since you read it. `error` is `revision_mismatch`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"Six codes share this status. `invalid_countries`, `invalid_age_range`, `invalid_amount`, `invalid_window`, `persona_not_in_brand` or `advertiser_not_owned`. Each one is checked against the row as it would stand after the change, not against what you sent."}},"summary":"Update a Line Item","tags":["Line Items"],"parameters":[{"name":"lineItemId","in":"path","required":true,"schema":{"type":"string","pattern":"^lni_[\\s\\S]{0,}$"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"advertiser_id":{"description":"null unlinks the ad account.","nullable":true,"type":"string","pattern":"^acc_[\\s\\S]{0,}$"},"age_max":{"nullable":true,"type":"integer","minimum":0,"maximum":120},"age_min":{"nullable":true,"type":"integer","minimum":0,"maximum":120},"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"budget":{"nullable":true,"type":"number","minimum":0,"description":"Whole units of the plan’s currency: `40.5` is €40.50. No more decimals than the currency has."},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"What is being bought, in a planner’s vocabulary — not a provider. Curated and deliberately incomplete."},"countries":{"description":"The array is the value — sending [] clears the geography.","type":"array","items":{"type":"string","minLength":2,"maxLength":2}},"end_date":{"description":"null makes the line inherit the Campaign Plan’s end — which can flip the budget between a total and a daily rate. That is allowed, and it reverts the line to draft.","nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"gender":{"nullable":true,"type":"string","enum":["all","men","women"],"description":"Omit for undecided, which executes the same as \"all\"."},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"],"description":"Provider-neutral outcome, translated at execution. There is deliberately no optimization goal here."},"persona_id":{"nullable":true,"type":"string","pattern":"^psn_[\\s\\S]{0,}$"},"start_date":{"nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":2,"budget":3000,"countries":["FR","BE","NL"]}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"advertiser_id":{"description":"null unlinks the ad account.","nullable":true,"type":"string","pattern":"^acc_[\\s\\S]{0,}$"},"age_max":{"nullable":true,"type":"integer","minimum":0,"maximum":120},"age_min":{"nullable":true,"type":"integer","minimum":0,"maximum":120},"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"budget":{"nullable":true,"type":"number","minimum":0,"description":"Whole units of the plan’s currency: `40.5` is €40.50. No more decimals than the currency has."},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"What is being bought, in a planner’s vocabulary — not a provider. Curated and deliberately incomplete."},"countries":{"description":"The array is the value — sending [] clears the geography.","type":"array","items":{"type":"string","minLength":2,"maxLength":2}},"end_date":{"description":"null makes the line inherit the Campaign Plan’s end — which can flip the budget between a total and a daily rate. That is allowed, and it reverts the line to draft.","nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"gender":{"nullable":true,"type":"string","enum":["all","men","women"],"description":"Omit for undecided, which executes the same as \"all\"."},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"],"description":"Provider-neutral outcome, translated at execution. There is deliberately no optimization goal here."},"persona_id":{"nullable":true,"type":"string","pattern":"^psn_[\\s\\S]{0,}$"},"start_date":{"nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":2,"budget":3000,"countries":["FR","BE","NL"]}}},"multipart/form-data":{"schema":{"type":"object","properties":{"advertiser_id":{"description":"null unlinks the ad account.","nullable":true,"type":"string","pattern":"^acc_[\\s\\S]{0,}$"},"age_max":{"nullable":true,"type":"integer","minimum":0,"maximum":120},"age_min":{"nullable":true,"type":"integer","minimum":0,"maximum":120},"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."},"budget":{"nullable":true,"type":"number","minimum":0,"description":"Whole units of the plan’s currency: `40.5` is €40.50. No more decimals than the currency has."},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"What is being bought, in a planner’s vocabulary — not a provider. Curated and deliberately incomplete."},"countries":{"description":"The array is the value — sending [] clears the geography.","type":"array","items":{"type":"string","minLength":2,"maxLength":2}},"end_date":{"description":"null makes the line inherit the Campaign Plan’s end — which can flip the budget between a total and a daily rate. That is allowed, and it reverts the line to draft.","nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"gender":{"nullable":true,"type":"string","enum":["all","men","women"],"description":"Omit for undecided, which executes the same as \"all\"."},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"],"description":"Provider-neutral outcome, translated at execution. There is deliberately no optimization goal here."},"persona_id":{"nullable":true,"type":"string","pattern":"^psn_[\\s\\S]{0,}$"},"start_date":{"nullable":true,"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":2,"budget":3000,"countries":["FR","BE","NL"]}}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a Line Item. Send the `base_revision` you read. If the Campaign Plan was approved, this call puts the Campaign Plan back into draft, because the allocation is no longer the one that a person approved.\n\nAdCrunch **keeps** the record of what the line already created. Those provider objects are live in an ad account, and this call does not remove them.","operationId":"deleteLineItem","responses":{"204":{"description":"Deleted. An approved plan returns to `draft`, and the Campaign Plan’s own revision moves — so re-read it before your next guarded write against it."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such plan, or no Line Item with that id on it. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false}}},"description":"The Line Item moved since you read it. `error` is `revision_mismatch`."}},"summary":"Delete a Line Item","tags":["Line Items"],"parameters":[{"name":"lineItemId","in":"path","required":true,"schema":{"type":"string","pattern":"^lni_[\\s\\S]{0,}$"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"base_revision","in":"query","required":true,"schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET — guards concurrent edits."}}]}},"/context/campaign-plans/{slug}/line-items/{lineItemId}/validate":{"post":{"security":[{"bearerAuth":[]}],"description":"Validates one Line Item. Use this call when you want to validate one row, and not to approve the whole plan.\n\nValidation is the gate for execution. It changes nothing on any provider. To execute the line, you also need the `mutation:write` permission.\n\nThis call checks nothing about the content of the line. An incomplete line validates, and execution makes its own checks. Read the readiness of the line to see them.\n\nA call on a line that is already validated succeeds and changes nothing. The Campaign Plan does not move: one validated line does not approve the Campaign Plan.","operationId":"validateLineItem","responses":{"200":{"content":{"application/json":{"example":{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":2,"startDate":null,"status":"validated","unit":"total","updatedAt":1769040000000},"schema":{"type":"object","properties":{"advertiserId":{"nullable":true,"type":"string"},"ageMax":{"nullable":true,"type":"number"},"ageMin":{"nullable":true,"description":"Each bound is independently optional, so \"35+\" is expressible.","type":"number"},"budget":{"nullable":true,"description":"Whole units of the **plan’s** currency.","type":"number"},"campaignPlanId":{"type":"string"},"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"],"description":"The planner’s word for what is being bought, not a provider name. A plan may name a channel we cannot execute."},"countries":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string"},"effectiveEndDate":{"nullable":true,"description":"The line’s own bound where it has one, else the plan’s.","type":"string"},"effectiveStartDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"executable":{"type":"boolean","description":"Whether this channel could be executed at all today. Derived from our capabilities, never a promise that it will be."},"executions":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"entityId":{"type":"string","description":"The provider’s own id, unprefixed, so it joins to `entities`."},"entityType":{"type":"string","description":"The provider’s word for the level — `campaign`, `adset`, `ad`."},"id":{"type":"string"},"mutationWorkflowId":{"nullable":true,"description":"The durable execution that performed the write, when known.","type":"string"},"provider":{"type":"string"}},"required":["advertiserId","createdAt","entityId","entityType","id","mutationWorkflowId","provider"],"additionalProperties":false},"description":"What this line has spawned. Never a claim that it is *finished* — nobody declared how many objects a line should produce."},"gender":{"nullable":true,"type":"string","enum":["all","men","women"]},"id":{"type":"string"},"objective":{"type":"string","enum":["awareness","engagement","leads","sales","traffic","app_promotion"]},"personaId":{"nullable":true,"description":"Explains the targeting; never supplies its values.","type":"string"},"provider":{"nullable":true,"description":"Who sells this channel, or null when nobody we integrate with does.","type":"string"},"revision":{"type":"number","description":"Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409."},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["draft","validated"],"description":"`validated` is the gate on execution."},"unit":{"type":"string","enum":["total","daily"],"description":"How `budget` reads, derived from the effective end date. Present → a total for the period; absent → a daily rate. A surface must print this beside the amount."},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["advertiserId","ageMax","ageMin","budget","campaignPlanId","channel","countries","createdAt","createdBy","effectiveEndDate","effectiveStartDate","endDate","executable","executions","gender","id","objective","personaId","provider","revision","startDate","status","unit","updatedAt"],"additionalProperties":false,"example":{"advertiserId":"acc_1203456789012345","ageMax":44,"ageMin":25,"budget":2500,"campaignPlanId":"cpl_4e1a08","channel":"meta","countries":["FR","BE"],"createdAt":1768435200000,"createdBy":"usr_5d2b71","effectiveEndDate":"2026-05-31","effectiveStartDate":"2026-03-01","endDate":null,"executable":true,"executions":[],"gender":"all","id":"lni_2d9e77","objective":"sales","personaId":"psn_8c4a20","provider":"meta","revision":2,"startDate":null,"status":"validated","unit":"total","updatedAt":1769040000000}}}},"description":"The Line Item, now `validated`. Validating an already-validated line is a no-op rather than an error, and the `revision` stays where it was. `executions` lists what the line created, the same as a read of its Campaign Plan."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such plan, or no Line Item with that id on it. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"currentRevision":{"type":"number","description":"The revision the row holds now. Re-read, re-apply your edit on top, and send this back as base_revision."},"error":{"type":"string","enum":["revision_mismatch"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["currentRevision","error","message"],"additionalProperties":false}}},"description":"The Line Item moved since you read it. `error` is `revision_mismatch`. This is what makes it a validation **of the content you read**."}},"summary":"Validate a Line Item","tags":["Line Items"],"parameters":[{"name":"lineItemId","in":"path","required":true,"schema":{"type":"string","pattern":"^lni_[\\s\\S]{0,}$"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET. This is what makes it a validation **of the content that was read**."}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":1}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET. This is what makes it a validation **of the content that was read**."}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":1}}},"multipart/form-data":{"schema":{"type":"object","properties":{"base_revision":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The revision last read via GET. This is what makes it a validation **of the content that was read**."}},"required":["base_revision"],"additionalProperties":false,"example":{"base_revision":1}}}}}}},"/context/campaign-plans/{slug}/line-items/{lineItemId}/readiness":{"get":{"security":[{"bearerAuth":[]}],"description":"Tells you whether AdCrunch can execute a Line Item now, and if it cannot, why. Ask this **before** any provider call.\n\nThis call reads. It changes nothing, and it calls no provider. It answers for every channel, including a channel AdCrunch cannot write to.\n\nA line is not ready in these conditions:\n\n- Nobody validated the line.\n- AdCrunch cannot write to the channel. The answer names the provider that sells it, or `null` when nobody we integrate with sells it.\n- The line has no ad account.\n- The ad account is not connected to this organization.\n- The ad account belongs to a different provider than the channel.\n- The currency of the ad account is different from the Campaign Plan currency, or AdCrunch does not know that currency yet. To make it known, refresh the provider connection.\n- The line and the Campaign Plan give no date at all.\n- The resolved start follows the resolved end. Each row is ordered on its own, and only the pair shows this.\n\nAdCrunch never converts a currency. A converted budget is a different plan.\n\nWhen the line is `ready`, the answer also gives the resolved window, and it tells you whether the budget goes to the provider as a lifetime total or as a daily rate.","operationId":"getLineItemExecutionReadiness","responses":{"200":{"content":{"application/json":{"example":{"advertiserId":"acc_1203456789012345","budget":2500,"currency":"EUR","endDate":"2026-05-31","provider":"meta","startDate":"2026-03-01","status":"ready","unit":"total"},"schema":{"oneOf":[{"type":"object","properties":{"advertiserId":{"type":"string"},"budget":{"nullable":true,"type":"number"},"currency":{"type":"string","description":"The plan’s currency, confirmed equal to the account’s."},"endDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"provider":{"type":"string"},"startDate":{"nullable":true,"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["ready"]},"unit":{"type":"string","enum":["total","daily"]}},"required":["advertiserId","budget","currency","endDate","provider","startDate","status","unit"],"additionalProperties":false},{"type":"object","properties":{"status":{"type":"string","enum":["not_validated"],"description":"The gate. Approving the plan, or validating the line, opens it."}},"required":["status"],"additionalProperties":false},{"type":"object","properties":{"channel":{"type":"string","enum":["meta","tiktok","snapchat","google_search","google_pmax","google_display","youtube","programmatic_display","linkedin","x"]},"provider":{"nullable":true,"description":"Who sells it, when someone does but we cannot write there yet.","type":"string"},"status":{"type":"string","enum":["channel_not_executable"]}},"required":["channel","provider","status"],"additionalProperties":false},{"type":"object","properties":{"status":{"type":"string","enum":["no_advertiser"]}},"required":["status"],"additionalProperties":false},{"type":"object","properties":{"status":{"type":"string","enum":["advertiser_not_owned"]}},"required":["status"],"additionalProperties":false},{"type":"object","properties":{"actual":{"type":"string"},"expected":{"type":"string"},"status":{"type":"string","enum":["provider_mismatch"]}},"required":["actual","expected","status"],"additionalProperties":false},{"type":"object","properties":{"advertiserId":{"type":"string"},"status":{"type":"string","enum":["account_currency_unknown"],"description":"Refresh the provider connection."}},"required":["advertiserId","status"],"additionalProperties":false},{"type":"object","properties":{"accountCurrency":{"type":"string"},"planCurrency":{"type":"string"},"status":{"type":"string","enum":["currency_mismatch"],"description":"Never converted. A converted budget is a different plan, and the planner allocated what they allocated."}},"required":["accountCurrency","planCurrency","status"],"additionalProperties":false},{"type":"object","properties":{"status":{"type":"string","enum":["no_window"]}},"required":["status"],"additionalProperties":false},{"type":"object","properties":{"endDate":{"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"startDate":{"type":"string","description":"A calendar date, `YYYY-MM-DD`."},"status":{"type":"string","enum":["window_inverted"]}},"required":["endDate","startDate","status"],"additionalProperties":false}],"description":"Asked before any provider call. This never mutates.","example":{"advertiserId":"acc_1203456789012345","budget":2500,"currency":"EUR","endDate":"2026-05-31","provider":"meta","startDate":"2026-03-01","status":"ready","unit":"total"}}}},"description":"The verdict, discriminated on `status`. **Every outcome is a 200**, `ready` included: \"this cannot run, and here is why\" is an answer rather than a failed request. The example shows `ready`; the schema lists the nine refusals and what each one carries."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such plan, or no Line Item with that id on it. `error` is `not_found`. This is the one readiness outcome that is not a 200, because the question itself has no subject."}},"summary":"Check a Line Item’s execution readiness","tags":["Line Items"],"parameters":[{"name":"lineItemId","in":"path","required":true,"schema":{"type":"string","pattern":"^lni_[\\s\\S]{0,}$"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}]}},"/context/campaign-plans/{slug}/line-items/{lineItemId}/executions":{"post":{"security":[{"bearerAuth":[]}],"description":"Records one provider object that this Line Item created. The caller that created the object writes this record, because only that caller knows which line the object belongs to.\n\nA record is a **fact**, not an attempt. AdCrunch keeps the attempts in the mutation ledger.\n\nThis call refuses almost nothing. The provider object already exists, and a refusal would leave it live and untracked. A draft line records, an edited line records, and a line on a channel AdCrunch cannot write to records. The state of the line does not change, and neither does the state of the Campaign Plan.\n\nThe same object recorded again answers `200` with the existing row, and not `201`. The same object is the same provider, the same level and the same native id, on the same line.\n\nAdCrunch never removes a record. It can tell you how many objects a line created. It cannot tell you how many objects a line must create, because a Campaign Plan never declares that number.","operationId":"recordLineItemExecution","responses":{"200":{"content":{"application/json":{"example":{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"},"schema":{"type":"object","properties":{"advertiserId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"entityId":{"type":"string","description":"The provider’s own id, unprefixed, so it joins to `entities`."},"entityType":{"type":"string","description":"The provider’s word for the level — `campaign`, `adset`, `ad`."},"id":{"type":"string"},"mutationWorkflowId":{"nullable":true,"description":"The durable execution that performed the write, when known.","type":"string"},"provider":{"type":"string"}},"required":["advertiserId","createdAt","entityId","entityType","id","mutationWorkflowId","provider"],"additionalProperties":false,"example":{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}}}},"description":"This object was already recorded against this line. The existing row comes back unchanged, so the call is idempotent — recording twice does not inflate what the line has spawned."},"201":{"content":{"application/json":{"example":{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"},"schema":{"type":"object","properties":{"advertiserId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"entityId":{"type":"string","description":"The provider’s own id, unprefixed, so it joins to `entities`."},"entityType":{"type":"string","description":"The provider’s word for the level — `campaign`, `adset`, `ad`."},"id":{"type":"string"},"mutationWorkflowId":{"nullable":true,"description":"The durable execution that performed the write, when known.","type":"string"},"provider":{"type":"string"}},"required":["advertiserId","createdAt","entityId","entityType","id","mutationWorkflowId","provider"],"additionalProperties":false,"example":{"advertiserId":"acc_1203456789012345","createdAt":1772409600000,"entityId":"120210000000000","entityType":"campaign","id":"exe_7a4c12","mutationWorkflowId":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}}}},"description":"Recorded. Neither response shape was observed against the deployed service, because the verification organization has no connected advertiser — both are written from the Context Layer’s view type."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `campaign_plan:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No such plan, or no Line Item with that id on it. `error` is `not_found`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"That ad account is not connected to this organization. `error` is `advertiser_not_owned`. Almost nothing else is refused here, deliberately: the provider object already exists, and refusing the record would leave it live and untracked."}},"summary":"Record what a Line Item spawned","tags":["Line Items"],"parameters":[{"name":"lineItemId","in":"path","required":true,"schema":{"type":"string","pattern":"^lni_[\\s\\S]{0,}$"}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"advertiser_id":{"type":"string","pattern":"^acc_[\\s\\S]{0,}$","description":"The ad account the object was created in."},"entity_id":{"type":"string","minLength":1,"description":"The **native** provider id, unprefixed — exactly as the Insight entities table holds it, which is what makes the two joinable."},"entity_type":{"type":"string","minLength":1,"description":"The provider’s own word for the level: campaign, adset, ad. Free text, because the vocabulary is the provider’s."},"mutation_workflow_id":{"description":"The mutation that performed the write, when known — links this fact to the attempt without either duplicating the other.","type":"string","minLength":1},"provider":{"anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]},{"type":"string","enum":["x"]},{"type":"string","enum":["openai"]}]}},"required":["advertiser_id","entity_id","entity_type","provider"],"additionalProperties":false,"example":{"advertiser_id":"acc_1203456789012345","entity_id":"23861234567890123","entity_type":"campaign","mutation_workflow_id":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"advertiser_id":{"type":"string","pattern":"^acc_[\\s\\S]{0,}$","description":"The ad account the object was created in."},"entity_id":{"type":"string","minLength":1,"description":"The **native** provider id, unprefixed — exactly as the Insight entities table holds it, which is what makes the two joinable."},"entity_type":{"type":"string","minLength":1,"description":"The provider’s own word for the level: campaign, adset, ad. Free text, because the vocabulary is the provider’s."},"mutation_workflow_id":{"description":"The mutation that performed the write, when known — links this fact to the attempt without either duplicating the other.","type":"string","minLength":1},"provider":{"anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]},{"type":"string","enum":["x"]},{"type":"string","enum":["openai"]}]}},"required":["advertiser_id","entity_id","entity_type","provider"],"additionalProperties":false,"example":{"advertiser_id":"acc_1203456789012345","entity_id":"23861234567890123","entity_type":"campaign","mutation_workflow_id":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"advertiser_id":{"type":"string","pattern":"^acc_[\\s\\S]{0,}$","description":"The ad account the object was created in."},"entity_id":{"type":"string","minLength":1,"description":"The **native** provider id, unprefixed — exactly as the Insight entities table holds it, which is what makes the two joinable."},"entity_type":{"type":"string","minLength":1,"description":"The provider’s own word for the level: campaign, adset, ad. Free text, because the vocabulary is the provider’s."},"mutation_workflow_id":{"description":"The mutation that performed the write, when known — links this fact to the attempt without either duplicating the other.","type":"string","minLength":1},"provider":{"anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]},{"type":"string","enum":["x"]},{"type":"string","enum":["openai"]}]}},"required":["advertiser_id","entity_id","entity_type","provider"],"additionalProperties":false,"example":{"advertiser_id":"acc_1203456789012345","entity_id":"23861234567890123","entity_type":"campaign","mutation_workflow_id":"b3d1f0c4-6a2e-4a1f-9f77-2c0d1e5a8b94","provider":"meta"}}}}}}},"/assets":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the active organization's Assets, newest first. Each row carries its Registrations, so one read answers where every file is and what still needs registering. A reservation is excluded until finalize promotes it to `ready`, and so is a deleted Asset.\n\nOne page at a time: `limit` rows at most, 100 by default. When the answer holds `nextCursor`, more rows exist: send it back as `cursor` to get the next page. There is no filter.","operationId":"listAssets","responses":{"200":{"content":{"application/json":{"example":{"data":[{"contentHash":"9d2f41b7c6e8a35014f7b2d9c3e6a810","createdAt":1790000000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":15000,"filename":"spring-sale.mp4","height":1920,"id":"ast_7c1e9a","key":"org_3a7f10/ast_7c1e9a","kind":"video","mimeType":"video/mp4","name":"Spring sale 9:16","organizationId":"org_3a7f10","sizeBytes":24117248,"status":"ready","updatedAt":1790000120000,"width":1080,"registrations":[{"advertiserId":"acc_1203456789012345","assetId":"ast_7c1e9a","createdAt":1790000121000,"deletedAt":null,"failureCode":null,"failureReason":null,"id":"reg_4b81d2","organizationId":"org_3a7f10","provider":"meta","providerIdentifier":"23843178520310445","status":"ready","updatedAt":1790000249000,"uploadSessionId":"1264871528937461","workflowId":"reg_4b81d2"}]},{"contentHash":"4a7e1c9b0d3f26854b1e7a9c2d6f3081","createdAt":1789900000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":null,"filename":"spring-sale-square.jpg","height":1080,"id":"ast_2d4f83","key":"org_3a7f10/ast_2d4f83","kind":"image","mimeType":"image/jpeg","name":null,"organizationId":"org_3a7f10","sizeBytes":486112,"status":"ready","updatedAt":1789900090000,"width":1080,"registrations":[]}]},"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"contentHash":{"type":"string","description":"AdCrunch’s own hash of the stored bytes. A hint for de-duplication, never an identity, and never a provider’s hash. Empty when the store reports none."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string","description":"The user who reserved the upload."},"deletedAt":{"nullable":true,"description":"Always `null` here. A deleted Asset is not answered.","type":"number"},"durationMs":{"nullable":true,"description":"How long the video runs, in milliseconds, read from the header of the file at finalize. `null` for an image. For a video, `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"filename":{"type":"string","description":"The name the file was uploaded under. Never a lookup key."},"height":{"nullable":true,"description":"The height in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"id":{"type":"string","description":"The id of the Asset, `ast_…`."},"key":{"type":"string","description":"Where the bytes are held. Read-only, and never sent by you."},"kind":{"type":"string","enum":["image","video"],"description":"Follows from `mimeType`, so the stored bytes verify it at finalize. It selects how a provider takes the file."},"mimeType":{"type":"string","description":"The `Content-Type` the file was uploaded with, verified against the stored bytes at finalize."},"name":{"nullable":true,"description":"The name a person gave this Asset, and what a provider receives at a future registration. `null` until somebody renames it, and the filename is the fallback.","type":"string"},"organizationId":{"type":"string","description":"The organization that owns the Asset."},"sizeBytes":{"type":"number","description":"Read from the stored object at finalize."},"status":{"type":"string","enum":["pending","ready"],"description":"`ready` once the bytes have been inspected. A `pending` Asset is a reservation whose bytes have not arrived, and no operation here answers one."},"updatedAt":{"nullable":true,"description":"When the Asset last changed, in milliseconds since the Unix epoch, UTC: at finalize, or at the last rename.","type":"number"},"width":{"nullable":true,"description":"The width in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"registrations":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser whose provider library gets the Asset."},"assetId":{"type":"string","description":"The Asset that the Registration places."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"deletedAt":{"nullable":true,"description":"Always `null` here. When a retry replaces a failed Registration, AdCrunch sets this time on the old one and answers it no more.","type":"number"},"failureCode":{"nullable":true,"description":"Why AdCrunch refused the Registration before any provider call. Branch on this field, never on `failureReason`. `not_found`: your organization owns no advertiser of this provider with this id. `provider_not_connected`: the advertiser has no usable connection to the provider. Reconnect the provider, then register again. `missing_write_access`: the connection does not permit this kind of file. Reconnect the provider to grant it. `null` when the Registration did not fail, or when it failed at the provider.","type":"string","enum":["not_found","provider_not_connected","missing_write_access"]},"failureReason":{"nullable":true,"description":"Why the Registration failed, as a sentence: the words of the provider, or the words of AdCrunch. `null` unless `status` is `failed`.","type":"string"},"id":{"type":"string","description":"The id of the Registration, `reg_…`."},"organizationId":{"type":"string","description":"The organization that owns the Asset."},"provider":{"type":"string","enum":["meta","gads","tiktok","x","openai","snapchat","dv360"],"description":"The provider that owns the advertiser."},"providerIdentifier":{"nullable":true,"description":"The id that the provider gave the file: a Meta image hash, or the id of the image or the video at the provider. `null` until the provider gives it, and `null` on a `failed` Registration. An image gets its id when the Registration becomes `ready`. A video gets its id earlier, while the Registration is `running`, because the provider gives the id before it has processed the file. Build a creative from a `ready` Registration only.","type":"string"},"status":{"type":"string","enum":["running","ready","failed"],"description":"Video can stay `running` for several minutes while the provider processes it. That is normal, and not a failure."},"updatedAt":{"nullable":true,"description":"When the Registration last changed, in milliseconds since the Unix epoch, UTC. `null` until its first change.","type":"number"},"uploadSessionId":{"nullable":true,"description":"An in-flight provider upload session, where the protocol has one. `null` for every image.","type":"string"},"workflowId":{"type":"string","description":"The durable execution that runs the Registration."}},"required":["advertiserId","assetId","createdAt","deletedAt","failureCode","failureReason","id","organizationId","provider","providerIdentifier","status","updatedAt","uploadSessionId","workflowId"],"additionalProperties":false},"description":"Each Registration of this Asset, one for each advertiser, with its state. An empty array means that the Asset has no Registration."}},"required":["contentHash","createdAt","createdBy","deletedAt","durationMs","filename","height","id","key","kind","mimeType","name","organizationId","sizeBytes","status","updatedAt","width","registrations"],"additionalProperties":false}},"nextCursor":{"description":"Send this value as `cursor` to get the next page. It is absent on the last page.","type":"string"}},"required":["data"],"additionalProperties":false,"example":{"data":[{"contentHash":"9d2f41b7c6e8a35014f7b2d9c3e6a810","createdAt":1790000000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":15000,"filename":"spring-sale.mp4","height":1920,"id":"ast_7c1e9a","key":"org_3a7f10/ast_7c1e9a","kind":"video","mimeType":"video/mp4","name":"Spring sale 9:16","organizationId":"org_3a7f10","sizeBytes":24117248,"status":"ready","updatedAt":1790000120000,"width":1080,"registrations":[{"advertiserId":"acc_1203456789012345","assetId":"ast_7c1e9a","createdAt":1790000121000,"deletedAt":null,"failureCode":null,"failureReason":null,"id":"reg_4b81d2","organizationId":"org_3a7f10","provider":"meta","providerIdentifier":"23843178520310445","status":"ready","updatedAt":1790000249000,"uploadSessionId":"1264871528937461","workflowId":"reg_4b81d2"}]},{"contentHash":"4a7e1c9b0d3f26854b1e7a9c2d6f3081","createdAt":1789900000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":null,"filename":"spring-sale-square.jpg","height":1080,"id":"ast_2d4f83","key":"org_3a7f10/ast_2d4f83","kind":"image","mimeType":"image/jpeg","name":null,"organizationId":"org_3a7f10","sizeBytes":486112,"status":"ready","updatedAt":1789900090000,"width":1080,"registrations":[]}]}}}},"description":"One page of the stored Assets of the organization, newest first, each with its Registrations. An Asset registered nowhere carries an empty array. This page is the last one, so it carries no `nextCursor`."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"string","enum":["invalid_cursor"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}]}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `asset:read`. `error` is `forbidden`."}},"summary":"List the organization Assets","tags":["Assets"],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"description":"The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.","type":"string","minLength":1}},{"name":"limit","in":"query","required":true,"schema":{"default":100,"description":"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`.","type":"integer","minimum":1,"maximum":500}}]}},"/assets/uploads":{"post":{"security":[{"bearerAuth":[]}],"description":"Reserves an Asset and returns a presigned `PUT` URL to upload the bytes to. The URL is valid for one hour, and `expiresAt` says when it stops being accepted.\n\nThe client uploads **directly** to storage — the bytes never pass through this worker, because Cloudflare rejects over-limit request bodies at the proxy before any code runs. Call `POST /assets/{id}/finalize` once the upload completes.\n\nThe URL is the credential: anyone holding it can write that one key until it expires. Nothing is known about the file until finalize inspects what actually landed, so size and type cannot be enforced here.\n\nThe reserved Asset stays `pending` until finalize. No read, rename or list answers a `pending` Asset, and a reservation nobody finalizes is swept a day later, together with whatever landed under it.","operationId":"createAssetUpload","responses":{"200":{"content":{"application/json":{"example":{"assetId":"ast_7c1e9a","expiresAt":1790003600000,"uploadUrl":"https://b7f3c81a29d64e0fa5c3d1e8b0947265.r2.cloudflarestorage.com/assets/org_3a7f10/ast_7c1e9a?X-Amz-Expires=3600&X-Amz-Date=20260921T141320Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=4f8e2a6c1b9d7e3f5a0c2b4d6e8f1a3c%2F20260921%2Fauto%2Fs3%2Faws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=7d1c4a9f2e8b0536c1a7e94d2b6f038a5c1e9b7d3f6a2c805d4e1b93a7f0c625"},"schema":{"type":"object","properties":{"assetId":{"type":"string","description":"The Asset this reservation made. Finalize it with this id."},"expiresAt":{"type":"number","description":"When the upload URL stops being accepted, in milliseconds since the Unix epoch, UTC."},"uploadUrl":{"type":"string","description":"PUT the bytes here, directly. The URL is the credential: anybody holding it can write that one object until it expires."}},"required":["assetId","expiresAt","uploadUrl"],"additionalProperties":false,"example":{"assetId":"ast_7c1e9a","expiresAt":1790003600000,"uploadUrl":"https://b7f3c81a29d64e0fa5c3d1e8b0947265.r2.cloudflarestorage.com/assets/org_3a7f10/ast_7c1e9a?X-Amz-Expires=3600&X-Amz-Date=20260921T141320Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=4f8e2a6c1b9d7e3f5a0c2b4d6e8f1a3c%2F20260921%2Fauto%2Fs3%2Faws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=7d1c4a9f2e8b0536c1a7e94d2b6f038a5c1e9b7d3f6a2c805d4e1b93a7f0c625"}}}},"description":"The reservation. Send the bytes to `uploadUrl`, then finalize `assetId`."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `asset:write`. `error` is `forbidden`."},"500":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"AdCrunch could not write the reservation. `error` is `reservation_failed`. Nothing was reserved, so send the request again."}},"summary":"Reserve an Asset and mint an upload URL","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255,"description":"The name of the file you are about to send. Shown until somebody renames the Asset, and never used to look one up."}},"required":["filename"],"additionalProperties":false,"example":{"filename":"spring-sale.mp4"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255,"description":"The name of the file you are about to send. Shown until somebody renames the Asset, and never used to look one up."}},"required":["filename"],"additionalProperties":false,"example":{"filename":"spring-sale.mp4"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255,"description":"The name of the file you are about to send. Shown until somebody renames the Asset, and never used to look one up."}},"required":["filename"],"additionalProperties":false,"example":{"filename":"spring-sale.mp4"}}}}}}},"/assets/{id}/finalize":{"post":{"security":[{"bearerAuth":[]}],"description":"Completes a reserved Asset from the object that actually landed, and promotes it to `ready`.\n\nThis is the only real validation point: a presigned `PUT` carries no policy document, so nothing could have stopped the client sending a different type — or far more bytes — than it declared. A refused upload is deleted rather than left to bill as an orphan.\n\nPass `advertiserId` to place the file in the same call. That starts the same Registration as `POST /assets/{id}/registrations`, and it starts one only — the Registration settles later. **The answer is the Asset either way.** A registration that could not start does not fail this call, because the file is stored and only the registration needs retrying — read `GET /assets/{id}` for the Registration and its state.","operationId":"finalizeAsset","responses":{"200":{"content":{"application/json":{"example":{"contentHash":"9d2f41b7c6e8a35014f7b2d9c3e6a810","createdAt":1790000000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":15000,"filename":"spring-sale.mp4","height":1920,"id":"ast_7c1e9a","key":"org_3a7f10/ast_7c1e9a","kind":"video","mimeType":"video/mp4","name":null,"organizationId":"org_3a7f10","sizeBytes":24117248,"status":"ready","updatedAt":1790000120000,"width":1080},"schema":{"type":"object","properties":{"contentHash":{"type":"string","description":"AdCrunch’s own hash of the stored bytes. A hint for de-duplication, never an identity, and never a provider’s hash. Empty when the store reports none."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string","description":"The user who reserved the upload."},"deletedAt":{"nullable":true,"description":"Always `null` here. A deleted Asset is not answered.","type":"number"},"durationMs":{"nullable":true,"description":"How long the video runs, in milliseconds, read from the header of the file at finalize. `null` for an image. For a video, `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"filename":{"type":"string","description":"The name the file was uploaded under. Never a lookup key."},"height":{"nullable":true,"description":"The height in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"id":{"type":"string","description":"The id of the Asset, `ast_…`."},"key":{"type":"string","description":"Where the bytes are held. Read-only, and never sent by you."},"kind":{"type":"string","enum":["image","video"],"description":"Follows from `mimeType`, so the stored bytes verify it at finalize. It selects how a provider takes the file."},"mimeType":{"type":"string","description":"The `Content-Type` the file was uploaded with, verified against the stored bytes at finalize."},"name":{"nullable":true,"description":"The name a person gave this Asset, and what a provider receives at a future registration. `null` until somebody renames it, and the filename is the fallback.","type":"string"},"organizationId":{"type":"string","description":"The organization that owns the Asset."},"sizeBytes":{"type":"number","description":"Read from the stored object at finalize."},"status":{"type":"string","enum":["pending","ready"],"description":"`ready` once the bytes have been inspected. A `pending` Asset is a reservation whose bytes have not arrived, and no operation here answers one."},"updatedAt":{"nullable":true,"description":"When the Asset last changed, in milliseconds since the Unix epoch, UTC: at finalize, or at the last rename.","type":"number"},"width":{"nullable":true,"description":"The width in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"}},"required":["contentHash","createdAt","createdBy","deletedAt","durationMs","filename","height","id","key","kind","mimeType","name","organizationId","sizeBytes","status","updatedAt","width"],"additionalProperties":false,"example":{"contentHash":"9d2f41b7c6e8a35014f7b2d9c3e6a810","createdAt":1790000000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":15000,"filename":"spring-sale.mp4","height":1920,"id":"ast_7c1e9a","key":"org_3a7f10/ast_7c1e9a","kind":"video","mimeType":"video/mp4","name":null,"organizationId":"org_3a7f10","sizeBytes":24117248,"status":"ready","updatedAt":1790000120000,"width":1080}}}},"description":"The stored Asset, now `ready`. `sizeBytes` is read off the stored object. `mimeType` and `kind` are the declared type, verified against the stored bytes. `width`, `height` and `durationMs` are read from the header of the file. It carries no Registration, whether or not you asked for one."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `asset:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Asset with that id in this organization. A deleted one answers this too. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"The reservation exists and no bytes have arrived. `error` is `not_uploaded`."},"413":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"The **stored** file is over the limit for its kind, whatever was declared at reservation. `error` is `too_large`, and the object has been discarded."},"415":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"AdCrunch refused the **stored** file and deleted it. `error` names why:\n\n- `unsupported_type`: the file was uploaded with a `Content-Type` that AdCrunch does not accept.\n- `type_mismatch`: the bytes are an accepted type, but not the `Content-Type` the file was uploaded with. PUT it again with its real type.\n- `unreadable`: the bytes are no accepted type, or the file ends before its header does."}},"summary":"Finalize an uploaded Asset","tags":["Assets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^(ast_)[\\s\\S]{0,}$","description":"The Asset the reservation made."}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"example":{"advertiserId":"acc_1203456789012345"},"type":"object","properties":{"advertiserId":{"description":"Place the file in this advertiser’s library in the same call. Optional — send no body to store the file alone.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"example":{"advertiserId":"acc_1203456789012345"},"type":"object","properties":{"advertiserId":{"description":"Place the file in this advertiser’s library in the same call. Optional — send no body to store the file alone.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},"additionalProperties":false}},"multipart/form-data":{"schema":{"example":{"advertiserId":"acc_1203456789012345"},"type":"object","properties":{"advertiserId":{"description":"Place the file in this advertiser’s library in the same call. Optional — send no body to store the file alone.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},"additionalProperties":false}}}}}},"/assets/{id}":{"get":{"security":[{"bearerAuth":[]}],"description":"Fetches one Asset with its Registrations and a short-lived presigned `previewUrl` for the stored bytes. An Asset belonging to another organization is reported as missing, so the surface cannot be used to discover that an id is real.\n\nThis is also how a Registration is polled: `status` and `providerIdentifier` on each Registration settle here, and there is no separate status resource.","operationId":"getAsset","responses":{"200":{"content":{"application/json":{"example":{"contentHash":"9d2f41b7c6e8a35014f7b2d9c3e6a810","createdAt":1790000000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":15000,"filename":"spring-sale.mp4","height":1920,"id":"ast_7c1e9a","key":"org_3a7f10/ast_7c1e9a","kind":"video","mimeType":"video/mp4","name":"Spring sale 9:16","organizationId":"org_3a7f10","sizeBytes":24117248,"status":"ready","updatedAt":1790000120000,"width":1080,"registrations":[{"advertiserId":"acc_1203456789012345","assetId":"ast_7c1e9a","createdAt":1790000121000,"deletedAt":null,"failureCode":null,"failureReason":null,"id":"reg_4b81d2","organizationId":"org_3a7f10","provider":"meta","providerIdentifier":"23843178520310445","status":"ready","updatedAt":1790000249000,"uploadSessionId":"1264871528937461","workflowId":"reg_4b81d2"}],"previewUrl":"https://b7f3c81a29d64e0fa5c3d1e8b0947265.r2.cloudflarestorage.com/assets/org_3a7f10/ast_7c1e9a?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260922T104000Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=2f9a7c1e4b8d6035a1c7e2b94f0d38615c9a7e3d2b4f81c60d5a3e9b7f1c4028"},"schema":{"type":"object","properties":{"contentHash":{"type":"string","description":"AdCrunch’s own hash of the stored bytes. A hint for de-duplication, never an identity, and never a provider’s hash. Empty when the store reports none."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string","description":"The user who reserved the upload."},"deletedAt":{"nullable":true,"description":"Always `null` here. A deleted Asset is not answered.","type":"number"},"durationMs":{"nullable":true,"description":"How long the video runs, in milliseconds, read from the header of the file at finalize. `null` for an image. For a video, `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"filename":{"type":"string","description":"The name the file was uploaded under. Never a lookup key."},"height":{"nullable":true,"description":"The height in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"id":{"type":"string","description":"The id of the Asset, `ast_…`."},"key":{"type":"string","description":"Where the bytes are held. Read-only, and never sent by you."},"kind":{"type":"string","enum":["image","video"],"description":"Follows from `mimeType`, so the stored bytes verify it at finalize. It selects how a provider takes the file."},"mimeType":{"type":"string","description":"The `Content-Type` the file was uploaded with, verified against the stored bytes at finalize."},"name":{"nullable":true,"description":"The name a person gave this Asset, and what a provider receives at a future registration. `null` until somebody renames it, and the filename is the fallback.","type":"string"},"organizationId":{"type":"string","description":"The organization that owns the Asset."},"sizeBytes":{"type":"number","description":"Read from the stored object at finalize."},"status":{"type":"string","enum":["pending","ready"],"description":"`ready` once the bytes have been inspected. A `pending` Asset is a reservation whose bytes have not arrived, and no operation here answers one."},"updatedAt":{"nullable":true,"description":"When the Asset last changed, in milliseconds since the Unix epoch, UTC: at finalize, or at the last rename.","type":"number"},"width":{"nullable":true,"description":"The width in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"registrations":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser whose provider library gets the Asset."},"assetId":{"type":"string","description":"The Asset that the Registration places."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"deletedAt":{"nullable":true,"description":"Always `null` here. When a retry replaces a failed Registration, AdCrunch sets this time on the old one and answers it no more.","type":"number"},"failureCode":{"nullable":true,"description":"Why AdCrunch refused the Registration before any provider call. Branch on this field, never on `failureReason`. `not_found`: your organization owns no advertiser of this provider with this id. `provider_not_connected`: the advertiser has no usable connection to the provider. Reconnect the provider, then register again. `missing_write_access`: the connection does not permit this kind of file. Reconnect the provider to grant it. `null` when the Registration did not fail, or when it failed at the provider.","type":"string","enum":["not_found","provider_not_connected","missing_write_access"]},"failureReason":{"nullable":true,"description":"Why the Registration failed, as a sentence: the words of the provider, or the words of AdCrunch. `null` unless `status` is `failed`.","type":"string"},"id":{"type":"string","description":"The id of the Registration, `reg_…`."},"organizationId":{"type":"string","description":"The organization that owns the Asset."},"provider":{"type":"string","enum":["meta","gads","tiktok","x","openai","snapchat","dv360"],"description":"The provider that owns the advertiser."},"providerIdentifier":{"nullable":true,"description":"The id that the provider gave the file: a Meta image hash, or the id of the image or the video at the provider. `null` until the provider gives it, and `null` on a `failed` Registration. An image gets its id when the Registration becomes `ready`. A video gets its id earlier, while the Registration is `running`, because the provider gives the id before it has processed the file. Build a creative from a `ready` Registration only.","type":"string"},"status":{"type":"string","enum":["running","ready","failed"],"description":"Video can stay `running` for several minutes while the provider processes it. That is normal, and not a failure."},"updatedAt":{"nullable":true,"description":"When the Registration last changed, in milliseconds since the Unix epoch, UTC. `null` until its first change.","type":"number"},"uploadSessionId":{"nullable":true,"description":"An in-flight provider upload session, where the protocol has one. `null` for every image.","type":"string"},"workflowId":{"type":"string","description":"The durable execution that runs the Registration."}},"required":["advertiserId","assetId","createdAt","deletedAt","failureCode","failureReason","id","organizationId","provider","providerIdentifier","status","updatedAt","uploadSessionId","workflowId"],"additionalProperties":false},"description":"Each Registration of this Asset, one for each advertiser, with its state. An empty array means that the Asset has no Registration."},"previewUrl":{"type":"string","description":"A signed, expiring link to the stored bytes. Minted for this answer, and valid for 15 minutes. Do not store it."}},"required":["contentHash","createdAt","createdBy","deletedAt","durationMs","filename","height","id","key","kind","mimeType","name","organizationId","sizeBytes","status","updatedAt","width","registrations","previewUrl"],"additionalProperties":false,"example":{"contentHash":"9d2f41b7c6e8a35014f7b2d9c3e6a810","createdAt":1790000000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":15000,"filename":"spring-sale.mp4","height":1920,"id":"ast_7c1e9a","key":"org_3a7f10/ast_7c1e9a","kind":"video","mimeType":"video/mp4","name":"Spring sale 9:16","organizationId":"org_3a7f10","sizeBytes":24117248,"status":"ready","updatedAt":1790000120000,"width":1080,"registrations":[{"advertiserId":"acc_1203456789012345","assetId":"ast_7c1e9a","createdAt":1790000121000,"deletedAt":null,"failureCode":null,"failureReason":null,"id":"reg_4b81d2","organizationId":"org_3a7f10","provider":"meta","providerIdentifier":"23843178520310445","status":"ready","updatedAt":1790000249000,"uploadSessionId":"1264871528937461","workflowId":"reg_4b81d2"}],"previewUrl":"https://b7f3c81a29d64e0fa5c3d1e8b0947265.r2.cloudflarestorage.com/assets/org_3a7f10/ast_7c1e9a?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260922T104000Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=2f9a7c1e4b8d6035a1c7e2b94f0d38615c9a7e3d2b4f81c60d5a3e9b7f1c4028"}}}},"description":"The Asset, its Registrations, and a signed link to the bytes that expires in 15 minutes."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `asset:read`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No stored Asset with that id in this organization. A reservation answers this too, until finalize promotes it to `ready`. `error` is `not_found`."}},"summary":"Get one Asset","tags":["Assets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^(ast_)[\\s\\S]{0,}$","description":"The Asset id."}}]},"patch":{"security":[{"bearerAuth":[]}],"description":"Renames an Asset. The name is what the console shows and what a provider receives at future registrations. AdCrunch trims the name, and a name that is empty after the trim clears it back to the original filename. A Registration already made keeps the name it was given. An Asset belonging to another organization is reported as missing, so the surface cannot be used to discover that an id is real.","operationId":"renameAsset","responses":{"200":{"content":{"application/json":{"example":{"contentHash":"9d2f41b7c6e8a35014f7b2d9c3e6a810","createdAt":1790000000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":15000,"filename":"spring-sale.mp4","height":1920,"id":"ast_7c1e9a","key":"org_3a7f10/ast_7c1e9a","kind":"video","mimeType":"video/mp4","name":"Spring sale 9:16","organizationId":"org_3a7f10","sizeBytes":24117248,"status":"ready","updatedAt":1790000120000,"width":1080},"schema":{"type":"object","properties":{"contentHash":{"type":"string","description":"AdCrunch’s own hash of the stored bytes. A hint for de-duplication, never an identity, and never a provider’s hash. Empty when the store reports none."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"createdBy":{"type":"string","description":"The user who reserved the upload."},"deletedAt":{"nullable":true,"description":"Always `null` here. A deleted Asset is not answered.","type":"number"},"durationMs":{"nullable":true,"description":"How long the video runs, in milliseconds, read from the header of the file at finalize. `null` for an image. For a video, `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"filename":{"type":"string","description":"The name the file was uploaded under. Never a lookup key."},"height":{"nullable":true,"description":"The height in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"},"id":{"type":"string","description":"The id of the Asset, `ast_…`."},"key":{"type":"string","description":"Where the bytes are held. Read-only, and never sent by you."},"kind":{"type":"string","enum":["image","video"],"description":"Follows from `mimeType`, so the stored bytes verify it at finalize. It selects how a provider takes the file."},"mimeType":{"type":"string","description":"The `Content-Type` the file was uploaded with, verified against the stored bytes at finalize."},"name":{"nullable":true,"description":"The name a person gave this Asset, and what a provider receives at a future registration. `null` until somebody renames it, and the filename is the fallback.","type":"string"},"organizationId":{"type":"string","description":"The organization that owns the Asset."},"sizeBytes":{"type":"number","description":"Read from the stored object at finalize."},"status":{"type":"string","enum":["pending","ready"],"description":"`ready` once the bytes have been inspected. A `pending` Asset is a reservation whose bytes have not arrived, and no operation here answers one."},"updatedAt":{"nullable":true,"description":"When the Asset last changed, in milliseconds since the Unix epoch, UTC: at finalize, or at the last rename.","type":"number"},"width":{"nullable":true,"description":"The width in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.","type":"number"}},"required":["contentHash","createdAt","createdBy","deletedAt","durationMs","filename","height","id","key","kind","mimeType","name","organizationId","sizeBytes","status","updatedAt","width"],"additionalProperties":false,"example":{"contentHash":"9d2f41b7c6e8a35014f7b2d9c3e6a810","createdAt":1790000000000,"createdBy":"usr_2e5b84","deletedAt":null,"durationMs":15000,"filename":"spring-sale.mp4","height":1920,"id":"ast_7c1e9a","key":"org_3a7f10/ast_7c1e9a","kind":"video","mimeType":"video/mp4","name":"Spring sale 9:16","organizationId":"org_3a7f10","sizeBytes":24117248,"status":"ready","updatedAt":1790000120000,"width":1080}}}},"description":"The Asset, with its new name. It carries no Registration — read the Asset for those."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `asset:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No stored Asset with that id in this organization. A reservation answers this too, until finalize promotes it to `ready`. `error` is `not_found`."}},"summary":"Rename an Asset","tags":["Assets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^(ast_)[\\s\\S]{0,}$","description":"The Asset id."}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"description":"The name to show. AdCrunch trims it. Send an empty string to clear it back to the original filename."}},"required":["name"],"additionalProperties":false,"example":{"name":"Spring sale 9:16"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"description":"The name to show. AdCrunch trims it. Send an empty string to clear it back to the original filename."}},"required":["name"],"additionalProperties":false,"example":{"name":"Spring sale 9:16"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"description":"The name to show. AdCrunch trims it. Send an empty string to clear it back to the original filename."}},"required":["name"],"additionalProperties":false,"example":{"name":"Spring sale 9:16"}}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes our copy of the bytes and removes the Asset from the library. A reservation can be deleted too, before or after its bytes arrive, which is how one is abandoned deliberately rather than left for the daily sweep.\n\n**This does not remove it from any provider.** Once registered, the bytes are in that ad account’s library and are no longer ours to withdraw — `remainingRegistrations` counts the Registrations the Asset still has. Say so to the user; letting them assume otherwise is worse than refusing.","operationId":"deleteAsset","responses":{"200":{"content":{"application/json":{"example":{"ok":true,"remainingRegistrations":1},"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"remainingRegistrations":{"type":"number","description":"How many provider libraries still hold these bytes. AdCrunch cannot withdraw them. Tell the person; letting them assume otherwise is worse."}},"required":["ok","remainingRegistrations"],"additionalProperties":false,"example":{"ok":true,"remainingRegistrations":1}}}},"description":"The Asset is gone from AdCrunch. `remainingRegistrations` counts the Registrations it still has, and AdCrunch cannot withdraw the bytes a provider already took."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `asset:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No Asset with that id in this organization, reservation or stored file. A deleted one answers this too, so a second delete answers `404`. `error` is `not_found`."}},"summary":"Delete an Asset","tags":["Assets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^(ast_)[\\s\\S]{0,}$","description":"The Asset id."}}]}},"/assets/{id}/registrations":{"post":{"security":[{"bearerAuth":[]}],"description":"**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers)\n\nPlaces an Asset in one **advertiser's** provider-side library. Not a provider — an organization has several advertisers, and the bytes land in exactly one of them.\n\nReturns immediately with a `running` Registration; poll `GET /assets/{id}` for its state. Video can take several minutes to process at the provider, which is normal rather than a failure.\n\nA Registration that is `running` or `ready` refuses a second one for the same pair. A `failed` one starts again: send this request, and AdCrunch replaces it.","x-adcrunch-providers":["meta","tiktok"],"operationId":"registerAsset","responses":{"200":{"content":{"application/json":{"example":{"advertiserId":"acc_1203456789012345","assetId":"ast_7c1e9a","createdAt":1790000121000,"deletedAt":null,"failureCode":null,"failureReason":null,"id":"reg_4b81d2","organizationId":"org_3a7f10","provider":"meta","providerIdentifier":null,"status":"running","updatedAt":null,"uploadSessionId":null,"workflowId":"reg_4b81d2"},"schema":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser whose provider library gets the Asset."},"assetId":{"type":"string","description":"The Asset that the Registration places."},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"deletedAt":{"nullable":true,"description":"Always `null` here. When a retry replaces a failed Registration, AdCrunch sets this time on the old one and answers it no more.","type":"number"},"failureCode":{"nullable":true,"description":"Why AdCrunch refused the Registration before any provider call. Branch on this field, never on `failureReason`. `not_found`: your organization owns no advertiser of this provider with this id. `provider_not_connected`: the advertiser has no usable connection to the provider. Reconnect the provider, then register again. `missing_write_access`: the connection does not permit this kind of file. Reconnect the provider to grant it. `null` when the Registration did not fail, or when it failed at the provider.","type":"string","enum":["not_found","provider_not_connected","missing_write_access"]},"failureReason":{"nullable":true,"description":"Why the Registration failed, as a sentence: the words of the provider, or the words of AdCrunch. `null` unless `status` is `failed`.","type":"string"},"id":{"type":"string","description":"The id of the Registration, `reg_…`."},"organizationId":{"type":"string","description":"The organization that owns the Asset."},"provider":{"type":"string","enum":["meta","gads","tiktok","x","openai","snapchat","dv360"],"description":"The provider that owns the advertiser."},"providerIdentifier":{"nullable":true,"description":"The id that the provider gave the file: a Meta image hash, or the id of the image or the video at the provider. `null` until the provider gives it, and `null` on a `failed` Registration. An image gets its id when the Registration becomes `ready`. A video gets its id earlier, while the Registration is `running`, because the provider gives the id before it has processed the file. Build a creative from a `ready` Registration only.","type":"string"},"status":{"type":"string","enum":["running","ready","failed"],"description":"Video can stay `running` for several minutes while the provider processes it. That is normal, and not a failure."},"updatedAt":{"nullable":true,"description":"When the Registration last changed, in milliseconds since the Unix epoch, UTC. `null` until its first change.","type":"number"},"uploadSessionId":{"nullable":true,"description":"An in-flight provider upload session, where the protocol has one. `null` for every image.","type":"string"},"workflowId":{"type":"string","description":"The durable execution that runs the Registration."}},"required":["advertiserId","assetId","createdAt","deletedAt","failureCode","failureReason","id","organizationId","provider","providerIdentifier","status","updatedAt","uploadSessionId","workflowId"],"additionalProperties":false,"example":{"advertiserId":"acc_1203456789012345","assetId":"ast_7c1e9a","createdAt":1790000121000,"deletedAt":null,"failureCode":null,"failureReason":null,"id":"reg_4b81d2","organizationId":"org_3a7f10","provider":"meta","providerIdentifier":null,"status":"running","updatedAt":null,"uploadSessionId":null,"workflowId":"reg_4b81d2"}}}},"description":"The Registration, at `running`. `providerIdentifier` is `null` until the provider has the bytes — read the Asset to see it settle."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"in":{"type":"string","enum":["body","cookie","headers","params","query"],"description":"The part of the request that holds the field."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."},"path":{"type":"string","description":"A JSON Pointer into that part of the request, such as `/filename`. An empty string is the whole part."}},"required":["in","message","path"],"additionalProperties":false},"description":"One entry for each field that does not match."},"error":{"type":"string","enum":["invalid_request"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["issues","error","message"],"additionalProperties":false}}},"description":"The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed."},"401":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`."},"402":{"content":{"application/json":{"schema":{"type":"object","properties":{"paywallUrl":{"type":"string","format":"uri","description":"The page where an owner of the organization subscribes. Show it to the person."},"error":{"type":"string","enum":["organization_unentitled"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["paywallUrl","error","message"],"additionalProperties":false}}},"description":"The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"],"description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false}}},"description":"The caller does not hold `asset:write`. `error` is `forbidden`."},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"No stored Asset with that id, or no such advertiser in this organization. The two are one answer, so the surface cannot be used to discover that an id is real. `error` is `not_found`."},"409":{"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"A stable code for the failure. This is the field to branch on. It does not change for a given failure."},"message":{"type":"string","description":"A sentence to show a person. Written to say what to do next. Reworded whenever it can be said better, so never branch on it."}},"required":["error","message"],"additionalProperties":false,"description":"The failure shape of every AdCrunch API."}}},"description":"This Asset is already in that advertiser’s library. `error` is `already_registered`."}},"summary":"Register an Asset to an advertiser","tags":["Assets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^(ast_)[\\s\\S]{0,}$","description":"The stored Asset to place."}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"advertiserId":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The advertiser whose library the bytes land in. Its provider decides how they get there."}},"required":["advertiserId"],"additionalProperties":false,"example":{"advertiserId":"acc_1203456789012345"}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"advertiserId":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The advertiser whose library the bytes land in. Its provider decides how they get there."}},"required":["advertiserId"],"additionalProperties":false,"example":{"advertiserId":"acc_1203456789012345"}}},"multipart/form-data":{"schema":{"type":"object","properties":{"advertiserId":{"type":"string","pattern":"^(acc_)[\\s\\S]{0,}$","description":"The advertiser whose library the bytes land in. Its provider decides how they get there."}},"required":["advertiserId"],"additionalProperties":false,"example":{"advertiserId":"acc_1203456789012345"}}}}}}}},"security":[{"bearerAuth":[]}],"servers":[{"description":"production stage","url":"https://api.adcrunch.dev"}],"tags":[{"description":"Aggregated metrics for any level: advertiser, campaign, ad group or ad. The metrics include spend, impressions, clicks, reach and conversions. Group the rows into days, weeks or months, and break them down by provider, by advertiser or by entity type. A date range is required.","name":"Insights"},{"description":"The campaigns of your advertisers. Read every account of your organization at once, or one account on its own. A campaign is a `campaign` at every provider, so one listing spans them all.","name":"Campaigns"},{"description":"The ad groups of your advertisers. One concept and three words: a Meta **ad set**, a TikTok **ad group** and a Google Ads **ad group**. Every row keeps the word its own provider writes, in `type`. Performance Max campaigns have no ad groups — read the asset groups instead.","name":"Ad groups"},{"description":"The ads of your advertisers. An `ad` at Meta and TikTok; at Google Ads an **`ad_group_ad`**, the link resource whose id is the composite `{adGroupId}~{adId}`. Narrow by `adGroupId` for one ad group, or by `campaignId` for every ad of a campaign. Performance Max campaigns have no ads.","name":"Ads"},{"description":"The creatives of your advertisers: the image or video, the headline, the body copy and the call to action a provider assembles for an ad. A Creative is **account-level**, and Meta is the one provider that has one — TikTok embeds its creative material in the Ad itself, and Google Ads keeps its own on the `ad_group_ad` link resource. A Creative is not an Asset: an Asset is a source media file your organization owns.","name":"Creatives"},{"description":"The asset groups of your advertisers. Performance Max’s delivery container, and Google Ads only. A Performance Max campaign has no ad groups and no ads — the asset group holds what its automation assembles into placements. A child of a campaign and a sibling of an ad group.","name":"Asset groups"},{"description":"Start a change on an advertiser, poll the one you started, and read the organization history.","name":"Mutations"},{"description":"Your organization's ad-ops playbooks. List them, read one, or write one. The slug is the handle an agent uses. You can also import a Catalog Skill from the Skill Catalog, the playbooks that AdCrunch writes, and re-import it when AdCrunch publishes a later version.","name":"Skills"},{"description":"Your organization's brand context: identity, voice, guidelines and messaging, written as markdown. A list gives you the name, the description, the logo and which of the four sections carry prose. Read one Brand to get its narrative. The slug is the handle an agent uses.","name":"Brands"},{"description":"The audiences a Brand speaks **to**, described as people. A Persona belongs to one Brand only, and you address it through that Brand. Its handle is thus the pair of slugs, and two Brands can each hold a `loyalists`. Two Brands that describe the same real person hold two Personas, and not one shared Persona.","name":"Personas"},{"description":"Files attached to a Brand, such as a logo or a guidelines PDF. The bytes do not go through this API: reserve the upload, send the bytes to the URL you get back, then finalize. A Document is reference material that an agent reads. It never leaves AdCrunch, and this is what makes it different from an Asset.","name":"Documents"},{"description":"What your organization intends to run, before it buys: on which channels, for whom, for how much, and over what period. **A Campaign Plan is not a provider campaign.** It belongs to AdCrunch, and it comes before any provider. The campaigns that a Campaign Plan later produces are in the Observe API. Write each amount in whole units of the currency of that Campaign Plan.","name":"Campaign Plans"},{"description":"The rows of a Campaign Plan. One Line Item is one thing you buy. It is coarser than an ad set: it holds the **envelope** of an intent, and it can produce more than one provider object. Therefore it holds only what is the same for all of them. A Line Item has no handle of its own. Address it through its Campaign Plan.","name":"Line Items"},{"description":"An organization's source media. Reserve an upload, transfer the bytes directly to storage, then finalize — which is where what actually landed is validated.","name":"Assets"}],"x-adcrunch-commit":"857b776d8cb5ad07d493248323bb2df2f8ab152b","x-tagGroups":[{"name":"Observe API","tags":["Insights","Campaigns","Ad groups","Ads","Creatives","Asset groups"]},{"name":"Mutations API","tags":["Mutations"]},{"name":"Context API","tags":["Skills","Brands","Personas","Documents","Campaign Plans","Line Items"]},{"name":"Assets API","tags":["Assets"]}]}