---
title: "Start a campaign"
method: POST
path: "/campaigns"
---

# Start a campaign

`POST /campaigns`

## Request

- Base URL: `https://app.distribution.tech/api/v1`
- URL: `https://app.distribution.tech/api/v1/campaigns`
- Auth: HTTP bearer

From a goal to a priced plan in one call: goal (launch, funding, feature or program), product, moment (what goes live and when), window_start, window_end and budget_usd; optionally audience and company (left out, they come from your company context), geography (worldwide), desired_outcome (the goal's usual outcome), constraints and budget_min_usd. The answer's defaults lists every field it filled. The budget is held to this key's mandate: the plan is built within the smaller of budget_usd, the mandate's maximum plan total and what is left of its monthly cap, and a budget_min_usd above either limit is refused (mandate_refused, over_max_plan_total or over_monthly_cap, with the numbers); with no budget_min_usd the campaign is held to the mandate instead. The campaign is started for the brand exactly as from the workspace and the planner builds the plan from creators on sale now. Returns campaign_id and the plan as plan_quote returns it, or plan_note when nothing on sale fits yet. Needs an idempotency_key; a retry with the same key never starts a second campaign. A brand starts at most 3 campaigns in any 24 hours (rate_limited, details.reason daily_campaign_limit, details.resets_at when it reopens). Scope: campaigns:write. MCP tool: create_campaign.

## Headers

- `Idempotency-Key` string, required

## Request body

- CampaignInput: Left out, audience and company come from your company context (the audience your website read found; with no website read yet, send audience), geography is worldwide and desired_outcome is the goal's usual outcome; the answer's defaults names each field filled. A 422 names every required field that is missing, each in details.problems.
  - `goal` 'launch' | 'funding' | 'feature' | 'program', required
  - `product` string, required
  - `moment` string, required: What goes live and when.
  - `audience` string
  - `geography` string
  - `window_start` string, date, required
  - `window_end` string, date, required
  - `budget_usd` integer, required: The most to spend, in US dollars. The plan is held to this key's mandate: never above its maximum plan total or what is left of its monthly cap.
  - `budget_min_usd` integer: The least to spend. Above the mandate's maximum plan total, or what is left of its monthly cap, the campaign is refused (mandate_refused). Left out, the budget is held to the mandate instead of refused.
  - `desired_outcome` string
  - `company` string
  - `constraints` string

## Response `201`

OK

- CampaignCreated
  - `campaign_id` string, uuid, required
  - `status` 'plan_ready' | 'no_plan', required
  - `plan` Quote, required: A plan as the person sees it and as an agent signs it (v1-product-spec 8.5 Quote).
    - `plan_id` string, uuid
    - `campaign_id` string, uuid
    - `version` integer
    - `status` 'awaiting_approval' | 'approved'
    - `currency` string
    - `launch_window_start` string, date
    - `launch_window_end` string, date
    - `terms_fingerprint` string: Send exactly this to approve. It changes when a line, a price or the dates change.
    - `total_cents` integer
    - `totals` object: creators, total_cents, views_low, views_high, unmeasured, cpm_low_cents, cpm_high_cents, tiers, budget_cents, remaining_cents.
    - `lines` QuoteLine[]
      - `id` string, uuid, required
      - `creator` object, nullable, required
        - `id` string, uuid: The creator's id, for creator_get. Left out when not known.
        - `name` string, nullable, required
        - `handle` string, nullable, required: The creator's X handle once this plan's order is paid; null before (the name shows before payment).
        - `profile_url` string, nullable, required: The creator's X profile link once this plan's order is paid; null before.
        - `avatar_url` string, uri: The creator's live profile photo. Left out when there is none.
        - `followers` integer: The creator's follower count on the channel. Left out when not measured.
      - `creator_id` string, uuid, nullable, required: The creator's id at the top level (the same as creator.id); null when not known.
      - `unit_id` string, uuid, nullable, required
      - `stage_id` 'planned' | 'offered' | 'creator_confirmed' | 'scheduled' | 'live' | 'proof_attached' | 'cancelled', required: Where this post stands; the same stage model as a campaign's lines_by_stage. Branch on this, never on stage.
      - `stage` string, required: The label for stage_id: Planned, Offered, Creator confirmed, Scheduled, Live, Proof attached, Cancelled.
      - `channel` string, required
      - `channel_label` string, required
      - `format` string, nullable, required
      - `deliverable` string, required
      - `scheduled_date` string, date, required
      - `price_cents` integer, required: The all-in price of this line. Never split into parts.
      - `vetting` string, nullable, required
      - `reason` string, nullable, required
      - `expected_views_low` integer, nullable, required
      - `expected_views_high` integer, nullable, required
      - `on_sale` boolean, required
    - `payment_options` PaymentOption[]
      - `rail` 'checkout' | 'invoice', required
      - `label` string, required
      - `clears` string, required
    - `agent_payment` 'checkout' | 'team_invoice'
    - `brand_terms` object
      - `current_version` string, nullable, required: The current brand terms version id (a date).
      - `current_number` integer, nullable, required: The number the app shows for that version (version 3).
      - `url` string, nullable, required
      - `accepted_for_this_key` string, nullable, required
      - `accepted_number_for_this_key` integer, nullable, required
    - `mandate` Mandate, nullable
      - `key_id` string, uuid
      - `version` integer
      - `max_plan_cents` integer
      - `monthly_cap_cents` integer
      - `channels` string[]
      - `may_approve` boolean
      - `may_exceed_budget` boolean: Retired, always false. No key and no person can approve a plan over its campaign's budget: approve_plan refuses it with over_budget until a creator is removed or the budget is raised.
      - `expires_at` string, date-time
      - `updated_at` string, date-time
      - `spent_this_month_cents` integer
      - `terms_version` string, nullable: The brand terms a person accepted for this key. Null on a mandate that may not sign (the terms govern signing) and on the workspace's own agent key.
      - `terms_accepted_at` string, date-time, nullable
      - `terms_accepted_email` string, nullable
    - `policy` object: allowed: whether this key's mandate lets it sign now; refusal: the reason and numbers when not (over_budget when the plan is over the campaign's budget, which no mandate can approve: remove a creator or have the brand raise the budget).
      - `allowed` boolean
      - `refusal` object, nullable
    - `blocked_lines` string[]
    - `over_budget` object, nullable: The plan over its campaign's budget: budget_cents, total_cents and over_by_cents, at current prices when a creator's price rose past the budget (the plan then keeps its earlier prices, so total_cents alone reads as within it). Null within the budget, with no budget, or once signed. Approval refuses it until a creator is removed or the budget is raised.
      - `budget_cents` integer
      - `total_cents` integer
      - `over_by_cents` integer
    - `order` object, nullable
    - `valid_while` 'terms_fingerprint_unchanged'
  - `plan_note` string, nullable, required
  - `defaults` string[]: The fields filled because the request left them out.
  - `budget` object: The budget the campaign was stored with. held is true when budget_usd alone was held lower, to what the mandate allows.
    - `min_usd` integer, nullable, required
    - `max_usd` integer, nullable, required
    - `held` boolean, required

