URL: /code/apps/ads/update-campaign · Id: `page:/code/apps/ads/update-campaign`

# Campaign.update

> Pause a Campaign, or change its budget or end date, in one call.

`POST https://api.sb/{your-domain}/campaigns/update`

Pause a [Campaign](https://startupbuilder.dev/business/nouns/campaign), or change its budget or end date, in one call.

- **Action class:** `spend`
- **Approval:** Asks above your rule
- **Emits:** `Campaign.updated`
- **Sites:** [10 sites](#on-each-venue)
- **Price:** Free

```text title="Run it · Ask"
Pause the LinkedIn test: it has spent $61.20 for one lead, over the $40 stop rule.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Campaign.update({ campaign: 'campaign_Li4tX9pQ2w', state: 'paused', reason: 'Cost per lead $61.20 after $61.20 spent, over the stop rule of $40 after $60.' })
```

```json title="Run it · Response"
{
  "api": "https://api.sb/leaseline.app",
  "campaign": {
    "$id": "https://api.sb/leaseline.app/campaigns/campaign_Li4tX9pQ2w",
    "$type": "Campaign",
    "venue": "linkedin",
    "state": "paused",
    "budget": {
      "amount": 100,
      "currency": "USD"
    },
    "endsAt": "2026-11-04T13:00:00Z"
  },
  "changed": [
    "state"
  ],
  "bill": null,
  "action": {
    "operation": "Campaign.update",
    "mode": "try",
    "status": "done"
  }
}
```

Pauses a Campaign, or changes its budget, bid or end date on your ad account, inside your standing approval. This Verb is a sketch on the hub’s [Campaign](https://startupbuilder.dev/business/nouns/campaign) Noun.</HubDefinition>

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `campaign` | `Campaign` | Required | The Campaign to change. |
| `state` | `'live' \| 'paused' \| 'ended'` | `null` | `paused` stops delivery and keeps every Ad; `ended` closes it for good. Unset: unchanged. |
| `budget` | `money` | `null` | The new total for its whole window, never per day. Unset: unchanged. |
| `endsAt` | `datetime` | `null` | The new end. Unset: unchanged. |
| `targeting` | `Targeting` | `null` | New targeting, in the same account-neutral fields `Campaign.create` takes. Unset: unchanged. |
| `keywords` | `string[]` | `null` | For a search ad account: the full new list of searches. Unset: unchanged. |
| `negatives` | `string[]` | `null` | For a search ad account: searches to stop showing on, added to the ones it has, such as those that spent with no signup. Unset: unchanged. |
| `stopRule` | `string` | `null` | A new stop rule. Unset: unchanged. |
| `reason` | `string` | `null` | Why, in one sentence. It is kept on the Action and shown in the Play’s readout. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `campaign` | `Campaign` | Never null. |
| `campaign.state` | `string` | Never null: the state after the change. |
| `campaign.budget` | `money`, in USD | Never null: the budget after the change. |
| `campaign.endsAt` | `datetime` | Never null: the end after the change. |
| `bill` | `Bill` | Null unless the budget went up: then the Bill for the difference, owed for the ad spend. |

## Effects
**Action class**

`spend`: an approval rule on `spend` can make it wait for your OK

**Modes**

- `try`: Validates the change on the ad account and returns the Campaign as it would stand; nothing changes.
- `send`: Returns a pending Action; `Campaign.updated` fires when the ad account confirms the change.
- `do`: Waits for the ad account to confirm and returns the changed Campaign.

**Idempotency**

Only with a key: the same `Idempotency-Key` and input return the first Action, and the same key with other input returns `409`. A pause or a cut is safe to repeat: a Campaign already in that state returns unchanged.

**Saved or shared**

Each change is a new Version of the Campaign, with its reason. A cut lowers the open Bill; a raise books a Bill for the difference.

**Emits**

`Campaign.updated` with `{ campaign, changed, bill, reason }`

**Performed by**

[Paid Media Manager](https://startupbuilder.dev/agents/marketing/paid-media-manager)

**Price**

Free on your own account. The spend is billed by the site directly: Google, LinkedIn, Meta, Microsoft, Reddit, TikTok, X.

**Standing approval**

It asks you first above its seat rule, unless a standing approval covers it: a [`Mandate`](https://startupbuilder.dev/business/nouns/mandate) you grant once with [`Mandate.authorize`](/code/integrations/calls/authorize).

## On each Venue
| Site | Fields | Formats |
| --- | --- | --- |
| [Google Search and Google Display and YouTube](#google-ads) | `objective`, `budget`, `keywords`, `negatives` | 2 |
| [LinkedIn](#linkedin-ads) | `objective`, `budget`, `endsAt`, `state`, `targeting` | 3 |
| [Facebook and Instagram](#meta-ads) | `objective`, `budget`, `state` | 5 |
| [Bing](#microsoft-ads) | `objective`, `budget`, `state`, `negatives`, `bid` | 1 |
| [Reddit](#reddit-ads) | `objective`, `budget`, `endsAt`, `state`, `bid` | 3 |
| [TikTok](#tiktok-ads) | `objective`, `budget`, `endsAt`, `state` | 2 |
| [X](#x-ads) | `objective`, `budget`, `endsAt`, `state`, `bid` | 2 |

### Google Search and Google Display and YouTube
Through [Google Ads](/code/integrations/google-ads).

| Our field | Google Ads’s field |
| --- | --- |
| `Campaign.objective` | `campaign.advertising_channel_type` (With a conversion goal; `leads` runs a SEARCH campaign bidding toward your signup conversion) |
| `Campaign.budget` | `campaign_budget.amount_micros` (A daily amount in millionths: $40 is 40000000) |
| `Campaign.keywords` | `ad_group_criterion.keyword` (match_type EXACT for a test) |
| `Campaign.negatives` | `campaign_criterion.negative` (With the wasted search term as its keyword) |

- **A day’s spend.** Google may spend up to twice the daily budget on one day and holds the month to about 30.4 days of it, so your standing approval’s cap is set on the month.
- **Spend.** Your standing approval holds the cap; a budget change past it waits with `over-mandate`.

Google Search: Responsive search ad YouTube: Video

### LinkedIn
Through [LinkedIn Ads](/code/integrations/linkedin-ads).

| Our field | LinkedIn Ads’s field |
| --- | --- |
| `Campaign.objective` | `adCampaign.objectiveType` (`leads` is LEAD_GENERATION with LinkedIn’s lead form, `traffic` WEBSITE_VISIT, `conversions` WEBSITE_CONVERSION) |
| `Campaign.budget` | `adCampaign.dailyBudget or adCampaign.totalBudget` (`{ amount, currencyCode }`, the amount as a decimal string; a lifetime budget runs on lifetime pacing, which LinkedIn recommends) |
| `Campaign.endsAt` | `adCampaign.runSchedule.end` |
| `Campaign.state` | `adCampaign.status` (`draft` is DRAFT, `live` ACTIVE, `paused` PAUSED) |
| `Campaign.targeting` | `adCampaign.targetingCriteria` (Job titles, industries, staff count ranges and locations, each a LinkedIn targeting facet) |

- **Spend.** Your standing approval holds the daily and total cap; a change past it waits with `over-mandate`.

Feed post Single image ad, 1200 × 628, 1200 × 1200 or 720 × 900 Document ad

### Facebook and Instagram
Through [Meta Ads](/code/integrations/meta-ads).

| Our field | Meta Ads’s field |
| --- | --- |
| `Campaign.objective` | `campaign.objective` (`leads` is OUTCOME_LEADS, `traffic` OUTCOME_TRAFFIC, `conversions` OUTCOME_SALES) |
| `Campaign.budget` | `adset.daily_budget or adset.lifetime_budget` (In the account currency’s smallest unit: $40.00 is 4000) |
| `Campaign.state` | `campaign.status` (`live` is ACTIVE, `paused` is PAUSED) |

- **Spend.** Your standing approval holds the daily and total cap; a change past it waits with `over-mandate`.

Facebook: Feed image ad, 1440 × 1800, 4:5 Instagram: Feed image, 320 to 1,440 pixels wide, 4:5 to 1.91:1 Instagram: Carousel Instagram: Reel, 9:16 Instagram: Story, 9:16

### Bing
Through [Microsoft Ads](/code/integrations/microsoft-ads).

| Our field | Microsoft Ads’s field |
| --- | --- |
| `Campaign.objective` | `campaign goal` (Set in Microsoft Ads’ own screens; `leads` bids toward the signup conversion the import brings over) |
| `Campaign.budget` | `campaign daily budget` |
| `Campaign.state` | `campaign status` (`live` is Active, `paused` Paused) |
| `Campaign.negatives` | `campaign negative keywords` |
| `Campaign.bid` | `ad group default bid, or the bid strategy` (`lowest-cost` is maximize clicks; a `costPer` cap is a manual CPC bid) |

- **Spend.** Your standing approval holds the cap; a budget change past it waits with `over-mandate`.

Responsive search ad

### Reddit
Through [Reddit Ads](/code/integrations/reddit-ads).

| Our field | Reddit Ads’s field |
| --- | --- |
| `Campaign.objective` | `campaign objective` (Picked in Ads Manager: `traffic` sends readers to your page; `leads` bids toward your signup event) |
| `Campaign.budget` | `ad group budget` |
| `Campaign.endsAt` | `ad group end date and time` |
| `Campaign.state` | `campaign and ad group status` (`live` is active, `paused` paused) |
| `Campaign.bid` | `ad group bid strategy` (`lowest-cost` is automatic bidding; a `costPer` cap is a cost cap on clicks or conversions) |

- **Comments on a promoted post.** Stay open; each one reaches you as a Message, and your agent drafts the answer.
- **Spend.** Your standing approval holds the cap; a change past it waits with `over-mandate`.

Text or link post Comment Promoted post

### TikTok
Through [TikTok Ads](/code/integrations/tiktok-ads).

| Our field | TikTok Ads’s field |
| --- | --- |
| `Campaign.objective` | `campaign objective_type` (`traffic` is TRAFFIC; `leads` is LEAD_GENERATION with TikTok’s instant form; `conversions` is WEB_CONVERSIONS on your pixel) |
| `Campaign.budget` | `adgroup budget with budget_mode BUDGET_MODE_DAY` (Over $20; the campaign budget stays unset unless you ask, and then it is over $50) |
| `Campaign.endsAt` | `adgroup schedule_end_time` (Set with schedule_type SCHEDULE_START_END) |
| `Campaign.state` | `adgroup operation_status` (`live` is ENABLE, `paused` DISABLE) |

- **Spend.** Your standing approval holds the cap; a change past it waits with `over-mandate`.

Video Spark Ad

### X
Through [X Ads](/code/integrations/x-ads).

| Our field | X Ads’s field |
| --- | --- |
| `Campaign.objective` | `campaign objective` (Picked in X Ads: `traffic` is website traffic; `engagement` is post engagement) |
| `Campaign.budget` | `campaign total and daily budget` |
| `Campaign.endsAt` | `ad group end time` |
| `Campaign.state` | `campaign status` (`live` is active, `paused` paused) |
| `Campaign.bid` | `ad group bid strategy` (`lowest-cost` is autobid; a `costPer` cap is a target cost) |

- **Spend.** Your standing approval holds the cap; a change past it waits with `over-mandate`.

Standard post Promoted post

## What try does
| Account | What try does |
| --- | --- |
| [Google Ads](/code/integrations/google-ads#try) | Sends every change with `validate_only` set: Google Ads checks the whole build and returns any error, and creates nothing. |
| [LinkedIn Ads](/code/integrations/linkedin-ads#try) | Reads your ad account, the matched count of each audience and the targeting facets, and checks the campaign against LinkedIn’s budget minimums; LinkedIn has no validate-only call, so nothing is sent to it and nothing is created. |
| [Meta Ads](/code/integrations/meta-ads#try) | Sends the whole build with Meta’s `validate_only` execution option: Meta runs every check and creates nothing, and the response names any field it would reject. |
| [Microsoft Ads](/code/integrations/microsoft-ads#try) | Returns what the import would bring over, term by term with the budget it would set, and the steps the browser agent would take; no session opens and nothing changes. |
| [Reddit Ads](/code/integrations/reddit-ads#try) | Returns the campaign and the promoted post as Ads Manager will hold them, with the steps the browser agent would take; no session opens and nothing is created. |
| [TikTok Ads](/code/integrations/tiktok-ads#try) | Returns the Spark Ad and its ad group as Ads Manager will hold them, checked against TikTok’s budget floors, with the steps the browser agent would take; nothing is created. |
| [X Ads](/code/integrations/x-ads#try) | Returns the promotion as X Ads will hold it, with the steps the browser agent would take; no session opens and nothing is promoted. |

## Refuses when
| Check | Error | Fix | Then call |
| --- | --- | --- | --- |
| A landing page fails its status or Price check | [`landing`](/code/refusals#stop-landing) | Fix what the reason names, then turn the Play back on. | [`Campaign.update`](/code/apps/ads/update-campaign) |

Every call through an account can also meet [the shared account refusals](/code/refusals#accounts): [`grant-missing`](/code/refusals#grant-missing), [`needs-reauth`](/code/refusals#needs-reauth), [`account-not-connected`](/code/refusals#account-not-connected), [`over-mandate`](/code/refusals#over-mandate), [`quota-reached`](/code/refusals#quota-reached), [`venue-rejected`](/code/refusals#venue-rejected).

```json
{
  "type": "https://startupbuilder.dev/errors/landing",
  "title": "Landing",
  "status": 422,
  "detail": "A landing Page fails its status or Price check.",
  "fix": "Fix what the reason names, then turn the Play back on.",
  "next": {
    "verb": "Campaign.update",
    "input": {}
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

## When it waits
| Fact | Value |
| --- | --- |
| `send` returns | A pending Action until the ad account confirms the change; a raise over your approval rule waits for an Approval first. |
| Delivered by | `Campaign.updated` |

## Example

**Ask**

```text
Pause the LinkedIn test: it has spent $61.20 for one lead, over the $40
stop rule.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/campaigns/update?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"campaign":"campaign_Li4tX9pQ2w","state":"paused","reason":"Cost per lead $61.20 after $61.20 spent, over the stop rule of $40 after $60."}'
```

The response is the one in the Run it window above.

**CLI**

```bash
npx startup-builder do Campaign.update --try --data '{"campaign":"campaign_Li4tX9pQ2w","state":"paused","reason":"Cost per lead $61.20 after $61.20 spent, over the stop rule of $40 after $60."}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Campaign.update({ campaign: 'campaign_Li4tX9pQ2w', state: 'paused', reason: 'Cost per lead $61.20 after $61.20 spent, over the stop rule of $40 after $60.' })"
  }
}
```

**SDK**

```ts
import { $ } from 'startup-builder'

const result = await $.try.Campaign.update({
  campaign: 'campaign_Li4tX9pQ2w',
  state: 'paused',
  reason: 'Cost per lead $61.20 after $61.20 spent, over the stop rule of $40 after $60.',
})
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/data#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `campaign_test_linkedin` | Refuses with [`over-mandate`](/code/refusals#over-mandate) | A raise to $999,999 is over the Limit of every test standing approval, so it refuses and the test Campaign keeps its budget. |
| `maxCost: 0.0001` | Refuses with [`max-cost`](/code/refusals#max-cost) | Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
| `rate-limited.test` | Refuses with [`rate-limited`](/code/refusals#rate-limited) | Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
| `sources-busy.test` | Refuses with [`sources-busy`](/code/refusals#sources-busy) | Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |

## Used by
- **Jobs:** [Run ads](/capabilities/run-ads)
- **Integrations:** [Google Ads](/code/integrations/google-ads) · [LinkedIn Ads](/code/integrations/linkedin-ads) · [Meta Ads](/code/integrations/meta-ads) · [Microsoft Ads](/code/integrations/microsoft-ads) · [Reddit Ads](/code/integrations/reddit-ads) · [TikTok Ads](/code/integrations/tiktok-ads) · [X Ads](/code/integrations/x-ads)

## Related
[`Audience.create`](/code/apps/ads/create-audience) · [`Campaign.create`](/code/apps/ads/create-campaign) · [`Ad.create`](/code/apps/ads/create-ad)