---
title: "Update a campaign"
method: PUT
path: "/api/v1/campaigns/{id}"
tags: ["campaigns"]
---

# Update a campaign

`PUT /api/v1/campaigns/{id}`

## Request

- Base URL: `https://api.growomat.com`
- URL: `https://api.growomat.com/api/v1/campaigns/{id}`
- Auth: HTTP bearer

Partial update. Send only the fields you want to change; omitted fields keep their values, and a field you do send replaces the stored value in full (arrays included).

`adSchedule` accepts a preset name (`ALL_HOURS_ALL_DAYS`, `BUSINESS_HOURS`) or a structured schedule; `adScheduleStructured` is recomputed on every write. For human-friendly input (`09:00` to `17:00`, weekdays, a time zone) use *Set the ad schedule* instead.

Leave `_platformBudgets` out to keep where the campaign runs. Sending it replaces the whole split, and it must still switch at least one platform on. Removing a platform the campaign is live on pauses it there at the next sync.

Changes reach the ad platforms on the next sync.

**Errors**

- `404 NOT_FOUND`: no campaign with this id in the active Space.
- `409 CAMPAIGN_IS_DRAFT`: the campaign is an unconfirmed draft; it can only be confirmed in the app.
- `400 PLATFORMS_REQUIRED`: the new `_platformBudgets` switches every platform off.
- `400 UNKNOWN_PLATFORM`: a platform id in `_platformBudgets` is not one Growomat delivers to.
- `400 VALIDATION_ERROR`: the body failed validation; `details.issues[]` names each field.
- `400 AD_SCHEDULE_TOO_MANY_WINDOWS`: more than 84 schedule windows.

## Path parameters

- `id` string, required

## Headers

- `X-Space-Id` string

## Request body

