---
title: "Start a mutation"
method: POST
path: "/mutations"
tags: ["Mutations"]
---

# Start a mutation

`POST /mutations`

## Request

- Base URL: `https://api.adcrunch.dev`
- URL: `https://api.adcrunch.dev/mutations`
- Auth: HTTP bearer

**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers)

Starts a change on a live advertiser and answers immediately.

The change does **not** happen during this request. The answer carries a `workflowId`. Poll `GET /mutations/{id}` with it to find out how the change ended. A Mutation is durable, so the id stays valid across retries and restarts.

A refused request starts nothing. AdCrunch refuses a body that does not match, an advertiser that is not yours, an advertiser with no usable connection, and an advertiser connected without write access before a Mutation exists. So a failed call leaves no row in the history.

**This operation checks the credential first and the body second.** A missing or unusable credential answers `401` even when the body is wrong as well. A body that does not match then answers `400` with `error` of `invalid_request`, and `issues` names each field.

A create never starts delivery: a campaign, an ad set and an ad all arrive **paused**, and so does a copy of a campaign or an ad set. This API also cannot delete, so `meta_set_status` with `ARCHIVED` is how an object is retired.

## Request body

- object
  - `action` union, required: What to do. The kind field selects the action, and each kind takes its own fields.
    - object
      - `id` string, required: The Meta id of the object to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found.
      - `kind` 'meta_set_status', required
      - `level` 'campaign' | 'adset' | 'ad', required: Which object the id refers to.
      - `status` 'ACTIVE' | 'PAUSED' | 'ARCHIVED', required: 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.
    - object: Change the daily or the lifetime budget of a campaign with Advantage campaign budget, or of an ad set. Before you change a budget, read its live amount: GET /observe/{advertiserId}/campaigns and GET /observe/{advertiserId}/ad-groups read Meta now. A daily budget is not a hard limit for one day: Meta can spend up to 175% of it on one day, and up to 7 times it in one week.
      - `dailyBudget` number: The amount to spend each day. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give a daily budget or a lifetime budget, not both. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time.
      - `id` string, required: The Meta id of the campaign or ad set to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found.
      - `kind` 'meta_update_budget', required
      - `level` 'campaign' | 'adset', required: Which object the id refers to. An ad has no budget of its own.
      - `lifetimeBudget` number: The amount to spend across the whole schedule. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. A lifetime budget needs an end time on the ad set. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time.
    - object
      - `endTime` string, nullable: When delivery stops, as an ISO 8601 timestamp in the future, for example 2026-12-31T23:59:00+01:00. Send null for no end. An ad set with a lifetime budget must keep an end time. A later end time adds days of spend, so the total spend of a daily budget increases. With no end, the ad set spends until you pause it, up to 7 times its daily budget in one week. With a lifetime budget, the total spend stays within the lifetime budget. An ACTIVE ad set that has ended delivers again when it gets a new end time or no end, with no status change. To change a daily budget to a lifetime budget, first set an end time with meta_update_schedule, then set the lifetime budget. To change a lifetime budget to a daily budget, first set the daily budget, then remove the end time with meta_update_schedule. Meta refuses a lifetime budget with no end time. To undo a schedule change, send the times that it replaced (priorValue). An end time that is not in the future cannot go back: Meta refuses it, and AdCrunch refuses it too. To stop an ad set after you extend its end, set its status to PAUSED with meta_set_status.
      - `id` string, required: The Meta id of the ad set to change: the native id, digits only, not an AdCrunch id. The ad set must be in the ad account of the advertiser, else the change ends with not_found. A campaign has no schedule that this API can change.
      - `kind` 'meta_update_schedule', required
      - `startTime` string: When delivery starts, as an ISO 8601 timestamp. Meta gets it as you send it, also on an ad set that has started, and Meta decides what it does. Give startTime, endTime, or both.
    - object
      - `adsetBudgetSharing` boolean: Let the ad sets share their budgets. Only when both campaign budgets are omitted; false when omitted. Each ad set can then share up to 20 % of its daily budget with the other ad sets of the campaign. Sharing works with daily budgets only. It needs a bidStrategy, and it locks that bid strategy for the life of the campaign. Sharing cannot be turned on later.
      - `bidStrategy` 'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'COST_CAP' | 'LOWEST_COST_WITH_MIN_ROAS': How Meta bids. LOWEST_COST_WITHOUT_CAP ("Highest volume" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP ("Bid cap") takes bidCap. COST_CAP ("Cost per result goal") takes costCap. LOWEST_COST_WITH_MIN_ROAS ("ROAS goal") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. On a campaign, give only the strategy. It applies when the campaign carries a budget, or when adsetBudgetSharing is true, which requires it. Give the amount of a bid cap, a cost cap or a ROAS goal on each ad set, with the same bidStrategy. Omit it to let Meta apply its default strategy.
      - `dailyBudget` number: The amount the campaign spends each day, shared across its ad sets. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give one of the two budgets, or neither. When each ad set carries its own budget, for example one ad set for each Line Item, omit both budgets.
      - `kind` 'meta_create_campaign', required
      - `lifetimeBudget` number: The amount the campaign spends across its whole schedule, shared across its ad sets. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Give one of the two budgets, or neither. When each ad set carries its own budget, for example one ad set for each Line Item, omit both budgets.
      - `name` string, required: The campaign name. It is visible in Meta Ads Manager.
      - `objective` 'OUTCOME_AWARENESS' | 'OUTCOME_ENGAGEMENT' | 'OUTCOME_LEADS' | 'OUTCOME_SALES' | 'OUTCOME_TRAFFIC' | 'OUTCOME_APP_PROMOTION', required: 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` string[], required: 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.
    - object
      - `ageMax` integer: 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.
      - `bidCap` number: The highest bid Meta places in an auction. Only with LOWEST_COST_WITH_BID_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has.
      - `costCap` number: The average cost per result that Meta tries to keep. Only with COST_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has. A cost cap can stop the ad set before it spends its full budget.
      - `roasFloor` number: The lowest return on ad spend that Meta bids for, as a ratio: 1.5 means purchase value of 1.5 times the spend. Only with LOWEST_COST_WITH_MIN_ROAS. From 0.01 to 1000, with at most 4 decimals.
      - `bidStrategy` 'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'COST_CAP' | 'LOWEST_COST_WITH_MIN_ROAS': How Meta bids. LOWEST_COST_WITHOUT_CAP ("Highest volume" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP ("Bid cap") takes bidCap. COST_CAP ("Cost per result goal") takes costCap. LOWEST_COST_WITH_MIN_ROAS ("ROAS goal") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. When the campaign carries the budget, or shares the ad set budgets, the campaign holds the strategy: give the same bidStrategy here, with the amount for this ad set. Omit it, with every amount, to let Meta apply its default strategy.
      - `ageMin` integer, required: 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` string, required: The Meta id of the campaign this ad set belongs to, digits only. Create the campaign first and use the id it returns.
      - `countries` string[], required: The countries to target, as two-letter ISO 3166-1 alpha-2 codes, for example US or GB. Give at least one.
      - `customEventType` 'PURCHASE' | 'LEAD' | 'COMPLETE_REGISTRATION' | 'ADD_TO_CART' | 'INITIATED_CHECKOUT' | 'ADD_PAYMENT_INFO' | 'VIEW_CONTENT' | 'SEARCH' | 'SUBSCRIBE' | 'START_TRIAL' | 'CONTACT' | 'OTHER': The conversion the pixel reports, for example PURCHASE. Required with a conversion optimization goal, and it must be sent together with pixelId.
      - `dailyBudget` number: The amount this ad set spends each day. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. Omit both budgets when the campaign carries one. When the campaign carries none, give exactly one.
      - `endTime` string: When delivery stops, as an ISO 8601 timestamp. Required with a lifetime budget.
      - `genders` 'all' | 'men' | 'women', required: Which genders to target. The default targets everybody.
      - `kind` 'meta_create_adset', required
      - `lifetimeBudget` number: The amount this ad set spends across its whole schedule. The amount is in whole units of the ad account currency: 10.5 is 10.50. Meta refuses more decimals than the currency has, and it counts some currencies, such as JPY and HUF, with none. It needs an end time.
      - `name` string, required: The ad set name. It is visible in Meta Ads Manager.
      - `optimizationGoal` 'IMPRESSIONS' | 'REACH' | 'LINK_CLICKS' | 'LANDING_PAGE_VIEWS' | 'OFFSITE_CONVERSIONS' | 'POST_ENGAGEMENT' | 'THRUPLAY' | 'LEAD_GENERATION' | 'VALUE', required: 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` string: The Meta pixel that reports conversions. Required with a conversion optimization goal. AdCrunch can list the pixels this ad account can use.
      - `startTime` string: 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.
    - object
      - `adsetId` string, required: The Meta id of the ad set this ad runs in, digits only. Create the ad set first and use the id it returns.
      - `creativeId` string, required: The Meta id of the creative this ad shows, digits only. Create the creative first with meta_create_creative and use the id it returns. One creative can be used by more than one ad.
      - `kind` 'meta_create_ad', required
      - `name` string, required: The ad name. It is visible in Meta Ads Manager.
    - object
      - `assetId` string, required: 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` 'LEARN_MORE' | 'SHOP_NOW' | 'SIGN_UP' | 'BOOK_TRAVEL' | 'DOWNLOAD' | 'GET_OFFER' | 'GET_QUOTE' | 'CONTACT_US' | 'SUBSCRIBE' | 'APPLY_NOW' | 'NO_BUTTON', required: The label on the button, for example SHOP_NOW.
      - `description` string: The text below the headline. Meta can truncate it.
      - `headline` string, required: The short bold line next to the button.
      - `kind` 'meta_create_creative', required
      - `link` string, uri, required: The page the ad opens when somebody clicks it.
      - `message` string, required: The main body text, shown above the media.
      - `name` string, required: The creative name. It is visible in Meta Ads Manager, and it is not shown to a person who sees the ad.
      - `pageId` string, required: The Facebook Page the ad is published from. AdCrunch can list the Pages this ad account can publish from.
    - object
      - `bidCap` number: The highest bid Meta places in an auction. Only with LOWEST_COST_WITH_BID_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has.
      - `costCap` number: The average cost per result that Meta tries to keep. Only with COST_CAP. A bid is per optimization event, for example per purchase, and per 1,000 impressions when the optimization goal is IMPRESSIONS or REACH. The amount is in whole units of the ad account currency: 2.5 is 2.50. Meta refuses more decimals than the currency has. A cost cap can stop the ad set before it spends its full budget.
      - `roasFloor` number: The lowest return on ad spend that Meta bids for, as a ratio: 1.5 means purchase value of 1.5 times the spend. Only with LOWEST_COST_WITH_MIN_ROAS. From 0.01 to 1000, with at most 4 decimals.
      - `bidStrategy` 'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'COST_CAP' | 'LOWEST_COST_WITH_MIN_ROAS', required: How Meta bids. LOWEST_COST_WITHOUT_CAP ("Highest volume" in Ads Manager) gets the most results for the budget and takes no amount. LOWEST_COST_WITH_BID_CAP ("Bid cap") takes bidCap. COST_CAP ("Cost per result goal") takes costCap. LOWEST_COST_WITH_MIN_ROAS ("ROAS goal") takes roasFloor, and needs the optimization goal VALUE. Give exactly the amount that the strategy takes, and no other amount. A higher bid cap, a higher cost cap or a lower ROAS floor can make an ad set spend its full budget, never more. Send the strategy also to change only an amount. A change of strategy starts the learning phase again, and a large change of amount can too. Meta refuses a change of strategy on a campaign with ad set budget sharing, on a campaign budget with more than 70 ad sets, and on a campaign budget on ROAS goal. Meta advises at most 2 or 3 bid or budget changes a day, and 15 minutes between a budget change and a bid change.
      - `id` string, required: The Meta id of the campaign or ad set to change: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the change ends with not_found.
      - `kind` 'meta_update_bidding', required
      - `level` 'campaign' | 'adset', required: Which object the id refers to. The strategy sits where the budget sits: on the campaign under a campaign budget, otherwise on each ad set. Under a campaign budget, an ad set takes only its amount: send the bidStrategy of the campaign with the amount for this ad set. Another strategy ends with wrong_level. A campaign without a campaign budget ends with wrong_level. On a campaign, a bid cap or a cost cap applies one amount to every ad set of the campaign. A ROAS floor sits on each ad set: on a campaign, LOWEST_COST_WITH_MIN_ROAS is refused.
    - object: Copy a campaign. The copy has no ad set and no ad: create them with meta_copy_adset or meta_create_adset. A copy is shallow: Meta copies this one object with its settings, and none of the objects under it. The copy starts paused, and Meta adds " - Copy" to its name. To give the copy a full name, use `meta_rename`. The result gives the id of the copy and the budget that the copy carries, which is what an activate can spend. A copy starts its own learning phase. Meta refuses some copies: an Advantage+ shopping campaign and an Advantage+ app campaign cannot be copied, and a copy that targets the EU needs the default payor and the default beneficiary of the ad account.
      - `id` string, required: The Meta id of the campaign to copy: the native id, digits only, not an AdCrunch id. The campaign must be in the ad account of the advertiser, else the copy ends with not_found. The copy stays in that ad account.
      - `kind` 'meta_copy_campaign', required
    - object: Copy an ad set. The copy has no ad: create its ads with meta_create_ad, with the creativeId of each source ad. A copy is shallow: Meta copies this one object with its settings, and none of the objects under it. The copy starts paused, and Meta adds " - Copy" to its name. To give the copy a full name, use `meta_rename`. The result gives the id of the copy and the budget that the copy carries, which is what an activate can spend. A copy starts its own learning phase. Meta refuses some copies: an Advantage+ shopping campaign and an Advantage+ app campaign cannot be copied, and a copy that targets the EU needs the default payor and the default beneficiary of the ad account.
      - `campaignId` string: The Meta id of the campaign that receives the copy, digits only. Omit it to keep the copy in the campaign of the source. The copy takes the settings of that campaign: under a campaign budget, it spends the campaign budget, else it keeps the budget of the source. An ad set with no budget of its own needs a campaign with a campaign budget, else the copy ends with invalid_request. Meta decides whether the campaign accepts the copy.
      - `endTime` string: When delivery of the copy stops, as an ISO 8601 timestamp. Omit it to keep the end time of the source.
      - `id` string, required: The Meta id of the ad set to copy: the native id, digits only, not an AdCrunch id. The ad set must be in the ad account of the advertiser, else the copy ends with not_found.
      - `kind` 'meta_copy_adset', required
      - `startTime` string: When delivery of the copy starts, as an ISO 8601 timestamp. Omit it to keep the start time of the source. A copy of an ad set that has ended starts when Meta makes it, with the duration of the source.
    - object: Rename a campaign, an ad set or an ad. The rename changes the name and no other field. Meta names a copy with " - Copy" at the end: rename the copy to give it a full name.
      - `id` string, required: The Meta id of the object to rename: the native id, digits only, not an AdCrunch id. The object must be in the ad account of the advertiser, else the rename ends with not_found.
      - `kind` 'meta_rename', required
      - `level` 'campaign' | 'adset' | 'ad', required: Which object the id refers to.
      - `name` string, required: The new name, which replaces the whole name. It is visible in Meta Ads Manager.
  - `advertiserId` string, required: The advertiser to act on, as an AdCrunch acc_ id. It must belong to your organization and be connected with write access.

## Response `200`

The Mutation started. Poll it for the outcome.

- object
  - `workflowId` string, required: The id of the Mutation. Poll GET /mutations/{id} with it to find out whether the change succeeded.

## Other responses

- `400`: The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed.
- `401`: No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`.
- `402`: The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`.
- `403`: The caller does not hold `mutation:write`, or the advertiser is connected without write access. In the second case `error` is `missing_write_access` and `message` says how to fix it. AdCrunch started nothing.
- `404`: The advertiser does not exist, it belongs to another organization, or it is not a Meta ad account. `error` is `not_found` in each case. An advertiser that does not exist and an advertiser of another organization get the same answer on purpose, so a caller cannot learn which ids exist. AdCrunch started nothing.
- `409`: The advertiser has no usable connection: a person removed it, it expired, or the provider refused it. A person must connect it again. `error` is `provider_not_connected`, the code that a live read of Observe sends for the same failure. AdCrunch started nothing.

---

[API](https://skmtc.dev/adcrunch/apis/adcrunch-api.md) · [All operations](https://skmtc.dev/adcrunch/apis/adcrunch-api/llms.txt) · [OpenAPI document](https://skmtc.dev/adcrunch/apis/adcrunch-api/revisions/a420ba80914f?raw)
