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

Make a change

Change what runs on an advertiser with the Mutations API — start a Mutation, poll it until it ends, and read the history of each change.

A change to a live advertiser is asynchronous. A provider write is slow, and it can fail halfway. So the API never does the provider write inside your request. It records your request as a Mutation, and it answers the id of that Mutation. The change runs after the answer. You poll the Mutation until the change ends.

Not every provider accepts a change. See What each provider supports.

Start a Mutation

Each change has its own route, on the path of the object that it changes. The path is the path of the live read, with /mutations in place of /observe. GET /observe/{advertiserId}/campaigns/{id} reads a campaign, and PATCH /mutations/{advertiserId}/campaigns/{id}/status changes its status. The path names the advertiser and the object. The body holds only the change. This request pauses a campaign:

curl -X PATCH https://api.adcrunch.dev/mutations/acc_1203456789012345/campaigns/120210000000000/status \
  -H "Authorization: Bearer $ADCRUNCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "PAUSED" }'

The answer is 202 Accepted. It is not the result of the change. Location holds the path of the Mutation, and the body holds its id and its status:

HTTP/1.1 202 Accepted
Location: /mutations/mut_k3x9q2v7m4n8p1r5t6w0y2z4

{ "mutationId": "mut_k3x9q2v7m4n8p1r5t6w0y2z4", "status": "running" }

The body has the shape of the answer of GET /mutations/{id}, so you parse one shape.

Before AdCrunch starts the change, it checks four things: the body, the owner of the advertiser, a usable connection, and write access. A request that fails a check starts nothing. It also adds no row to the history.

Status error Cause
400 invalid_request The body, or an id in the path, does not match the schema.
403 missing_write_access The connection of the advertiser has no write access.
404 not_found The advertiser does not exist, it belongs to a different organization, or it is not a Meta ad account. A route that does not exist, such as a budget on an ad, also answers 404.
409 provider_not_connected The advertiser has no usable connection: a person removed it, it expired, or the provider refused it. A person must connect it again.

Poll until it ends

Read the path in Location:

curl https://api.adcrunch.dev/mutations/mut_k3x9q2v7m4n8p1r5t6w0y2z4 \
  -H "Authorization: Bearer $ADCRUNCH_API_KEY"

status has one of four values:

status Meaning
running The change has not ended. Wait a few seconds, then poll again.
awaiting_confirmation A person must confirm the change in the AdCrunch console. Only a change that an AI agent started through MCP waits. A change that you start through this API never waits.
complete AdCrunch reached the provider and has its answer. Read result to learn what that answer was.
errored The change ran into a failure that AdCrunch did not expect. It cannot say whether the provider applied the change, so read the object at the provider before you send the request again.

For a create that succeeds, result holds the id of the new object. For a copy, result also holds the budget that the copy carries: { "dailyBudget": 50 } or { "lifetimeBudget": 500 } in whole units of the currency of the advertiser, "campaign" for an ad set copy that spends the budget of its campaign, or "adset" for a campaign copy whose ad sets each carry a budget. That budget is what the copy can spend after you activate it. For a schedule change that gives an ACTIVE ad set that has ended a new end time, or no end, result also holds deliveryStartsAgain of true: the ad set delivers again.

For a change to an object that exists, the answer also holds priorValue. It holds the values that the change replaced, read from the provider just before the change, with the field names and units of the action: { "dailyBudget": 800 } for a budget, { "status": "ACTIVE" } for a status, { "startTime": "2026-10-01T00:00:00+0200", "endTime": null } for a schedule with no end, { "name": "Spring Prospecting" } for a name. To undo the change, send the same kind again with these values. A create and a copy have no priorValue.

To poll, you need mutation:write, the permission that started the change.

The routes

The name of each kind starts with its provider. The route takes the same body as the action, without kind, level and id: the path gives them. In the reference, each route sits under the tag of its object, beside the live reads of that object.

