---
title: "Update a campaign"
method: PUT
path: "/v1/ads/campaigns/{campaignId}"
tags: ["Ad Campaigns"]
---

# Update a campaign

`PUT /v1/ads/campaigns/{campaignId}`

## Request

- Base URL: `https://zernio.com/api`
- URL: `https://zernio.com/api/v1/ads/campaigns/{campaignId}`
- Auth: HTTP bearer

Changes a campaign's settings, such as its name, budget, bidding or tracking URLs.

Send at least one of `budget`, `bidStrategy`,
`portfolioBidStrategyId`, `targetImpressionShare`, `manualCpc`, `networkSettings`,
`trackingUrlTemplate`, `finalUrlSuffix`, `sharedBudgetId`, `name` or `platformSpecificData`. An unsupported
field is always an error, never a silent drop.

| Body field | Meta | Google | Others |
|---|---|---|---|
| `bidStrategy` | Yes | Yes | 501 |
| `bidAmount`, `roasAverageFloor` | 400 (ad-set level) | Yes | 400 |
| `portfolioBidStrategyId` | 400 | Yes | 400 |
| `targetImpressionShare` | 400 | Search only | 400 |
| `manualCpc` | 400 | Search and Display | 400 |
| `networkSettings` | 400 | Search only | 400 |
| `trackingUrlTemplate`, `finalUrlSuffix` | 400 | Yes | 400 |
| `sharedBudgetId` | 400 | Yes | 400 |
| `budget` (CBO; ABO returns 409) | Yes | Daily only | OpenAI and Reddit: daily or lifetime; others 501 |
| `name` | Yes | 501 | Reddit: Yes; others 501 |
| `platformSpecificData.spendCap` | Yes | 400 | 400 |
| `accountId` (empty campaigns) | Yes | - | - |

Meta budget edits check the live campaign budget, so an older local ABO stamp
cannot block a CBO campaign. A successful edit repairs local ad budget fields.
A live ABO campaign still returns 409 with the ad-set budget endpoint.

On Google: `LOWEST_COST_WITHOUT_CAP` = Maximize Conversions, `COST_CAP` +
`bidAmount` = Target CPA, `LOWEST_COST_WITH_MIN_ROAS` + `roasAverageFloor` =
Target ROAS, `LOWEST_COST_WITH_BID_CAP` + `bidAmount` = Maximize Clicks with a
CPC ceiling; `portfolioBidStrategyId` attaches a portfolio strategy instead
(exclusive with `bidStrategy`). `targetImpressionShare` switches the campaign to
Target impression share and `manualCpc: { maxCpc }` to Manual CPC (every ad group of the campaign gets `maxCpc` as its bid, in the same mutate); each is exclusive with
every other strategy field. `networkSettings`, `trackingUrlTemplate` and
`finalUrlSuffix` go out in one campaign update, each field sent written on its own
leaf, so an omitted one keeps its current value; an empty string clears a URL field.
Setting the standard triplet on a campaign that
is currently on a PORTFOLIO strategy is rejected: detach it in Google Ads
first, since it is shared across campaigns.

Google budget updates read the current budget before mutation. Shared budgets return
409 unless allowSharedBudgetUpdate=true is explicitly supplied, because the change
affects every campaign using that budget. A budget counts as shared when Google flags
it as shared or when more than one campaign uses it (older budgets can serve several
campaigns without the flag). Unknown sharing state also returns 409.

`sharedBudgetId` (Google) moves the campaign onto a shared budget from
GET /v1/ads/shared-budgets, which also needs `allowSharedBudgetUpdate: true` (409
otherwise) because the campaign then splits that budget with every campaign on it;
`budget` cannot ride along. `sharedBudgetId: null` moves it back onto a new budget of its
own, sized by `budget` (required, daily); the new budget and the switch go out in one
atomic Google mutate. A campaign that already has its own budget returns 409 for null.
The budget a campaign leaves is not removed. The response carries the budget the
campaign now uses.

OpenAI Ads campaigns carry exactly one spend cap: `budget.type` daily or lifetime
replaces whichever cap the campaign had, with a minimum of 1 in the ad account's
currency (422 below it). Lifetime can switch to daily, but OpenAI never switches a
daily cap back to lifetime (422).

Reddit Ads: `budget` changes the amount of a campaign budget (Reddit campaign budget
optimization); a campaign whose ad groups carry the budget returns 409 or 422, use
PUT /v1/ads/ad-sets/{adSetId} there. Reddit cannot switch a published budget between
daily and lifetime, so `budget.type` must match the current one (422 otherwise).
`bidStrategy` is set per ad group on Reddit (PUT /v1/ads/ad-sets/{adSetId}).

`accountId` forwards the update straight to Meta for a campaign with zero ads,
which would otherwise 404; the response then carries `updated: 0`.

## Path parameters

- `campaignId` string, required

## Request body