- CampaignUpdate: Partial update of a campaign: any subset of `CampaignCreate` fields. Omitted fields keep their values.
  - `_native` NativeExtensions: Settings that exist on one platform only, keyed by platform id (for example `google-ads`). Each value is a free-form object that is stored and returned exactly as sent; keys Growomat does not know are kept. Absent by default. Keys read by platforms whose ad is a post (on the ad, with the campaign as fallback): `postType` (`TEXT` or `IMAGE`), `postTitle`, `postBody`, `callToAction`, `imageUrl`, `videoUrl`, `videoThumbnailUrl`, `carouselCards` (`[{ imageUrl, destinationUrl }]`), `communities`, `interests`. Delivery keys read on the campaign: `objective`, `goalType`, `startTime`, `endTime`, `optimizationGoal`, `conversionPixelId`.
  - `_platformBudgets` PlatformBudgets: Where this campaign runs, and how much of the budget each platform gets, keyed by platform id (`reddit-ads`, `microsoft-ads`, and so on). **A platform not listed here is not part of the campaign.** It is never exported to and never spends, whatever is connected. At least one entry must be switched on. An entry without `amount` gets an even share of the budget the listed amounts leave, so `{ "reddit-ads": {} }` is the whole budget on Reddit Ads. `percentage` is computed from `amount` on every write; sending it is ignored. The split is checked against the campaign budget and rejected when it exceeds it.
  - `name` string: Campaign name, shown in the app and on the platforms.
  - `budget` number: Daily budget, in `currency`.
  - `currency` string: ISO 4217 currency code of `budget`, for example `EUR` or `USD`.
  - `status` 'DRAFT' | 'ENABLED' | 'PAUSED' | 'REMOVED': `PAUSED` (the default) is safe: the campaign exists on the platforms after a sync but does not serve. `ENABLED` serves after the next sync. `DRAFT` is an unconfirmed build from the app's campaign builder and is never synced until it is confirmed.
  - `type` 'SEARCH' | 'DISPLAY' | 'VIDEO' | 'SHOPPING' | 'PERFORMANCE_MAX': The campaign type. It decides which ad formats and which asset roles apply.
  - `networks` string: Platform network setting, as the platform names it.
  - `languages` string: Language of the audience, as a language code.
  - `businessId` string: The business this campaign advertises. Generated copy and the brand voice come from it.
  - `goal` string: What the campaign should achieve, in your own words. Used when Growomat drafts ads and keywords for it.
  - `geoTargets` GeoTarget[]: Where the ads may show: named areas and radius circles in one include/exclude list. Empty means no location restriction; the ads can serve anywhere.
    - `id` string: Stable row id. Generated when omitted.
    - `kind` 'AREA' | 'RADIUS', required: `AREA` is a named place, from a country down to a postcode. `RADIUS` is a circle around a point.
    - `negative` boolean: `true` excludes this row from the included set. Not allowed on `RADIUS` rows (`400 RADIUS_EXCLUSION_UNSUPPORTED`), and a set where every row is negative is rejected (`400 ONLY_EXCLUSIONS`).
    - `bidAdjustment` number: Percent bid adjustment for this row. Included rows only.
    - `reach` number: Audience estimate as reported by the platform, when it gave one. Read-only; never estimated by Growomat.
    - `criterionId` string, nullable: Areas: the Google Ads geo target id.
    - `msLocationId` string, nullable: Areas: the Microsoft Advertising location id.
    - `canonicalId` string, nullable: Areas: Growomat's platform-independent id for the place.
    - `name` string, required: Display name of the place or circle.
    - `type` 'COUNTRY' | 'REGION' | 'METRO' | 'CITY' | 'DISTRICT' | 'POSTAL_CODE' | 'AIRPORT' | 'UNIVERSITY' | 'GLOBAL': Areas only: what kind of place this is.
    - `countryCode` string, nullable: Areas: ISO 3166-1 country code.
    - `lat` number: Circles only: latitude of the centre.
    - `lng` number: Circles only: longitude of the centre.
    - `radius` number: Circles only. Clamped to the platform's range (1 to 500) when pushed; a clamp is reported as a `RADIUS_CLAMPED_TO_MIN` warning.
    - `unit` 'km' | 'mi': Circles only: the unit of `radius`.
  - `locationIntent` 'PRESENCE' | 'PRESENCE_OR_INTEREST': Who counts as being in the targeted places. `PRESENCE` (default): people who are in, or regularly in, those places. `PRESENCE_OR_INTEREST`: also people elsewhere who search about them. The second is wider and much less qualified; it is the platforms' own default, which this API deliberately does not inherit.
  - `customLabels` string[]: Up to ten free-form labels for your own grouping and reporting.
  - `adSchedule` union: The campaign's ad schedule, as a preset name (`ALL_HOURS_ALL_DAYS`, `BUSINESS_HOURS`, `CUSTOM`) or as a `StructuredSchedule`. On read, `adSchedule` returns what you last wrote and `adScheduleStructured` always holds the expanded weekly schedule. `CUSTOM` is treated as `ALL_HOURS_ALL_DAYS`.
    - 'ALL_HOURS_ALL_DAYS' | 'BUSINESS_HOURS' | 'CUSTOM'
    - StructuredSchedule: A weekly ad schedule: up to 84 windows and the time zone they are in.
      - `windows` ScheduleWindow[], required: The windows during which ads may show.
        - `day` 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY' | 'SUNDAY', required: The day of the week.
        - `startHour` integer, required: Hour the window opens (0 to 23), in the schedule's time zone.
        - `startMinute` 'ZERO' | 'FIFTEEN' | 'THIRTY' | 'FORTY_FIVE': Minute the window opens, in quarter hours.
        - `endHour` integer, required: Hour the window closes (0 to 24). `24` means end of day.
        - `endMinute` 'ZERO' | 'FIFTEEN' | 'THIRTY' | 'FORTY_FIVE': Minute the window closes, in quarter hours.
        - `bidModifier` number: Bid multiplier for this window; `1.0` means no change. Platforms that express this as a percentage receive the converted value, clamped to their range; a clamp is reported as a `BID_ADJUSTMENT_CLAMPED` warning.
      - `timezone` string: IANA time zone the hours are in, for example `Europe/Berlin`. Defaults to the account time zone.
  - `adScheduleUseSearcherTimeZone` boolean: Interpret the ad schedule in the searcher's local time zone instead of the account time zone, on platforms that support it (Microsoft Advertising). Ignored elsewhere.
  - `biddingStrategy` BiddingStrategy: How the platform bids for this campaign.
    - `type` 'MANUAL_CPC' | 'ENHANCED_CPC' | 'MAXIMIZE_CLICKS' | 'MAXIMIZE_CONVERSIONS' | 'MAXIMIZE_CONVERSION_VALUE' | 'TARGET_CPA' | 'TARGET_ROAS' | 'TARGET_IMPRESSION_SHARE': The bidding strategy. Target strategies read their target from the matching field below.
    - `targetCpa` number: Target cost per acquisition, for `TARGET_CPA`.
    - `targetRoas` number: Target return on ad spend as a ratio (2.5 means 250%), for `TARGET_ROAS`.
    - `targetImpressionShare` number: Target impression share in percent, for `TARGET_IMPRESSION_SHARE`.
    - `maxCpcBidCeiling` number: Never bid more than this per click.
  - `targeting` CampaignTargeting: Additional targeting: schedule, devices and platform-specific audience settings. Locations live in the campaign's `geoTargets` list, not here.
    - `schedule` object[]
    - `devices` object
    - `linkedin` object
  - `trackingUrlTemplate` string: Tracking template applied to every ad in the campaign, for example `{lpurl}?src=ads`.
  - `finalUrlSuffix` string: Query string appended to every final URL in the campaign.
  - `urlCustomParameters` object: Custom `{_name}` parameters available to tracking templates.
  - `negativeKeywords` NegativeKeywordEntry[]: Search terms the whole campaign must not show for.
    - `text` string, required: The term.
    - `matchType` 'EXACT' | 'PHRASE' | 'BROAD', required: How closely a search must match the term.