## Other responses

- `400`: A parameter is not valid: details.problems names each field and what it accepts.
- `401`: No API key, an unknown one, or a revoked one.
- `403`: The key lacks the scope, or the mandate refuses (mandate_refused, with the limit and how much over).
- `404`: No plan, line or creator of yours has that id.
- `409`: conflict (with a reason), idempotency_conflict, or in_progress (Retry-After).
- `422`: unprocessable: the body is readable but a value cannot be used (a blank or too long name, a campaign that cannot be planned yet); details.problems names each field when there are several.
- `429`: Too many requests for this key in the current minute (60); Retry-After and RateLimit-Reset say when to try again. On POST /campaigns and on POST /plans/current/lines when it starts a plan: the brand's 3 new campaigns in 24 hours are used (details.reason daily_campaign_limit); details.resets_at and Retry-After say when the limit reopens, a rolling 24 hours from the first of them, not a calendar day; archiving or deleting a campaign does not give its slot back. On GET /creators and GET /creators/{id}: a creator read limit is used (details.reason per_minute, daily_lists, daily_reveals, daily_seen or locked). daily_seen is your workspace's one pool of 300 creator views a day (300 different creators shown or opened since midnight UTC), shared by the app and every API key of the brand: details.scope workspace and details.limit 300; a creator already seen today, in the app or through any key, still opens and costs nothing. The other reasons are this key's own limit (details.scope key) or the brand's ceiling over all its keys (details.scope api).
- `500`: internal: the request failed on our side. Retry it with the same Idempotency-Key.
- `503`: unavailable: writes are not available right now (for an approval: payments are not open yet).

---

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