- object
  - `platform` 'facebook' | 'instagram' | 'tiktok' | 'linkedin' | 'pinterest' | 'google' | 'twitter' | 'openai' | 'reddit', required: Required: platform campaign IDs are not globally unique.
  - `accountId` string: **Meta only.** Zernio SocialAccount id owning the ad account. Needed only for an EMPTY campaign (zero ads); ignored otherwise.
  - `bidStrategy` 'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'COST_CAP' | 'LOWEST_COST_WITH_MIN_ROAS': Meta bid strategy. Same enum applies at campaign and ad-set level; ad-set value (when set) overrides campaign-level. Cross-field rules: - `LOWEST_COST_WITHOUT_CAP` (default): auto-bid, forbids `bidAmount` and `roasAverageFloor`. - `LOWEST_COST_WITH_BID_CAP` / `COST_CAP`: require `bidAmount` (whole currency units). - `LOWEST_COST_WITH_MIN_ROAS`: requires `roasAverageFloor` (decimal multiplier, 2.0 = 2.0x). Source: facebook-business-sdk-codegen api_specs/specs/enum_types.json (`AdSet_bid_strategy`, `Campaign_bid_strategy`).
  - `bidAmount` number: **Google only.** Whole currency units (USD: 12 = $12.00). Max CPC for LOWEST_COST_WITH_BID_CAP, CPA target for COST_CAP; required for both.
  - `roasAverageFloor` number: **Google only.** Decimal ROAS multiplier (2.0 = 2.0x), required for LOWEST_COST_WITH_MIN_ROAS.
  - `portfolioBidStrategyId` string: **Google only.** Attach an existing portfolio bid strategy (numeric id from GET /v1/ads/bid-strategies) instead of setting bidStrategy. Exclusive with bidStrategy.
  - `allowSharedBudgetUpdate` boolean: Google only. Explicitly allow changing a shared campaign budget (flagged as shared by Google, or used by more than one campaign), affecting every campaign that uses it. Does not bypass an unknown sharing state. Also required to move a campaign onto a shared budget with sharedBudgetId.
  - `targetImpressionShare` GoogleTargetImpressionShare: Google only. Target impression share bidding (Search campaigns only): Google sets bids so the ads show in `location` for `percent` of eligible impressions.
    - `location` 'ANYWHERE_ON_PAGE' | 'TOP_OF_PAGE' | 'ABSOLUTE_TOP_OF_PAGE', required
    - `percent` number, required: Target share of impressions, in percent (65 = 65%). Sent to Google as location_fraction_micros (1% = 10,000).
    - `maxCpc` number, required: Max CPC bid limit, in the account's currency units. Google requires it.
  - `manualCpc` GoogleManualCpc: Google Search and Display only. Manual CPC bidding. Enhanced CPC is not offered: Google no longer lets Search and Display campaigns enable it. Exclusive with every other strategy field.
    - `maxCpc` number, required: Max CPC of the ad groups, in the account's currency units: set on the ad group created with the campaign, and on every ad group of the campaign when an update switches it to Manual CPC. Google gives an ad group without one a 0.01 bid.
  - `networkSettings` GoogleNetworkSettings: Google Search campaigns only. Which networks the campaign serves on besides Google Search. When omitted at creation, search partners are ON and the Display Network is OFF (the defaults Zernio has always used); on an update only the fields sent change.
    - `searchPartners` boolean: campaign.network_settings.target_search_network
    - `displayNetwork` boolean: campaign.network_settings.target_content_network (Search with Display expansion)
  - `trackingUrlTemplate` string: **Google only.** campaign.tracking_url_template; an empty string clears it.
  - `finalUrlSuffix` string: **Google only.** campaign.final_url_suffix; an empty string clears it.
  - `sharedBudgetId` string, nullable: **Google only.** Move the campaign onto this shared budget (id from GET /v1/ads/shared-budgets), or null to move it back onto a budget of its own sized by `budget`.
  - `budget` object: Meta CBO, Google daily, or OpenAI Ads or Reddit Ads daily or lifetime campaign budget, in whole currency units.
    - `amount` number, required: Budget amount in the ad account's currency
    - `type` 'daily' | 'lifetime', required
  - `name` string: **Meta and Reddit.** Rename the campaign.
  - `platformSpecificData` object: **Meta only.** Platform implied by the `platform` body param, same convention as POST /v1/ads/create.
    - `spendCap` number, nullable: Campaign lifetime spend cap, in the ad account's currency (Meta `spend_cap`). Pass null to remove the cap; 0 is rejected by Meta.

## Response `200`

Campaign updated