## Response `200`

OK. The result is in `data`. List responses also carry `meta` with `nextCursor` and `total`.

- SuccessEnvelope: The shape of every successful response.
  - `data` unknown, required
  - `meta` object: Present on list responses.
    - `nextCursor` union: Pass as `cursor` to get the next page. `null` on the last page.
      - string
      - number
    - `total` integer: Number of items in this page.

## Other responses

- `400`: The request is malformed or fails validation. `error.code` is `VALIDATION_ERROR` (with `details.issues[]` naming each field), `INVALID_REQUEST` (the body is not valid JSON or has the wrong shape), or one of the specific codes listed in this operation's description.
- `401`: The bearer token is missing, invalid, expired or revoked (`INVALID_TOKEN`).
- `403`: The token lacks the scope this operation needs (`INSUFFICIENT_SCOPE`), the plan is read-only (`READ_ONLY_MODE`), or the caller is not a member of the Space named in `X-Space-Id` (`NOT_A_MEMBER`).
- `404`: No such resource in the active Space (`NOT_FOUND`), or `X-Space-Id` names a Space that does not exist (`SPACE_NOT_FOUND`).
- `409`: The request conflicts with the current state, for example a delete blocked by dependents or a sync already running. This operation's description names the `error.code` values it can return.
- `429`: Too many requests (`RATE_LIMITED`). Wait for the number of seconds in the `Retry-After` header, then retry.

## Changes

- **2026-10-20** `f02611b531aa`: 1 info
  - the request property `_platformBudgets/additionalProperties/amount` became optional

[Operation history](https://skmtc.dev/growomat/apis/growomat-public-api/changes/api/v1/campaigns/:id/put.md)

---

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