{"components":{"securitySchemes":{"bearerAuth":{"description":"Send `Authorization: Bearer <credential>`. Two credentials work.\n\nAn **API key** (`acr_…`) is what a server integration uses. Create one in the AdCrunch console under Settings → API keys — see https://docs.adcrunch.dev/use/account/api-keys. A key is bound to the organization that was active when you created it, and it keeps acting on that organization whatever you do later. It carries the permissions of the member who created it.\n\nA **session** issued by the AdCrunch sign-in flow also works, which is how the console calls this service from the browser.\n\nEither way the organization comes from the credential. There is no organization parameter.","scheme":"bearer","type":"http"}}},"info":{"contact":{"email":"tech@adcrunch.dev"},"description":"Every AdCrunch API, in one document. Each service also publishes its own, under its path segment.\n\nOne origin serves all of them, and the path segment names the service: `/observe` reads, `/mutation` writes, `/define` holds what an organization writes down for its agents. One credential works everywhere — see the security scheme.\n\nEach section below is the service’s own description, as that service published it.\n\n## Observe API\n\nReads the ad data of the ad accounts your organization connects to AdCrunch: campaigns, ad groups, ads, creatives, and the metrics (Insights) that each provider reports against them. The providers are Meta, TikTok, Google Ads, Snapchat and DV360.\n\n## Scope\n\nEvery endpoint reads only the organization that your credential names. An **Advertiser** is an ad account that your organization connects. It is not a competitor that AdCrunch observed somewhere else.\n\nAdCrunch normalizes the entities across the providers: a Meta `adset` and a TikTok `adgroup` are both an ad group. Where a provider has no such entity, the rows are absent. TikTok, for example, has no separate Creative. AdCrunch does not invent data.\n\n## Authentication\n\nEvery route needs the `observe:read` permission. An **API key** (`acr_…`) or a sign-in session carries it. Create a key in the AdCrunch console under Settings → API keys: https://docs.adcrunch.dev/use/account/api-keys.\n\nThe credential also names the organization. If you send no `advertiserId`, a list endpoint reads every advertiser that the organization owns. AdCrunch refuses a request for the data of a different organization.\n\n## Entity ids\n\nAn advertiser id starts with `acc_`. Every other entity keeps the id that its provider gives it, with no prefix. AdCrunch keys an entity by provider, type and id, and an insight by provider, type, entity id and date. Therefore the same provider id never collides across two types or two providers.\n\n## Failures\n\nEvery failure answers `{ \"error\": \"<code>\", \"message\": \"<sentence>\" }`. `error` is a stable code, and it is the only field to branch on. `message` is written for a person, and it is reworded whenever it can be said better. Every AdCrunch API answers this way.\n\nThis API refuses on the credential and on nothing else, so `unauthorized` and `forbidden` are the only codes it sends today. **A read that finds nothing is not a failure.** It answers an empty list, or an empty body, and both are normal.\n\nTwo answers do not take that shape, and both come from the framework rather than from AdCrunch: the `422` a request gets when it does not match an operation’s own schema, and the plain text `NOT_FOUND` a `404` carries. Each operation that can send one describes it.\n\n## Mutation API\n\nChanges live ad accounts: pause and resume, budgets, and the creation of campaigns, ad sets, ads, and creatives.\n\n## A change is asynchronous\n\nA provider write is slow and can fail halfway, so this API never performs one inside your request. `POST /mutations` records what you asked for and answers with a `workflowId`. Poll `GET /mutations/{id}` until it reports `complete` or `errored`.\n\nThe record is durable. Every change is written to the organization history with the ad account, the exact change, who asked for it, and how it ended — readable through `GET /mutations` and shown on the console Activity page.\n\nA refused request starts nothing. Validation, ownership, and write access are all checked before a Mutation exists, so a failed call leaves no row in the history.\n\n## What this API will not do\n\nThese limits are deliberate, and no field turns them off.\n\n- **A create never starts spend.** Everything this API creates arrives paused. Start delivery as a separate, explicit act with `meta_set_status`.\n- **Nothing is deleted.** `meta_set_status` with `ARCHIVED` retires an object instead. On Meta that is close to one-way: an archived object cannot be returned to delivery.\n- **Only named fields are accepted.** There is no passthrough object for arbitrary provider fields. An action states what it takes, and the schema below is the whole of it.\n\n## Money\n\nEvery budget is an integer in the **minor unit** of the ad account currency. 1000 is 10.00 in an account denominated in dollars or euros. There is no decimal form, and a budget sent as a major unit spends 100 times what you intended.\n\n## Authentication\n\nSend `Authorization: Bearer <credential>`. Use an **API key** (`acr_…`) from the AdCrunch console under Settings → API keys, or a sign-in session. A key acts as the organization that was active when you created it. See https://docs.adcrunch.dev/use/account/api-keys.\n\nThe credential names the organization, and an ad account belonging to another organization answers `404`.\n\n## Permissions\n\n`mutation:write` starts a Mutation and polls the one it started. `mutation:read` reads the organization history. Polling sits with the write because it is the second half of the change you started.\n\n## Failures\n\nEvery failure answers `{ \"error\": \"<code>\", \"message\": \"<sentence>\" }`. `error` is a stable code, and it is the only field to branch on. `message` is written for a person, and it is reworded whenever it can be said better.\n\nEvery AdCrunch API answers this way, the Define API included. The one exception is a request that fails an operation’s own schema: that `422` carries the framework’s shape, and each operation that can return one says so.\n\nThe same capabilities reach an AI agent as MCP tools, on the same permissions.\n\n## Define API\n\nStores what an organization writes down so that an AI agent knows how to act, and who it acts for: its playbooks, its brands, the audiences those brands speak to, and the campaigns it intends to run.\n\nA **Skill** is an ad-ops playbook. It teaches an agent how to do one workflow. A **Brand** is narrative context — identity, voice, guidelines, messaging — that an agent reads before it acts for that brand. A **Persona** is one audience that a Brand speaks to. A **Campaign Plan** states what an organization intends to run, and its rows are **Line Items**. An agent reads the first three. It acts from the fourth.\n\n## HTTP or MCP?\n\nEverything here also reaches an AI agent as MCP tools: `skill_*`, `brand_*`, `persona_*`, `document_*`, `campaign_plan_*` and `line_item_*`. The two surfaces run the same code, use the same words, and obey the same permissions. Choose by **who calls**.\n\n- **MCP** is for an agent in a conversation. Its permissions come from the scopes on an OAuth token, and its tools show a little at a time.\n- **This API** is for a server integration that you write. Its permissions come from the role of the member who made the API key.\n\nThe two surfaces are different in two places, and this is deliberate. This API lists Personas and MCP does not, because there a Brand fetch already carries its Personas. Neither surface has a fetch for one Line Item, because a plan fetch carries its rows in full.\n\n## Read before you write\n\nA listing never carries prose. `GET /brands/` tells you which sections have content, not what they say. `GET /skills/` gives the descriptions, not the bodies. To read one thing, fetch it. An agent can thus choose without a load of everything.\n\nA section that nobody wrote is `null`. This is a normal state, because you write these one part at a time. It is never an error.\n\n## Every write is guarded\n\nEach row carries a `revision`. Send the one you read as `base_revision`, in the body of a `PATCH` or in the query string of a `DELETE`. If somebody wrote first, the answer is `409` with `error: \"revision_mismatch\"` and the current revision. Read the row again, apply your change on top, and send it again. People in a console and agents over MCP edit these at the same time, so this is a real race.\n\n## Money\n\nEvery amount on a Campaign Plan is an integer in the **minor unit** of the plan currency. In a plan in euros, 1000 is 10.00. An amount sent as a major unit plans 100 times what you intended.\n\nA plan holds three different money figures: the **envelope** it has, the **allocated** sum of its rows, and the gap between the two. AdCrunch enforces nothing between them. A plan that somebody is writing is usually under-allocated. An over-allocated plan is information to show, and never an error. Where the rows mix lifetime totals and daily rates, there is no meaningful sum: `allocation.comparable` is then `false`.\n\n## Failures\n\nEvery failure answers `{ \"error\": \"<code>\", \"message\": \"<sentence>\" }`. `error` is a stable code, and it is the only field to branch on. `message` is written for a person, and it is reworded whenever it can be said better. Some failures add a field that makes them actionable: `currentRevision` on `revision_mismatch`, `slug` on `slug_conflict`.\n\nEvery AdCrunch API answers this way, the Mutation API included. The one exception is a request that fails an operation’s own schema: that `422` carries the framework’s shape, and each operation that can return one says so.\n\nThe credential names the organization. Anything belonging to another organization answers `404` rather than `403`, because this API does not confirm that a slug exists somewhere else.\n\nNothing in this API touches a provider, spends money, or changes a live campaign. The approval of a plan opens the gate for execution. To execute, use the Mutation API with the `mutation:write` permission.\n\n## Assets API\n\nHolds an organization's **Assets** — the source media it owns, image and video — and **registers** an Asset into an advertiser's provider-side library, which is what makes it usable in an ad.\n\nAn Asset is the file. A **Registration** is that file in one advertiser’s library at that provider, with the identifier the provider gave back. One Asset holds many Registrations, one for each advertiser you place it with. The identifier is what the Mutation API builds a creative from.\n\n## Upload in three steps\n\nThe bytes never pass through this API. Cloudflare refuses an over-limit body before any code runs, so a large video could not arrive here at all.\n\n1. `POST /assets/uploads` reserves an Asset and answers a short-lived `uploadUrl`.\n2. `PUT` the file to that URL. That URL is the credential: anybody holding it can write that one object until it expires.\n3. `POST /assets/{id}/finalize` completes the Asset from the object that landed.\n\n**Finalize is the only validation point.** A presigned `PUT` carries no policy, so what you declared at step 1 controls nothing. The kind, the type and the size are read at step 3 off the stored object, and a file that fails there is discarded rather than left to bill as an orphan. AdCrunch accepts GIF, JPEG, PNG and WebP up to 30 MB, and MP4 and QuickTime up to 4 GB.\n\n## Registration is asynchronous\n\nA registration answers immediately at `running` and settles later. Video can take several minutes at the provider, which is normal rather than a failure. Read `GET /assets/{id}` for the state; there is no separate status resource, because \"where is this file, and how did each Registration go\" is one question.\n\nA provider with no adapter — Google Ads, Snapchat — records the Registration as `unsupported`. AdCrunch never claims a Registration it did not make.\n\n**Deleting an Asset does not withdraw it from a provider.** Those bytes are in that ad account’s library and are no longer ours. `remainingRegistrations` counts how many still hold them.\n\n## HTTP or MCP?\n\nEverything here also reaches an AI agent as MCP tools: `asset_list`, `asset_get`, `asset_create_upload`, `asset_finalize` and `asset_register`. The two surfaces run the same code, use the same words, and obey the same permissions. Choose by **who calls**.\n\n- **MCP** is for an agent in a conversation. Its permissions come from the scopes on an OAuth token.\n- **This API** is for a server integration that you write. Its permissions come from the role of the member who made the API key.\n\nRenaming an Asset and deleting one are the two things this API has that MCP does not.\n\n## Authentication\n\nA read needs the `asset:read` permission. Everything that stores bytes or places them needs `asset:write`. An API key (`acr_…`) or a sign-in session carries either — see the security scheme.\n\nThe credential also names the organization. An Asset belonging to another organization answers `404` rather than `403`, because this API does not confirm that an id exists somewhere else.\n\n## Failures\n\nEvery failure answers `{ \"error\": \"<code>\", \"message\": \"<sentence>\" }`. `error` is a stable code, and it is the only field to branch on. `message` is written for a person, and it is reworded whenever it can be said better. Every AdCrunch API answers this way.\n\nThe one exception is a request that fails an operation’s own schema: that `422` carries the framework’s shape, and each operation that takes input says so.","license":{"name":"Proprietary","url":"https://adcrunch.dev"},"title":"AdCrunch API","version":"1.0.0"},"openapi":"3.0.3","paths":{"/observe/entities/":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the ad entities of the advertisers of your organization, normalized across the providers: campaigns, ad groups, ads, creatives and the rest of the tree, in one shape.\n\nFilter them by the provider `type` (`campaign`, `adset`, `ad_group`, `line_item`, `ad`, `creative`, …), by `parentId`, by `status`, by `ids`, or by any combination. The newest entity comes first.\n\n## Walking the tree\n\nA row names its parent in `parentId`, written `type:id`. Send that string back as the `parentId` filter to list its children — one call for each level down.\n\nA row also carries a `path`: its ancestors and itself, ids joined by `/`. That is what makes a whole subtree one string comparison rather than a walk.\n\n## Advertisers\n\nOmit `advertiserId` to read every advertiser of your organization in one call. `limit` and `offset` then page **each advertiser**, not the merged answer, so a `limit` of 10 over three advertisers can answer 30 rows.\n\nAn `advertiserId` your organization does not own answers an empty list, not a failure.\n\n## What is here, and what is not\n\nA row carries the fields AdCrunch normalizes and nothing else. For the provider’s own payload — objective, budget mode, targeting, and every other key the provider writes — read `GET /entities/{provider}/{type}/{id}`.\n\nWhere a provider has no such entity, the rows are simply absent. TikTok has no separate Creative, for example, so a TikTok advertiser lists none. AdCrunch does not invent one.","operationId":"listEntities","responses":{"200":{"content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The ad account that owns it, prefixed `acc_`."},"createdAt":{"type":"number","description":"When AdCrunch first stored this row."},"createdTime":{"nullable":true,"description":"When the provider created the entity. Null where the provider reports none, which happens for criteria-like entities such as keywords and audiences.","type":"number"},"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."},"parentId":{"nullable":true,"description":"The parent, written `type:id`, for example `campaign:1868434093718705`. Null at the top of the tree. Send it back as the `parentId` filter to list the children.","type":"string"},"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`, `gads`, `snapchat` or `dv360`."},"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`."},"type":{"type":"string","description":"The type the provider uses, kept as the provider writes it: `campaign`, `adset` at Meta, `ad_group` at TikTok and Google Ads, `asset_group`, `keyword`, `ad`, `creative`. AdCrunch does not rename them, because a Meta `adset` and a Google `ad_group` are not the same object."},"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"}},"required":["advertiserId","createdAt","createdTime","deletedAt","id","name","parentId","path","provider","status","type","updatedAt","updatedTime"],"additionalProperties":false},"description":"The matching entities, newest first by the provider’s own created time. An empty array means nothing matched, which is a normal answer and not a failure."}}},"description":"The matching entities. An empty array means nothing matched."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"summary":"List entities","tags":["Entities"],"parameters":[{"name":"advertiserId","in":"query","required":false,"schema":{"description":"Read one advertiser. Omit it to read every advertiser of your organization.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},{"name":"ids","in":"query","required":false,"schema":{"description":"Entity ids to filter on, of any type, separated by commas. An id is the bare id its provider gives it.","type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"description":"The maximum number of rows **for each advertiser**, not for the answer. Reading every advertiser of your organization with a limit of 10 can therefore answer more than 10 rows.","type":"integer","minimum":1,"maximum":500}},{"name":"offset","in":"query","required":false,"schema":{"description":"The number of rows to skip, for each advertiser.","type":"integer","minimum":0,"maximum":9007199254740991}},{"name":"parentId","in":"query","required":false,"schema":{"description":"Give only the direct children of this entity. Write the entity as `type:id`, for example `campaign:123` or `adset:456`. This is the `parentId` a row carries, sent back unchanged.","type":"string"}},{"name":"provider","in":"query","required":false,"schema":{"description":"Read one provider. AdCrunch ignores this field when you send an `advertiserId`, because the advertiser already names its provider.","anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]}]}},{"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":"type","in":"query","required":false,"schema":{"description":"The entity type that the provider uses, for example `campaign`, `adset`, `ad_group`, `insertion_order`, `line_item`, `asset_group`, `keyword`, `ad` or `creative`. Omit it to list every type.","type":"string"}}]}},"/observe/entities/{provider}/{type}/{id}":{"get":{"security":[{"bearerAuth":[]}],"description":"Gives the payload the provider reports for one entity, exactly as the provider sent it. This is the escape hatch from the normalized fields: objective, budget mode, targeting, and everything else AdCrunch does not model.\n\nAddress the entity by all three of `provider`, `type` and `id`. The `id` alone is not unique — AdCrunch keys an entity by the three together, so the same bare id may legitimately name a different object under another type or another provider. `GET /entities/` answers all three for every row.\n\n## When there is nothing to give\n\nTwo different empty answers, and they mean different things.\n\n- **`200` with an empty body** — AdCrunch does not know this entity, or your organization does not own the advertiser that holds it. The two are deliberately not told apart: distinguishing them would confirm that an entity exists to somebody who cannot read it.\n- **`404`** — AdCrunch knows the entity but holds no stored payload for it. It is a fresh row whose payload has not landed yet, or one stored before AdCrunch kept payloads.\n\nNeither is an error on your side. Both are worth retrying later; only the `404` is likely to change.","operationId":"getEntity","responses":{"200":{"content":{"application/json":{"schema":{"description":"The provider’s own payload for this entity, verbatim. 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. Read `GET /entities/` instead when you want the normalized fields."}}},"description":"The provider’s payload. The body is **empty** when AdCrunch does not know the entity, or when your organization does not own it."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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":{"text/plain":{"schema":{"enum":["NOT_FOUND"],"type":"string"}}},"description":"AdCrunch knows the entity but holds no stored payload for it. This body is the framework’s own — the plain text `NOT_FOUND`, not the `error`/`message` shape every AdCrunch failure takes (ADR 0045)."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"summary":"Get entity by provider, type, and id","tags":["Entities"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The entity id that the provider gives, with no prefix."}},{"name":"provider","in":"path","required":true,"schema":{"anyOf":[{"type":"string","enum":["meta"]},{"type":"string","enum":["tiktok"]},{"type":"string","enum":["snapchat"]},{"type":"string","enum":["gads"]},{"type":"string","enum":["dv360"]}],"description":"Provider that owns this entity."}},{"name":"type","in":"path","required":true,"schema":{"type":"string","description":"The entity type that the provider uses, for example `campaign`, `adset`, `ad` or `creative`."}}]}},"/observe/insights":{"get":{"security":[{"bearerAuth":[]}],"description":"Gives 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## 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 a `422`.\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| `provider` | one for each ad platform | `provider` |\n| `advertiser` | one for each ad account | `provider`, `advertiserId` |\n| an entity type | one for each entity of that type | `provider`, `advertiserId`, `entityId`, `type` |\n\nAn entity type is the name its provider uses — `campaign`, `adset` at Meta, `ad_group` at TikTok and Google Ads, `ad`, and so on.\n\nA `breakdown` deeper than `campaign` needs a `provider` or an entity filter, because the levels below a campaign differ per platform. `breakdown=adset` with no `provider` is refused with a `422` that names the breakdowns that are available.\n\n## Money\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.","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. Use a provider-native entity type (campaign, adset, ad_group, line_item, asset_group, keyword, ad, …) or `provider`/`advertiser`. Omit to aggregate every matching row into one result.","in":"query","name":"breakdown","required":false,"schema":{"description":"Group results by this level. Use a provider-native entity type (campaign, adset, ad_group, line_item, asset_group, keyword, ad, …) or `provider`/`advertiser`. Omit to aggregate every matching row into one result.","type":"string","enum":["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":"Filter to a single entity (any type), or — combined with 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 an entity-type `breakdown` — scope the breakdown to that entity’s subtree.","type":"string"}},{"description":"Filter rows to a single provider-native entity type.","in":"query","name":"entityType","required":false,"schema":{"description":"Filter rows to a single provider-native entity type.","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":"Page size. Clamped to 100.","in":"query","name":"limit","required":false,"schema":{"description":"Page size. Clamped to 100.","type":"number","minimum":1,"maximum":100}},{"description":"Pagination offset.","in":"query","name":"offset","required":false,"schema":{"description":"Pagination offset.","type":"number","minimum":0}},{"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"]}]}},{"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":{"schema":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"description":"The ad account this row belongs to, prefixed `acc_`. Present when `breakdown` is `advertiser` 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 an entity type.","type":"string"},"provider":{"description":"The ad platform: `meta`, `tiktok`, `gads`, `snapchat` or `dv360`. Present when `breakdown` is set to anything.","type":"string"},"type":{"description":"The provider entity type of `entityId`, for example `campaign`, `adset`, `ad_group` or `ad`. Present when `breakdown` is an entity type.","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 reports 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 does not report it, so it reads 0 there.","type":"number"},"frequency":{"description":"Impressions ÷ reach. Google Ads reports 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."}}},"description":"The matching rows. An empty array means nothing matched."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"summary":"List insights","tags":["Insights"]}},"/mutation/mutations":{"post":{"description":"Starts a change on a live ad account and returns immediately.\n\nThe change does **not** happen during this request. The response carries a `workflowId`; poll `GET /mutations/{id}` with it to find out whether the change succeeded. A Mutation is durable, so the id stays valid across retries and restarts.\n\nA request that is refused here starts nothing. A malformed body, an ad account that is not yours, and an ad account connected without write access are all rejected before any Mutation is created, so a failed call leaves no row in the history.\n\nEvery create action produces a **paused** object. This API cannot create anything that spends money on creation, and it cannot delete: `meta_set_status` with `ARCHIVED` is how an object is retired.","operationId":"startMutation","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","description":"The Meta id of the object to change. It is the native id, not an AdCrunch id."},"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 the minor unit of the ad account currency. 1000 is 10.00 in an account that uses dollars or euros. Give a daily budget or a lifetime budget, not both.","type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":9007199254740991},"id":{"type":"string","description":"The Meta id of the campaign or ad set to change. It is the native id, not an AdCrunch id."},"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 the minor unit of the ad account currency. 1000 is 10.00 in an account that uses dollars or euros. A lifetime budget needs an end time on the ad set.","type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":9007199254740991}},"required":["id","kind","level"],"additionalProperties":false,"title":"meta_update_budget"},{"type":"object","properties":{"dailyBudget":{"description":"The amount the campaign spends each day, shared across its ad sets. The amount is in the minor unit of the ad account currency. 1000 is 10.00 in an account that uses dollars or euros. Give one of the two budgets. Meta refuses a campaign with neither, and AdCrunch cannot yet create a campaign whose ad sets carry their own budgets.","type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":9007199254740991},"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 the minor unit of the ad account currency. 1000 is 10.00 in an account that uses dollars or euros. Give one of the two budgets. Meta refuses a campaign with neither, and AdCrunch cannot yet create a campaign whose ad sets carry their own budgets.","type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":9007199254740991},"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},"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","description":"The Meta id of the campaign this ad set belongs to. 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 the minor unit of the ad account currency. 1000 is 10.00 in an account that uses dollars or euros. Omit both budgets when the campaign carries one, which is the usual case: a campaign created here always carries a budget, and it is shared across its ad sets.","type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":9007199254740991},"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 the minor unit of the ad account currency. 1000 is 10.00 in an account that uses dollars or euros. It needs an end time.","type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":9007199254740991},"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","description":"The Meta id of the ad set this ad runs in. Create the ad set first and use the id it returns."},"creativeId":{"type":"string","description":"The Meta id of the creative this ad shows. 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"}],"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 ad account 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}}},"required":true},"responses":{"200":{"content":{"application/json":{"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}}},"description":"The Mutation started. Poll it for the outcome."},"400":{"content":{"application/json":{"schema":{"type":"object","properties":{"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":["error","message"],"additionalProperties":false}}},"description":"The body does not match the schema. `error` is `invalid_request`. Nothing was started."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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 ad account is connected without write access. In the second case `error` is `missing_write_access` and `message` says how to fix it. Nothing was started."},"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 ad account does not exist, or it belongs to another organization. Both answer the same way on purpose, so a caller cannot learn which. `error` is `not_found`. Nothing was started."}},"summary":"Start a mutation","tags":["Mutations"]},"get":{"description":"Lists your organization's Mutations, newest first.\n\nOne page at a time. When `nextCursor` is present there are more, so send it back as `cursor` to get the next page. When it is absent you have reached the end. The cursor is opaque: pass it back as you received it and do not construct one.\n\nThe page size is fixed, so there is no parameter for it.\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":{"schema":{"type":"object","properties":{"mutations":{"type":"array","items":{"type":"object","properties":{"action":{"description":"What was asked for, in the shape POST /mutations accepts."},"advertiserId":{"type":"string","description":"The ad account 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."},"provider":{"type":"string","description":"The ad platform, for example meta."},"result":{"nullable":true,"description":"What the provider returned. Present when the status is complete."},"status":{"type":"string","enum":["running","complete","errored"],"description":"How it ended, or that it has not."},"tool":{"type":"string","description":"Which action was performed."},"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","provider","result","status","tool","userId","workflowId"],"additionalProperties":false},"description":"The Mutations, newest first."},"nextCursor":{"description":"Send this back as cursor to read the next page. Absent on the last page.","type":"string"}},"required":["mutations"],"additionalProperties":false}}},"description":"One page of Mutations, newest first."},"400":{"content":{"application/json":{"schema":{"anyOf":[{"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},{"type":"object","properties":{"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":["error","message"],"additionalProperties":false}]}}},"description":"The cursor could not be read. `error` is `invalid_cursor`, or `invalid_request` when the query itself is malformed."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"summary":"List mutations","tags":["Mutations"],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"description":"The nextCursor from the previous page. Omit it for the first page. Pass it back exactly as you received it.","type":"string"}}]}},"/mutation/mutations/{id}":{"get":{"description":"Reports how a Mutation ended, or that it is still running.\n\n`running` means the change has not finished. Poll again. `complete` means the provider accepted the change, and `result` carries what it returned — a created object arrives as its new id. `errored` means the change did not happen.\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":{"schema":{"type":"object","properties":{"result":{"description":"What the provider returned. Present only when status is complete. A create carries the new object id."},"status":{"type":"string","enum":["running","complete","errored"],"description":"running means poll again. complete means the provider accepted the change. errored means it did not happen."}},"required":["status"],"additionalProperties":false}}},"description":"The Mutation is running, complete, or errored."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"summary":"Read a mutation","tags":["Mutations"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The workflowId that POST /mutations returned."}}]}},"/define/skills/":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the Skills of your organization. Each row gives the slug, the name and the description. It does not give the body. Deleted Skills are not in the list.","operationId":"listSkills","responses":{"200":{"content":{"application/json":{"schema":{"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."}},"required":["description","name","slug"],"additionalProperties":false}}}},"description":"Every Skill in the organization, as slug, name and description. Bodies are never returned here — fetch one Skill for its instructions."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"]},"post":{"security":[{"bearerAuth":[]}],"description":"Creates a Skill in your organization. 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":{"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"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["body","createdAt","createdBy","description","id","name","revision","slug","updatedAt"],"additionalProperties":false}}},"description":"The Skill as created."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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}},"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}},"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}}}}}},"/define/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.","operationId":"getSkill","responses":{"200":{"content":{"application/json":{"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"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["body","createdAt","createdBy","description","id","name","revision","slug","updatedAt"],"additionalProperties":false}}},"description":"The Skill, with its instruction body."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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 every field, and send the `base_revision` you read. The answer is `409` with `error=revision_mismatch` if the Skill changed after you read it. It is `409` with `error=slug_conflict` if the new name gives a slug that already exists.","operationId":"updateSkill","responses":{"200":{"content":{"application/json":{"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"},"updatedAt":{"nullable":true,"type":"number","description":"Milliseconds since the Unix epoch, UTC."}},"required":["body","createdAt","createdBy","description","id","name","revision","slug","updatedAt"],"additionalProperties":false}}},"description":"The Skill as it now stands."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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}},"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}},"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}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a Skill. Send the `base_revision` you read. The answer is `409` with `error=revision_mismatch` if the Skill changed after you read it.","operationId":"deleteSkill","responses":{"204":{"description":"Deleted. The row is soft-deleted, and the slug is free for re-use."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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."}}]}},"/define/brands/":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the Brands of your organization. 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.","operationId":"listBrands","responses":{"200":{"content":{"application/json":{"schema":{"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}}}},"description":"Every Brand in the organization. `sections` says which of the four carry prose, so a chooser can rank Brands without the listing becoming the brief."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"]},"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":{"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}}},"description":"The Brand as created. Every unwritten section comes back `null`, which is a normal state rather than an error."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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}},"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}},"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}}}}}},"/define/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":{"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}}},"description":"The Brand with all four sections. It does **not** carry its Personas — ask `GET /brands/{slug}/personas/` for those."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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 answer is `409` with `error=revision_mismatch` if the Brand changed after you read it. It is `409` with `error=slug_conflict` if the new name gives a slug that already exists.","operationId":"updateBrand","responses":{"200":{"content":{"application/json":{"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}}},"description":"The Brand as it now stands."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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}},"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}},"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}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a Brand. Send the `base_revision` you read. 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 and its Documents. 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."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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."}}]}},"/define/brands/{slug}/personas/":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists a Brand's Personas 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.","operationId":"listPersonas","responses":{"200":{"content":{"application/json":{"schema":{"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}}}},"description":"This Brand's Personas, 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."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"}}]},"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":{"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}}},"description":"The Persona as created."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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}},"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}},"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}}}}}},"/define/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":{"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}}},"description":"The Persona with all four sections."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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 answer is `409` with `error=revision_mismatch` if the Persona changed after you read it. It is `409` with `error=slug_conflict` if the new name gives a slug that already exists.","operationId":"updatePersona","responses":{"200":{"content":{"application/json":{"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}}},"description":"The Persona as it now stands."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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}},"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}},"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}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a Persona. Send the `base_revision` you read. The slug becomes free again in its Brand. The answer is `409` with `error=revision_mismatch` if the Persona changed after you read it.","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`."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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."}}]}},"/define/brands/{slug}/documents/":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the documents of a Brand. 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.","operationId":"listBrandDocuments","responses":{"200":{"content":{"application/json":{"schema":{"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}}}},"description":"The Brand’s finalized Documents. A reservation whose bytes never arrived is never listed, because there is no readable object behind it."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"}}]},"post":{"security":[{"bearerAuth":[]}],"description":"Reserves a document on a Brand. The answer gives a URL with a short life. Send the bytes to that URL, then call finalize. The bytes do not go through this API.","operationId":"createBrandDocumentUpload","responses":{"201":{"content":{"application/json":{"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}}},"description":"A reserved destination. PUT the bytes to `uploadUrl`, then call finalize with `documentId`. The Document does not exist for any reader until finalize succeeds."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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}},"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}},"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}}}}}},"/define/brands/{slug}/documents/{documentId}/finalize":{"post":{"security":[{"bearerAuth":[]}],"description":"Completes a reserved document after you send the bytes. 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.","operationId":"finalizeBrandDocument","responses":{"200":{"content":{"application/json":{"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}}},"description":"The Document, now readable. `sizeBytes` and `mimeType` are read off the stored object rather than taken from your declaration."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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":{"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":"The **stored** object is not an accepted type, whatever was declared at reservation. `error` is `unsupported_type`, and the object has been discarded."}},"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"}}]}},"/define/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.","operationId":"getBrandDocument","responses":{"200":{"content":{"application/json":{"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}}},"description":"The Document."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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. 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, that nomination is cleared — the clearing is done here rather than by the database, because the row is soft-deleted and a delete action would never fire."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"}}]}},"/define/brands/{slug}/advertisers/":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the ad accounts attached to a Brand.","operationId":"listBrandAdvertisers","responses":{"200":{"content":{"application/json":{"schema":{"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}}}},"description":"The ad accounts attached to this Brand, oldest first. The row shape here was not observed live — the verification organization has no connected advertiser — so it is written from the query projection."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"}}]},"post":{"security":[{"bearerAuth":[]}],"description":"Attaches an ad account to a Brand. An agent that works on that account can then find this brand context. Send the same call again for the same result. AdCrunch reads the provider from the advertiser. It does not take the provider from you.","operationId":"attachBrandAdvertiser","responses":{"201":{"content":{"application/json":{"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}}},"description":"Attached. Idempotent: attaching an account that is already attached answers the same way."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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 ad account 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}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"advertiser_id":{"type":"string","pattern":"^acc_[\\s\\S]{0,}$"}},"required":["advertiser_id"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"advertiser_id":{"type":"string","pattern":"^acc_[\\s\\S]{0,}$"}},"required":["advertiser_id"],"additionalProperties":false}}}}}},"/define/brands/{slug}/advertisers/{advertiserId}":{"delete":{"security":[{"bearerAuth":[]}],"description":"Detaches an ad account from a Brand.","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."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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 account 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 ad account 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"}}]}},"/define/campaign-plans/":{"get":{"security":[{"bearerAuth":[]}],"description":"Lists the Campaign Plans of your organization. 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 plan uses. This call does not give the Line Items. To read them, read one plan. Deleted plans are not in the list.","operationId":"listCampaignPlans","responses":{"200":{"content":{"application/json":{"schema":{"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. Integer minor units of `currency`.","type":"number"}},"required":["allocation","brandId","channels","currency","description","endDate","lineItemCount","name","revision","slug","startDate","status","totalBudget"],"additionalProperties":false}}}},"description":"Every Campaign Plan in the organization, with its money 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."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"]},"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 as an integer in the minor unit of the currency, for example cents.\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":{"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":"Integer minor 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}}},"description":"The Campaign Plan as created, in `draft`, with no rows."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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` (amounts are whole minor units, 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 plan may precede its brand or cover a portfolio. Addressed by slug because that is the handle every Define surface uses; the 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 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":"integer","minimum":0,"maximum":9007199254740991}},"required":["currency","description","name"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"brand_name":{"description":"The Brand's slug. Optional and re-pointable — a plan may precede its brand or cover a portfolio. Addressed by slug because that is the handle every Define surface uses; the 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 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":"integer","minimum":0,"maximum":9007199254740991}},"required":["currency","description","name"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"brand_name":{"description":"The Brand's slug. Optional and re-pointable — a plan may precede its brand or cover a portfolio. Addressed by slug because that is the handle every Define surface uses; the 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 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":"integer","minimum":0,"maximum":9007199254740991}},"required":["currency","description","name"],"additionalProperties":false}}}}}},"/define/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 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 plan.","operationId":"getCampaignPlan","responses":{"200":{"content":{"application/json":{"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":"Integer minor 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}}},"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."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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.\n\nThe answer is `409` with `error=currency_frozen` if a Line Item already has a budget, with `error=revision_mismatch` if the 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":{"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":"Integer minor 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}}},"description":"The Campaign Plan as it now stands."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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 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 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 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":"integer","minimum":0,"maximum":9007199254740991}},"required":["base_revision"],"additionalProperties":false}},"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 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 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":"integer","minimum":0,"maximum":9007199254740991}},"required":["base_revision"],"additionalProperties":false}},"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 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 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":"integer","minimum":0,"maximum":9007199254740991}},"required":["base_revision"],"additionalProperties":false}}}}},"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 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."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`. Deleting a Line Item moves the plan too, so re-read before deleting the plan."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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."}}]}},"/define/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 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\nA line that you add after the approval starts in draft, and it puts the plan back into draft. The answer is `409` with `error=revision_mismatch` if the plan changed after you read it.","operationId":"approveCampaignPlan","responses":{"200":{"content":{"application/json":{"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":"Integer minor 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}}},"description":"The approved plan, and how many Line Items this call validated. **Approving spends nothing and creates nothing** — it opens the execution gate, and executing goes through the mutation tools on `mutation:write`."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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}},"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}},"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}}}}}},"/define/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 plan was approved, this call puts the plan back into draft, and the answer tells you so.","operationId":"createLineItem","responses":{"201":{"content":{"application/json":{"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":"Integer minor 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}}},"description":"The Line Item, and the 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 plan to `draft`, and you have to be told."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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` (ISO 3166-1 alpha-2, uppercase); `invalid_age_range`; `invalid_amount` (whole minor units of the plan’s currency, never negative); `invalid_window`; `persona_not_in_brand` (a Persona must belong to the 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 plan, because an account belongs to one provider and a 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":"Integer minor units of the **plan’s** currency. Whether it reads as a total or a daily rate is derived from the effective end date, never stored.","type":"integer","minimum":0,"maximum":9007199254740991},"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 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 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 plan’s.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["channel","objective"],"additionalProperties":false}},"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 plan, because an account belongs to one provider and a 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":"Integer minor units of the **plan’s** currency. Whether it reads as a total or a daily rate is derived from the effective end date, never stored.","type":"integer","minimum":0,"maximum":9007199254740991},"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 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 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 plan’s.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["channel","objective"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"advertiser_id":{"description":"The ad account this line runs through. On the line rather than the plan, because an account belongs to one provider and a 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":"Integer minor units of the **plan’s** currency. Whether it reads as a total or a daily rate is derived from the effective end date, never stored.","type":"integer","minimum":0,"maximum":9007199254740991},"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 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 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 plan’s.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["channel","objective"],"additionalProperties":false}}}}}},"/define/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, because the approval of a plan speaks about the whole allocation. To open the gate again, validate the line again.\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":{"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":"Integer minor 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}}},"description":"The Line Item, bare rather than wrapped — unlike the create, which also reports the plan status. Any change returns a `validated` line to `draft`."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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":"`invalid_countries`, `invalid_age_range`, `invalid_amount`, `invalid_window`, `persona_not_in_brand` or `advertiser_not_owned`."}},"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":"integer","minimum":0,"maximum":9007199254740991,"description":"Integer minor units (cents), never major units."},"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 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}},"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":"integer","minimum":0,"maximum":9007199254740991,"description":"Integer minor units (cents), never major units."},"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 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}},"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":"integer","minimum":0,"maximum":9007199254740991,"description":"Integer minor units (cents), never major units."},"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 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}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes a Line Item. Send the `base_revision` you read. If the plan was approved, this call puts the 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 plan’s own revision moves — so re-read it before your next guarded write against it."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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."}}]}},"/define/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\nA call on a line that is already validated succeeds and changes nothing.","operationId":"validateLineItem","responses":{"200":{"content":{"application/json":{"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":"Integer minor 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}}},"description":"The Line Item, now `validated`. Validating an already-validated line is a no-op rather than an error."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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}},"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}},"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}}}}}},"/define/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\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.\n- The line has no ad account.\n- The ad account belongs to a different provider.\n- The currency of the ad account is different from the plan currency, or AdCrunch does not know that currency yet. To make it known, refresh the provider connection.\n- The dates do not resolve to a window.\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":{"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."}}},"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."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"}}]}},"/define/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. If you record the same object again, the answer is `200` with the existing row, and not `201`.\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 plan never declares that number.","operationId":"recordLineItemExecution","responses":{"200":{"content":{"application/json":{"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}}},"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":{"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}}},"description":"Recorded. Neither response shape was observed against the deployed service, because the verification organization has no connected advertiser — both are written from the Define Layer’s view type."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"]}]}},"required":["advertiser_id","entity_id","entity_type","provider"],"additionalProperties":false}},"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"]}]}},"required":["advertiser_id","entity_id","entity_type","provider"],"additionalProperties":false}},"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"]}]}},"required":["advertiser_id","entity_id","entity_type","provider"],"additionalProperties":false}}}}}},"/asset/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. Reservations whose bytes never arrived are excluded, as are deleted Assets.","operationId":"listAssets","responses":{"200":{"content":{"application/json":{"schema":{"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, where that is known. `null` for an image.","type":"number"},"filename":{"type":"string","description":"The name the file was uploaded under. Never a lookup key."},"height":{"nullable":true,"type":"number"},"id":{"type":"string"},"key":{"type":"string","description":"Where the bytes are held. Read-only, and never sent by you."},"kind":{"type":"string","enum":["image","video"],"description":"Read from the stored bytes at finalize, and never from what you declared. It selects how a provider takes the file."},"mimeType":{"type":"string","description":"Read from the stored object 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"},"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,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"width":{"nullable":true,"type":"number"},"registrations":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser whose library holds the bytes."},"assetId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"deletedAt":{"nullable":true,"description":"Set when a failed attempt was tombstoned to free the pair for a retry. `null` on a live Registration.","type":"number"},"failureReason":{"nullable":true,"description":"The provider’s own code and message. Only set on a failure.","type":"string"},"id":{"type":"string"},"organizationId":{"type":"string"},"provider":{"type":"string","enum":["meta","gads","tiktok","snapchat","dv360"],"description":"The provider that owns the advertiser."},"providerIdentifier":{"nullable":true,"description":"What the provider gave back — a Meta image hash or video id. `null` until the Registration is ready. This is the id a creative is built with.","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,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"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 placed it."}},"required":["advertiserId","assetId","createdAt","deletedAt","failureReason","id","organizationId","provider","providerIdentifier","status","updatedAt","uploadSessionId","workflowId"],"additionalProperties":false},"description":"Every advertiser this Asset has been placed with, and how each Registration went. An empty array means it is stored and registered nowhere."}},"required":["contentHash","createdAt","createdBy","deletedAt","durationMs","filename","height","id","key","kind","mimeType","name","organizationId","sizeBytes","status","updatedAt","width","registrations"],"additionalProperties":false}}}},"description":"Every stored Asset in the organization, newest first, each with its Registrations."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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"]}},"/asset/assets/uploads":{"post":{"security":[{"bearerAuth":[]}],"description":"Reserves an Asset and returns a short-lived presigned `PUT` URL to upload the bytes to.\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.","operationId":"createAssetUpload","responses":{"200":{"content":{"application/json":{"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."},"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}}},"description":"The reservation. Send the bytes to `uploadUrl`, then finalize `assetId`."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."},"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}},"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}},"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}}}}}},"/asset/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. **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":{"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, where that is known. `null` for an image.","type":"number"},"filename":{"type":"string","description":"The name the file was uploaded under. Never a lookup key."},"height":{"nullable":true,"type":"number"},"id":{"type":"string"},"key":{"type":"string","description":"Where the bytes are held. Read-only, and never sent by you."},"kind":{"type":"string","enum":["image","video"],"description":"Read from the stored bytes at finalize, and never from what you declared. It selects how a provider takes the file."},"mimeType":{"type":"string","description":"Read from the stored object 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"},"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,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"width":{"nullable":true,"type":"number"}},"required":["contentHash","createdAt","createdBy","deletedAt","durationMs","filename","height","id","key","kind","mimeType","name","organizationId","sizeBytes","status","updatedAt","width"],"additionalProperties":false}}},"description":"The stored Asset, now `ready`. `kind`, `mimeType` and `sizeBytes` are read off the stored object rather than taken from your declaration. It carries no Registration, whether or not you asked for one."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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 reservation with that id in this organization. `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":"The **stored** file is not an accepted type, whatever was declared at reservation. `error` is `unsupported_type`, and the object has been discarded."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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":{"type":"object","properties":{"advertiserId":{"description":"Place the file in this advertiser’s library in the same call. Optional.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"advertiserId":{"description":"Place the file in this advertiser’s library in the same call. Optional.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"advertiserId":{"description":"Place the file in this advertiser’s library in the same call. Optional.","type":"string","pattern":"^(acc_)[\\s\\S]{0,}$"}},"additionalProperties":false}}}}}},"/asset/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.","operationId":"getAsset","responses":{"200":{"content":{"application/json":{"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, where that is known. `null` for an image.","type":"number"},"filename":{"type":"string","description":"The name the file was uploaded under. Never a lookup key."},"height":{"nullable":true,"type":"number"},"id":{"type":"string"},"key":{"type":"string","description":"Where the bytes are held. Read-only, and never sent by you."},"kind":{"type":"string","enum":["image","video"],"description":"Read from the stored bytes at finalize, and never from what you declared. It selects how a provider takes the file."},"mimeType":{"type":"string","description":"Read from the stored object 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"},"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,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"width":{"nullable":true,"type":"number"},"registrations":{"type":"array","items":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser whose library holds the bytes."},"assetId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"deletedAt":{"nullable":true,"description":"Set when a failed attempt was tombstoned to free the pair for a retry. `null` on a live Registration.","type":"number"},"failureReason":{"nullable":true,"description":"The provider’s own code and message. Only set on a failure.","type":"string"},"id":{"type":"string"},"organizationId":{"type":"string"},"provider":{"type":"string","enum":["meta","gads","tiktok","snapchat","dv360"],"description":"The provider that owns the advertiser."},"providerIdentifier":{"nullable":true,"description":"What the provider gave back — a Meta image hash or video id. `null` until the Registration is ready. This is the id a creative is built with.","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,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"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 placed it."}},"required":["advertiserId","assetId","createdAt","deletedAt","failureReason","id","organizationId","provider","providerIdentifier","status","updatedAt","uploadSessionId","workflowId"],"additionalProperties":false},"description":"Every advertiser this Asset has been placed with, and how each Registration went. An empty array means it is stored and registered nowhere."},"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}}},"description":"The Asset, its Registrations, and a signed link to the bytes that expires in 15 minutes."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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 whose bytes never arrived answers this too. `error` is `not_found`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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; sending an empty name 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":{"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, where that is known. `null` for an image.","type":"number"},"filename":{"type":"string","description":"The name the file was uploaded under. Never a lookup key."},"height":{"nullable":true,"type":"number"},"id":{"type":"string"},"key":{"type":"string","description":"Where the bytes are held. Read-only, and never sent by you."},"kind":{"type":"string","enum":["image","video"],"description":"Read from the stored bytes at finalize, and never from what you declared. It selects how a provider takes the file."},"mimeType":{"type":"string","description":"Read from the stored object 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"},"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,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"width":{"nullable":true,"type":"number"}},"required":["contentHash","createdAt","createdBy","deletedAt","durationMs","filename","height","id","key","kind","mimeType","name","organizationId","sizeBytes","status","updatedAt","width"],"additionalProperties":false}}},"description":"The Asset, with its new name."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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. `error` is `not_found`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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. Send an empty string to clear it back to the original filename."}},"required":["name"],"additionalProperties":false}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"description":"The name to show. Send an empty string to clear it back to the original filename."}},"required":["name"],"additionalProperties":false}},"multipart/form-data":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"description":"The name to show. Send an empty string to clear it back to the original filename."}},"required":["name"],"additionalProperties":false}}}}},"delete":{"security":[{"bearerAuth":[]}],"description":"Deletes our copy of an Asset and removes it from the library. A reservation whose bytes never arrived can be deleted too, which is how one is abandoned deliberately rather than left for the 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 how many still hold them. Say so to the user; letting them assume otherwise is worse than refusing.","operationId":"deleteAsset","responses":{"200":{"content":{"application/json":{"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}}},"description":"The Asset is gone from AdCrunch. `remainingRegistrations` is how many provider libraries still hold the bytes."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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. `error` is `not_found`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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."}}]}},"/asset/assets/{id}/registrations":{"post":{"security":[{"bearerAuth":[]}],"description":"Places an Asset in one **advertiser's** provider-side library. Not a provider — an organization has several ad accounts, 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\nRegistering the same Asset to the same advertiser twice is refused rather than duplicated.","operationId":"registerAsset","responses":{"200":{"content":{"application/json":{"schema":{"type":"object","properties":{"advertiserId":{"type":"string","description":"The advertiser whose library holds the bytes."},"assetId":{"type":"string"},"createdAt":{"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"deletedAt":{"nullable":true,"description":"Set when a failed attempt was tombstoned to free the pair for a retry. `null` on a live Registration.","type":"number"},"failureReason":{"nullable":true,"description":"The provider’s own code and message. Only set on a failure.","type":"string"},"id":{"type":"string"},"organizationId":{"type":"string"},"provider":{"type":"string","enum":["meta","gads","tiktok","snapchat","dv360"],"description":"The provider that owns the advertiser."},"providerIdentifier":{"nullable":true,"description":"What the provider gave back — a Meta image hash or video id. `null` until the Registration is ready. This is the id a creative is built with.","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,"type":"number","description":"Milliseconds since the Unix epoch, UTC."},"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 placed it."}},"required":["advertiserId","assetId","createdAt","deletedAt","failureReason","id","organizationId","provider","providerIdentifier","status","updatedAt","uploadSessionId","workflowId"],"additionalProperties":false}}},"description":"The Registration, at `running`. `providerIdentifier` is `null` until the provider has the bytes — read the Asset to see it settle."},"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 credential, or one that does not resolve. Send an API key or a session. See the security scheme. `error` is `unauthorized`."},"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`."},"422":{"content":{"application/json":{"schema":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"One entry per failing field."},"found":{"description":"What was sent."},"message":{"type":"string"},"on":{"type":"string","description":"Which part of the request failed: body, query or params."},"property":{"type":"string","description":"The field that failed."},"type":{"type":"string","enum":["validation"]}},"required":["errors","found","message","on","property","type"],"additionalProperties":false}}},"description":"The request did not match this operation’s schema. This is the framework’s own shape, not the `error`/`message` one."}},"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}},"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}},"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}}}}}}},"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. You can group the rows into days, weeks or months, and you can break them down by level.","name":"Insights"},{"description":"Ad entities of every level: campaigns, ad sets, ad groups, line items, asset groups, keywords, ads and creatives. One endpoint lists them all. Filter them by the provider `type`, by `parentId`, or by both. To read the full payload that a provider reports for one entity, use `/entities/:provider/:type/:id`.","name":"Entities"},{"description":"Start a change on an ad account, 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.","name":"Skills"},{"description":"Your organization's brand context: identity, voice, guidelines and messaging, written as markdown. A list gives you the name and the description only. 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 plan later produces are in the Observe API. Write each amount as an integer in the minor unit of the plan currency.","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 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":"e06bc962dbb5271d431061e80c9c93da87aefb98","x-tagGroups":[{"name":"Observe API","tags":["Insights","Entities"]},{"name":"Mutation API","tags":["Mutations"]},{"name":"Define API","tags":["Skills","Brands","Personas","Documents","Campaign Plans","Line Items"]},{"name":"Assets API","tags":["Assets"]}]}