---
title: "Add a creator"
method: POST
path: "/plans/{id}/lines"
---

# Add a creator

`POST /plans/{id}/lines`

## Request

- Base URL: `https://app.distribution.tech/api/v1`
- URL: `https://app.distribution.tech/api/v1/plans/{id}/lines`
- Auth: HTTP bearer

Add a creator to a plan awaiting approval by its unit_id (a format's unit_id from creators_search or creator_get), at that format's current all-in price, the same as the brand adding it in the workspace. Leave out plan_id (REST: POST /api/v1/plans/current/lines) to add to the brand's newest plan awaiting approval, or to start a plan for this creator when none waits; that also needs the mandate to allow starting a campaign, and counts toward the brand's 3 new campaigns in 24 hours. Only approved creators on sale can be added (not_approved otherwise). A creator who would take the plan's all-in total over the mandate's maximum plan total, or this month's signed spend plus that total over its monthly cap, is refused (mandate_refused, over_max_plan_total or over_monthly_cap) and the plan is left as it was. A creator who would take the plan's all-in total over its campaign's budget is refused for everyone (conflict, reason over_budget, with budget_cents and remaining_cents, the room left before this creator) and the plan is left as it was; pick a creator priced within remaining_cents or remove one first. Returns the new line_id and the plan as plan_quote returns it. Needs an idempotency_key. Scope: campaigns:write. MCP tool: plan_add_creator.

## Path parameters

- `id` union, required
  - string, uuid
  - 'current'

## Headers

- `Idempotency-Key` string, required

## Request body

- LineInput
  - `unit_id` string, uuid, required: A format's unit_id from /creators.

## Response `200`

OK

- LineAdded
  - `line_id` string, uuid, 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'

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