---
title: "Update a Line Item"
method: PATCH
path: "/context/campaign-plans/{slug}/line-items/{lineItemId}"
tags: ["Line Items"]
---

# Update a Line Item

`PATCH /context/campaign-plans/{slug}/line-items/{lineItemId}`

## Request

- Base URL: `https://api.adcrunch.dev`
- URL: `https://api.adcrunch.dev/context/campaign-plans/{slug}/line-items/{lineItemId}`
- Auth: HTTP bearer

Updates a Line Item. Send the `base_revision` you read. Omit a field to keep it. Send `null` to clear it.

**Any change puts a validated line back into draft.** Only that line changes. The Campaign Plan keeps its approval, because that approval speaks about the whole allocation. To open the gate again, validate the line again.

A write that changes nothing keeps the validation, and it does not move the `revision`.

The provider objects that the line already created do not change. This call never alters a live campaign.

## Path parameters

- `lineItemId` string, required
- `slug` string, required

## Request body

- object
  - `advertiser_id` string, nullable: null unlinks the ad account.
  - `age_max` integer, nullable
  - `age_min` integer, nullable
  - `base_revision` integer, required: The revision last read via GET — guards concurrent edits.
  - `budget` number, nullable: Whole units of the plan’s currency: `40.5` is €40.50. No more decimals than the currency has.
  - `channel` 'meta' | 'tiktok' | 'snapchat' | 'google_search' | 'google_pmax' | 'google_display' | 'youtube' | 'programmatic_display' | 'linkedin' | 'x': What is being bought, in a planner’s vocabulary — not a provider. Curated and deliberately incomplete.
  - `countries` string[]: The array is the value — sending [] clears the geography.
  - `end_date` string, nullable: null makes the line inherit the Campaign Plan’s end — which can flip the budget between a total and a daily rate. That is allowed, and it reverts the line to draft.
  - `gender` 'all' | 'men' | 'women', nullable: Omit for undecided, which executes the same as "all".
  - `objective` 'awareness' | 'engagement' | 'leads' | 'sales' | 'traffic' | 'app_promotion': Provider-neutral outcome, translated at execution. There is deliberately no optimization goal here.
  - `persona_id` string, nullable
  - `start_date` string, nullable

## Response `200`

The Line Item, bare rather than wrapped — unlike the create, which also reports the Campaign Plan status. Any change returns a `validated` line to `draft`. `executions` lists what the line created, the same as a read of its Campaign Plan.

- object
  - `advertiserId` string, nullable, required
  - `ageMax` number, nullable, required
  - `ageMin` number, nullable, required: Each bound is independently optional, so "35+" is expressible.
  - `budget` number, nullable, required: Whole units of the **plan’s** currency.
  - `campaignPlanId` string, required
  - `channel` 'meta' | 'tiktok' | 'snapchat' | 'google_search' | 'google_pmax' | 'google_display' | 'youtube' | 'programmatic_display' | 'linkedin' | 'x', required: The planner’s word for what is being bought, not a provider name. A plan may name a channel we cannot execute.
  - `countries` string[], required
  - `createdAt` number, required: Milliseconds since the Unix epoch, UTC.
  - `createdBy` string, required
  - `effectiveEndDate` string, nullable, required: The line’s own bound where it has one, else the plan’s.
  - `effectiveStartDate` string, nullable, required: A calendar date, `YYYY-MM-DD`.
  - `endDate` string, nullable, required: A calendar date, `YYYY-MM-DD`.
  - `executable` boolean, required: Whether this channel could be executed at all today. Derived from our capabilities, never a promise that it will be.
  - `executions` object[], required: What this line has spawned. Never a claim that it is *finished* — nobody declared how many objects a line should produce.
    - `advertiserId` string, required
    - `createdAt` number, required: Milliseconds since the Unix epoch, UTC.
    - `entityId` string, required: The provider’s own id, unprefixed, so it joins to `entities`.
    - `entityType` string, required: The provider’s word for the level — `campaign`, `adset`, `ad`.
    - `id` string, required
    - `mutationWorkflowId` string, nullable, required: The durable execution that performed the write, when known.
    - `provider` string, required
  - `gender` 'all' | 'men' | 'women', nullable, required
  - `id` string, required
  - `objective` 'awareness' | 'engagement' | 'leads' | 'sales' | 'traffic' | 'app_promotion', required
  - `personaId` string, nullable, required: Explains the targeting; never supplies its values.
  - `provider` string, nullable, required: Who sells this channel, or null when nobody we integrate with does.
  - `revision` number, required: Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409.
  - `startDate` string, nullable, required: A calendar date, `YYYY-MM-DD`.
  - `status` 'draft' | 'validated', required: `validated` is the gate on execution.
  - `unit` 'total' | 'daily', required: How `budget` reads, derived from the effective end date. Present → a total for the period; absent → a daily rate. A surface must print this beside the amount.
  - `updatedAt` number, nullable, required: Milliseconds since the Unix epoch, UTC.

## 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 `campaign_plan:write`. `error` is `forbidden`.
- `404`: No such plan, or no Line Item with that id on it. `error` is `not_found`.
- `409`: The Line Item moved since you read it. `error` is `revision_mismatch`.
- `422`: Six codes share this status. `invalid_countries`, `invalid_age_range`, `invalid_amount`, `invalid_window`, `persona_not_in_brand` or `advertiser_not_owned`. Each one is checked against the row as it would stand after the change, not against what you sent.

---

[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)