- object: Echoes back only the fields you sent, plus `updated`.
  - `updated` integer: Local Ad documents mirrored. 0 on the empty-campaign path.
  - `budget` AdCampaignBudget: Budget amount in the ad account's native currency (see the campaign's `currency` field for the code).
    - `amount` number, required
    - `type` 'daily' | 'lifetime', required
    - `amountMicros` string: Google only. Exact decimal micros; DAILY uses amount_micros and CUSTOM_PERIOD uses total_amount_micros.
    - `explicitlyShared` boolean, nullable: Google only. True for a shared budget; null when unavailable. Shared writes require allowSharedBudgetUpdate=true; unknown sharing status cannot be overridden.
    - `resourceName` string, nullable: Google only. campaign_budget.resource_name, or null when unavailable.
    - `deliveryMethod` string, nullable: Google only. campaign_budget.delivery_method, typically STANDARD, or null when unavailable.
  - `budgetLevel` 'campaign'
  - `bidStrategy` 'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'COST_CAP' | 'LOWEST_COST_WITH_MIN_ROAS': Meta bid strategy. Same enum applies at campaign and ad-set level; ad-set value (when set) overrides campaign-level. Cross-field rules: - `LOWEST_COST_WITHOUT_CAP` (default): auto-bid, forbids `bidAmount` and `roasAverageFloor`. - `LOWEST_COST_WITH_BID_CAP` / `COST_CAP`: require `bidAmount` (whole currency units). - `LOWEST_COST_WITH_MIN_ROAS`: requires `roasAverageFloor` (decimal multiplier, 2.0 = 2.0x). Source: facebook-business-sdk-codegen api_specs/specs/enum_types.json (`AdSet_bid_strategy`, `Campaign_bid_strategy`).
  - `bidAmount` number
  - `roasAverageFloor` number
  - `portfolioBidStrategyId` string: Google only. Echoed back, but NOT mirrored onto local Ad documents (no column for it yet).
  - `targetImpressionShare` GoogleTargetImpressionShare: Google only. Target impression share bidding (Search campaigns only): Google sets bids so the ads show in `location` for `percent` of eligible impressions.
    - `location` 'ANYWHERE_ON_PAGE' | 'TOP_OF_PAGE' | 'ABSOLUTE_TOP_OF_PAGE', required
    - `percent` number, required: Target share of impressions, in percent (65 = 65%). Sent to Google as location_fraction_micros (1% = 10,000).
    - `maxCpc` number, required: Max CPC bid limit, in the account's currency units. Google requires it.
  - `manualCpc` GoogleManualCpc: Google Search and Display only. Manual CPC bidding. Enhanced CPC is not offered: Google no longer lets Search and Display campaigns enable it. Exclusive with every other strategy field.
    - `maxCpc` number, required: Max CPC of the ad groups, in the account's currency units: set on the ad group created with the campaign, and on every ad group of the campaign when an update switches it to Manual CPC. Google gives an ad group without one a 0.01 bid.
  - `networkSettings` GoogleNetworkSettings: Google Search campaigns only. Which networks the campaign serves on besides Google Search. When omitted at creation, search partners are ON and the Display Network is OFF (the defaults Zernio has always used); on an update only the fields sent change.
    - `searchPartners` boolean: campaign.network_settings.target_search_network
    - `displayNetwork` boolean: campaign.network_settings.target_content_network (Search with Display expansion)
  - `trackingUrlTemplate` string
  - `finalUrlSuffix` string
  - `sharedBudgetId` string, nullable: Google only. Echoed back when the campaign moved budgets; `budget` is then the budget it now uses.
  - `platformSpecificData` object

## Other responses

- `400`: Invalid input, or a field the resolved platform does not support at the campaign level (see the support table)
- `401`: Missing or invalid API key. `code` is `missing_credentials` when no Authorization header was sent and `invalid_credentials` when the key is unknown, revoked or expired.
- `403`: Returned with code `ads_allowance_exceeded` when the team has no payment method on file and has reached the 500 free live ads: add a card to resume. Also returned with code `ad_account_not_selected` when the connection has selected ad accounts and the ad account this request targets is not among them; `details` carries `accountId`, `adAccountId` and `selectedAdAccountIds`. Add the ad account to the selection (GET /v1/connect/{platform}/ads with adAccountIds, which replaces the selection) or use a connection that has it selected.
- `404`: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
- `409`: Meta campaign is ABO, or the Google budget is shared without allowSharedBudgetUpdate=true, or sharing state cannot be verified. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
- `501`: Operation not supported on this platform

## Changes

- **2026-10-13** `27c96850cc66`: 6 info
  - added the new `linkedin` enum value to the request property `platform`
  - added the new `openai` enum value to the request property `platform`
  - added the new `pinterest` enum value to the request property `platform`
  - added the new `reddit` enum value to the request property `platform`
  - …2 more
- **2026-10-07** `664b218652be`: 12 info
  - added the new optional request property `finalUrlSuffix`
  - added the new optional request property `manualCpc`
  - added the new optional request property `networkSettings`
  - added the new optional request property `sharedBudgetId`
  - …8 more
- **2026-10-06** `f8c8f34fb41a`: 2 warning
  - added the new `account` enum value to the `details/budgetScope` response property for the response status `401`
  - added the new `account` enum value to the `details/budgetScope` response property for the response status `404`
- **2026-10-02** `d556591af3f6`: 8 info
  - added the optional property `code` to the response with the `401` status
  - added the optional property `details` to the response with the `401` status
  - added the optional property `docUrl` to the response with the `401` status
  - added the optional property `docUrl` to the response with the `404` status
  - …4 more
- **2026-09-30** `16a7b9d5373e`: 1 info
  - removed the `platform` enum value from the `details/budgetScope` response property for the response status `404`

[Operation history](https://skmtc.dev/zernio/apis/zernio-api/changes/v1/ads/campaigns/:campaignId/put.md)

---

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