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_scheduleormeta_update_namewrites, and before a copy, AdCrunch reads the object at Meta. When the object is in another ad account, the Mutation endscompletewithresult.errornot_found, and nothing reaches Meta. - An id is the native Meta id, digits only. The path and the body refuse any other id with
400invalid_request. - The API deletes nothing. To retire an object, send
PATCH …/statuswithARCHIVED. 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.5is 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.5is a purchase value of 1.5 times the spend. - Read the live value before you change it.
GET /observe/{advertiserId}/campaignsandGET /observe/{advertiserId}/ad-groupsread 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.