kind Route, after /mutations/{advertiserId} What it does
meta_update_status PATCH /{campaigns, ad-groups or ads}/{id}/status Sets the status: ACTIVE, PAUSED, or ARCHIVED.
meta_update_name PATCH /{campaigns, ad-groups or ads}/{id}/name Sets the name.
meta_update_budget PATCH /{campaigns or ad-groups}/{id}/budget Sets the daily budget or the lifetime budget.
meta_update_bidding PATCH /{campaigns or ad-groups}/{id}/bidding Sets the bid strategy, and the amount that the strategy takes, at the level where the budget sits.
meta_update_schedule PATCH /ad-groups/{id}/schedule Sets the start time or the end time of an ad set, or removes its end time.
meta_create_campaign POST /campaigns Creates a campaign.
meta_create_adset POST /ad-groups Creates an ad set in a campaign.
meta_create_creative POST /creatives Creates a creative from an Asset that you registered to this advertiser.
meta_create_ad POST /ads Creates an ad from an ad set and a creative.
meta_copy_campaign POST /campaigns/{id}/copies Copies a campaign, with no ad set and no ad, into the same ad account. It takes no body.
meta_copy_adset POST /ad-groups/{id}/copies Copies an ad set, with no ad, into its own campaign or into another campaign. It takes an optional start time and end time.

The path uses the word of Observe, ad-groups. The kind and the body keep the word of Meta, adset.

The page of each route shows each field of its body. For example, see Update a campaign’s status and Create an ad group.

POST /mutations is deprecated. It takes { "advertiserId": "acc_…", "action": { "kind": "meta_update_status", "level": "campaign", "id": "…", "status": "PAUSED" } }, starts the same Mutation, and answers the same 202. Use the route of the kind instead.

To build an ad, create the objects in this order: the campaign, the ad set, the creative, and the ad. Each step takes the new id from the result of the step before it. The creative takes the ast_ id of an Asset that you registered to the same advertiser. See Upload a file.

A copy is shallow: it holds the one object that you copy, and none of the objects under it. Meta adds “ - Copy” to the name of the copy. To give it a full name, send meta_update_name. To copy an ad, create an ad with the creativeId of the source ad. A copy starts its own learning phase. Meta refuses to copy an Advantage+ shopping campaign or an Advantage+ app campaign, and a copy that targets the EU needs the default payor and the default beneficiary of the ad account.

A creative also needs the id of a Facebook Page. An ad set that optimizes for a conversion also needs a pixel. The REST API has no operation that lists Pages or pixels. Over MCP, the tools meta_list_pages and meta_list_pixels list them.

Rules for each write

  • A change acts only on an object of the advertiser. Before meta_update_status, meta_update_budget, meta_update_bidding, meta_update_schedule or meta_update_name writes, and before a copy, AdCrunch reads the object at Meta. When the object is in another ad account, the Mutation ends complete with result.error not_found, and nothing reaches Meta.
  • An id is the native Meta id, digits only. The path and the body refuse any other id with 400 invalid_request.
  • The API deletes nothing. To retire an object, send PATCH …/status with ARCHIVED. On Meta, you cannot make an archived object active again from this API.
  • A budget is in whole units of the currency of the advertiser. 10.5 is 10.50. A budget that you read from Observe is in the same unit. See Conventions. A bid cap and a cost cap use the same unit. A ROAS floor is a ratio: 1.5 is a purchase value of 1.5 times the spend.
  • Read the live value before you change it. GET /observe/{advertiserId}/campaigns and GET /observe/{advertiserId}/ad-groups read Meta now: the budget, the bid, and the start time and the end time of an ad set. A daily budget is not a hard limit for one day: Meta can spend up to 175% of it on one day, and up to 7 times it in one week.
  • An action takes only the fields that its schema names. No field sends a value to the provider unchanged.

Read the history

Each Mutation stays in the history of your organization. The history shows the advertiser, the change, who asked for it, and how it ended. A row of a change to an object that exists also holds its priorValue. GET /mutations lists the history, and needs mutation:read:

curl https://api.adcrunch.dev/mutations \
  -H "Authorization: Bearer $ADCRUNCH_API_KEY"

The answer holds data, at most limit rows (100 by default). When more rows exist, it also holds nextCursor. Send that value back as cursor to read the next page. The console shows the same history on the Activity page.

Was this page helpful?