← Back to product·Simulmedia Switchboard Docs · v1 draft·API reference / Orders
API reference

Orders

Create, track, and decide orders. Buyers write through /orders; sellers decide through /me/orders. The order shape is defined once, at order; the lifecycle at the state machine.

At a glance
  • Scopes: orders:write (buyers), orders:approve (sellers)
  • Idempotency: external_order_id; byte-identical resubmission replays the stored response, 409 only on a changed payload
  • Price locks at order time via locked_rate_id; see the price-clearance invariant
  • Track by webhook or event replay; the snapshot is always authoritative

Create an order

POST/v1/ordersorders:write

Creates an order from line items, a proposal request, or a document reference. Returns immediately; everything after intake arrives as events.

Request body

FieldTypeDescription
seller requiredstringThe counterparty, from GET /sellers.
external_order_id requiredstringYour idempotency key, echoed on every event. Identical duplicates replay the original response with X-Idempotent-Replay: true; that replay is how you learn the outcome of a request that timed out on you.
rate_version requiredstringThe card commit you priced against. Version-current cards pass rate match by construction.
workflowenumOne of the workflows. Default: catalog.
line_itemsarray of objectRequired for workflow: "catalog". One line books one week or one slot; a two-week buy is two lines.
requestobjectProposal orders: a proposal_request. No package IDs required.
documentobjectDocument orders: {upload_id, format}. The parse resumes at needs_confirmation.
settlement_modeenumOne of the settlement modes. Default: direct.
notify_urlstringWebhook target for this order; must be registered and allowlisted.
agreement_idstringPresents an agreement, which attaches its terms to this order: the cancellation_terms snapshot and any posting policy override. Omitting it attaches nothing. Must resolve to an agreement with the same seller, or the create returns 422 INVALID_INPUT with details.agreement_id.
buyer_metadataobjectNamed keys: advertiser, advertiser_category, advertiser_class, campaign, campaign_id, brand, agency, estimate_id, notes. estimate_id is the agency estimate number, echoed on EDI delivery so your billing system can match the record. advertiser_category is an open string naming what the advertiser sells, which is what competitive separation runs on: advertiser_class prices, advertiser_category separates.

Send exactly one of line_items, request, or document, matched to workflow: catalog, proposal, or document.

line_items[]

FieldTypeDescription
package_id requiredstringFrom the seller's catalog.
provider, network requiredstringBookable entity and channel, as the catalog lists them.
weekISO weekBooks the package week; the seller distributes units across the week's slots and reports the dates in allocation. Set week or slot_id, not both.
slot_idstringPins the air date: acme-prime:2026-10-07, daylocked by construction. See slot.
unitsintegerRequired for spot-cost buy types; forbidden on audience_guaranteed.
impressions_goalintegerRequired for audience_guaranteed; forbidden on spot-cost lines.
spot_length requiredintegerSeconds; a length the cited rate entry prices.
rate requiredobjectThe priced cell you are locking: {class, advertiser_class, buy_type, clearance_tier?, rate_basis, unit_cost? or cpm?, locked_rate_id, currency}. Take locked_rate_id from avails.
include_mirrorbooleanBooks the package's paired mirror airing. Default: false.
rc_impressionsobjectImpression estimates at order time, per audience code, for buysheet parity.
copy_idstringCreative reference; Ad-ID and ISCI ride here.

Example request

{
  "seller": "acme",
  "workflow": "catalog",
  "external_order_id": "q4-campaign-042",
  "rate_version": "acme-2026q4-v1",
  "line_items": [
    {
      "package_id": "acme-prime",
      "provider": "National ACME",
      "network": "ACME",
      "week": "2026-W41",
      "units": 10,
      "spot_length": 30,
      "rate": {
        "class": "scatter",
        "advertiser_class": "general",
        "buy_type": "preemptible",
        "clearance_tier": "P2",
        "rate_basis": "per_unit",
        "unit_cost": 405.00,
        "locked_rate_id": "lr_88c1",
        "currency": "USD"
      },
      "rc_impressions": {
        "hh": 128000,
        "p2plus": 195000,
        "a25_54": 82000
      },
      "copy_id": "creative-q4-30s"
    },
    {
      "package_id": "acme-prime",
      "provider": "National ACME",
      "network": "ACME",
      "week": "2026-W41",
      "impressions_goal": 1280000,
      "spot_length": 30,
      "rate": {
        "class": "scatter",
        "advertiser_class": "general",
        "buy_type": "audience_guaranteed",
        "rate_basis": "cpm",
        "cpm": 3.40,
        "locked_rate_id": "lr_88c3",
        "currency": "USD"
      }
    }
  ],
  "agreement_id": "agr_71c0d2",
  "buyer_metadata": {
    "advertiser": "YourClient",
    "advertiser_category": "quick_service_restaurant",
    "estimate_id": "EST-4402"
  }
}

Response

201 Response
{
  "order_id": "ord_9f3a12c4b7e1",
  "external_order_id": "q4-campaign-042",
  "status": "submitted",
  "created_at": "2026-10-01T14:02:11Z"
}

Errors

StatusCodeWhen
409CONFLICTSame external_order_id, different payload. Identical duplicates replay instead.
422INVALID_INPUTMalformed input, unknown package_id, an agreement_id that does not resolve to an agreement with this seller (details.agreement_id), or a non-allowlisted notify_url.
422CREDIT_LIMITA cleared order that would breach your credit cap fails fast at create.
asyncSTALE_RATE_CARDSurfaces as a failed event with a rate_drift diff per line. Refresh the card and resubmit; see the price-clearance invariant.
asyncINVENTORY_UNAVAILABLEAvailability rejection, the one legitimate validation failure. Carries details.pool_id when a shared pool is exhausted.

Error shape and recovery: Errors.

Read an order

GET/v1/orders/{id}orders:write

The authoritative snapshot, with a real last_seq. Webhooks are hints; this is truth. Full field reference: order.

Response

200 Response
{
  "order_id": "ord_9f3a12c4b7e1",
  "external_order_id": "q4-campaign-042",
  "seller": "acme",
  "workflow": "catalog",
  "status": "placed",
  "substatus": null,
  "last_seq": 5,
  "rate_version": "acme-2026q4-v1",
  "settlement_mode": "direct",
  "created_at": "2026-10-01T14:02:11Z",
  "updated_at": "2026-10-02T14:41:09Z",
  "line_items": [
    {
      "unit_id": "u-0001",
      "package_id": "acme-prime",
      "provider": "National ACME",
      "network": "ACME",
      "week": "2026-W40",
      "allocation": [
        {
          "date": "2026-10-05",
          "units": 4
        },
        {
          "date": "2026-10-07",
          "units": 3
        },
        {
          "date": "2026-10-09",
          "units": 3
        }
      ],
      "spot_length": 30,
      "num_requested": 10,
      "num_accepted": 10,
      "status": "accepted",
      "reason": null,
      "rate": {
        "class": "scatter",
        "advertiser_class": "general",
        "buy_type": "preemptible",
        "clearance_tier": "P2",
        "rate_basis": "per_unit",
        "unit_cost": 405.00,
        "locked_rate_id": "lr_88c1",
        "currency": "USD"
      },
      "rc_impressions": {
        "hh": 128000,
        "p2plus": 195000,
        "a25_54": 82000
      },
      "availability": {
        "state": "available",
        "basis": "actual",
        "sellout_level": 0.62
      }
    }
  ],
  "totals": {
    "gross": 4050.00,
    "currency": "USD",
    "spots": 10,
    "impressions_hh": 1280000,
    "impressions_p2plus": 1950000,
    "impressions_a25_54": 820000
  },
  "artifacts": [
    {
      "name": "placement_receipt",
      "seq": 5,
      "seller_order_id": "ACME-77120",
      "daylocks": [
        {
          "unit_id": "u-0001",
          "date": "2026-10-05",
          "units": 4
        },
        {
          "unit_id": "u-0001",
          "date": "2026-10-07",
          "units": 3
        },
        {
          "unit_id": "u-0001",
          "date": "2026-10-09",
          "units": 3
        }
      ],
      "placed_at": "2026-10-02T14:41:09Z"
    }
  ],
  "events_url": "/v1/events?order=ord_9f3a12c4b7e1"
}

The allocation on each line is authoritative: a week line's units spread across the package's weekday pattern, one entry per air date, summing to num_accepted. A slot line has a single entry. See day resolution.

Confirm an order at needs_confirmation

POST/v1/orders/{id}/confirmorders:write

Resumes an order parked at needs_confirmation, the gate where you confirm an artifact before it books. Two workflows park here: a document order (confirm the parsed interpretation) and a proposal (confirm the seller's returned plan_proposal). Catalog orders never park here. On confirm, a document order proceeds to seller_review and a proposal books its plan through the normal order path (one order per pay-basis type). On reject, the order goes to canceled. To counter a plan instead of confirming it, use respond. A satisfied buyer acceptance band issues this confirm automatically.

Request body

FieldTypeDescription
action requiredenumconfirm or reject.
actor requiredstringWho decided; persisted in the audit trail. For an auto-accepted plan, the recorded actor is the acceptance band.
plan_idstringOn a proposal confirm: the plan being accepted, so a plan superseded by a fresh one cannot be confirmed by mistake.
reasonstringRequired on reject; persisted and surfaced to the seller.

Response

202 Response : document order
{
  "order_id": "ord_9f3a12c4b7e1",
  "status": "confirmed"
}

202 Response : proposal, plan booked
{
  "order_id": "ord_c19a44e2f0b1",
  "status": "seller_review",
  "booked_orders": ["ord_7a10b3c2d4e5"]
}

Respond in a negotiation

POST/v1/orders/{id}/respondorders:write

Answers a seller counter on an order parked in negotiating. Rounds live on the negotiation object. See Negotiation.

Request body

FieldTypeDescription
action requiredenumaccept, counter, or withdraw.
actor requiredstringWho decided; persisted.
counterobjectRequired on counter: revised line_items (unit_id, week, units, rate) and an expires_at.

Example request

{
  "action": "counter",
  "actor": "buyer@yourco.com",
  "counter": {
    "line_items": [
      {
        "unit_id": "u-0001",
        "week": "2026-W41",
        "units": 8,
        "rate": {
          "unit_cost": 392.00
        }
      }
    ],
    "expires_at": "2026-09-30T20:00:00Z"
  }
}

Response

202 Response
{
  "order_id": "ord_9f3a12c4b7e1",
  "status": "negotiating",
  "negotiation_id": "neg_33a7",
  "round": 2
}

Cancel an order

POST/v1/orders/{id}/cancelorders:write

Idempotent. Canceling a terminal order returns the terminal snapshot unchanged, so a cancel is always safe to retry. Cancellation is a buyer action; a seller declines an order through decide.

Request body

FieldTypeDescription
reason requiredstringPersisted; canceled snapshots keep their structured reason on cancellation. A retry never overwrites it, because the second call appends no event.
actor requiredstringWho decided; persisted alongside the reason.
as_ofISO dateThe day the notice window is measured from. In the sandbox it defaults to the seeded day 2026-10-05, derived from the catalog's effective start: a fixed value that does not advance with real time, so every notice-window branch is reproducible on demand. Send it to pick the branch you want to see.

Which states cancel

Order stateBehavior
Any non-terminal state, including placedTransitions to canceled and emits one order.canceled event at the next seq.
completed, canceled, rejected, failedReturns the terminal status with a note. No transition, no event, no last_seq advance.
Unknown order_id404, as on GET /orders/{id}.

placed is not terminal, and it is the state cancellation matters most in: a placed order has committed air dates, and how close those dates are is what decides whether the cancellation is late.

The notice window

When the order snapshotted cancellation_terms from a presented agreement, the response says whether the cancellation landed inside the notice period the two of you negotiated. The window is measured to the next airing still ahead of the clock: dates that have already aired are discarded, because a notice period protects the airings a cancellation would actually stop. On a flight that has fully aired, days_remaining is negative and measured to the last date, stating how long ago the flight ended. An order with no committed allocation has no anchor, so no window applies.

A late cancellation is reported on three surfaces, and each carries a different part of it:

SurfaceWhat it carries
The cancel responseThe notice block, and only when the cancellation is late. No substatus: this response stays compact.
The order.canceled eventsubstatus, either "late_notice" or null, plus the same notice block when late, field for field identical to the response's.
The order snapshotstatus: "canceled", the same substatus, and the cancellation block. Never a notice field: substatus is the durable signal, and the block itself replays from the event.

Outside the window, or with no terms attached, substatus is null and the notice block is absent from both the response and the event: omitted rather than present and false. The edge is not late, so days_remaining equal to notice_days is inside the terms.

Switchboard records and routes. It does not refuse the cancellation and it does not price a penalty: late_notice is how it tells the seller there is something to decide.

Response

202 Response : order was non-terminal
{
  "order_id": "ord_9f3a12c4b7e1",
  "status": "canceled"
}

202 Response : inside the notice window
{
  "order_id": "ord_9f3a12c4b7e1",
  "status": "canceled",
  "notice": {
    "within_notice_window": true,
    "notice_days": 14,
    "days_remaining": 13
  }
}

202 Response : order was already terminal
{
  "order_id": "ord_9f3a12c4b7e1",
  "status": "completed",
  "note": "order already in terminal state; no change made"
}

The response stays compact by design. For the full picture after a cancel, read the snapshot at GET /orders/{id}: it carries the terminal status, the substatus, and the cancellation block.

Errors

StatusCodeWhen
404NOT_FOUNDNo order with that ID under your credentials.
422INVALID_INPUTA missing reason or actor (details.missing), a body that is not an object, or an as_of that is not an ISO date (details.as_of). stage is intake.

List orders

GET/v1/ordersorders:write

Your order book: the reconciliation sweep. Page with the cursor until exhausted.

Query parameters

FieldTypeDescription
statusenumOne or more order statuses; repeatable or comma-separated.
sellerstringFilter to one counterparty.
buy_typeenumFilter orders whose lines contain one of the buy types.
created_afterISO datetimeLower bound on creation time.
afterstringOpaque cursor from the prior response.

Response

200 Response
{
  "orders": [
    {
      "order_id": "ord_9f3a12c4b7e1",
      "status": "placed",
      "seller": "acme",
      "external_order_id": "q4-campaign-042"
    }
  ],
  "cursor": "eyJpZCI6Im...",
  "total": 142
}

cursor is null when exhausted; pass it as ?after= on the next call.

Approval queue

GET/v1/me/ordersorders:approve

The seller's order book. With the default filter it is your approval queue: everything waiting at seller_review, your gate on every order. Also available in the console. See Approve orders.

Query parameters

FieldTypeDescription
statusenumAny order status. Default: seller_review.
afterstringOpaque cursor from the prior response.

Response

200 Response
{
  "orders": [
    {
      "order_id": "ord_9f3a12c4b7e1",
      "status": "seller_review",
      "buyer": "yourco",
      "totals": {
        "gross": 4050.00,
        "currency": "USD"
      },
      "received_at": "2026-10-01T14:02:14Z"
    }
  ],
  "cursor": null,
  "total": 1
}

Read an order (seller)

GET/v1/me/orders/{id}orders:approve

The seller's view of one order: the same authoritative snapshot the buyer sees, plus the buyer identity and buyer_metadata you decide against, plus your own audit records. Shape: order.

FieldTypeDescription
buyerstringThe buyer that placed the order.
approvalsarray of objectOne approval per decision taken at your gate, in seq order: gate, actor, decision, reason when the decide request sent one, seq, and occurred_at. decision is the outcome derived from the per-unit actions, not the request's headline: a body sending approve with a countered unit records counter, because that is what happened. Empty before any decision. The buyer's GET /v1/orders/{id} does not carry it: an approval is your record of your own gate, and the buyer already sees the outcome on the line.

Return a plan for a proposal

POST/v1/me/orders/{id}/planorders:approve

Answer a proposal order: attach the assembled plan_proposal and park the order at needs_confirmation for the buyer. Precondition: the order has workflow: "proposal" and is in submitted, validating, or seller_review; a non-proposal order or a terminal one is rejected. Mint per-campaign prices as quote commits first, then reference their locked_rate_ids here. Posting a new plan supersedes any prior one on the same order.

Request body

FieldTypeDescription
plan requiredobjectThe plan_proposal: line_items (each citing a locked_rate_id), minted_packages, quote_refs, plan_totals (including projected_delivery), unmet, and an expires_at.
actor requiredstringWho assembled the plan; persisted.

Response

202 Response
{
  "order_id": "ord_c19a44e2f0b1",
  "status": "needs_confirmation",
  "plan_id": "plan_5b2e"
}

Errors

StatusCodeWhen
409INVALID_STATEThe order is not a proposal, or not in a state that accepts a plan.
422INVALID_INPUTA line cites an unknown or expired locked_rate_id, or the plan omits a required field.

Decide an order

POST/v1/me/orders/{id}/decideorders:approve

Your decision on an order in seller_review: approve, reject, or counter, per unit. Nothing airs without it.

Request body

FieldTypeDescription
decision requiredenumapprove, reject, or counter: the headline. Per-unit actions below may mix.
actor requiredstringWho decided; persisted in the audit trail.
reasonstringYour note on the decision as a whole, persisted on the approval and returned by GET /v1/me/orders/{id}. Distinct from a per-unit reason, and never derived from one.
units requiredarray of objectOne entry per unit. Fields below.

units[]

FieldTypeDescription
unit_id requiredstringThe line being decided.
action requiredenumaccept, counter, or reject.
num_acceptedintegerOn accept: may be fewer than requested (partial acceptance).
counterobjectRequired on counter: {week, unit_cost, num_offered}. Requires the counter_proposals capability.
reasonstringRequired on reject and counter; persisted and surfaced to the buyer.

Example request

{
  "decision": "approve",
  "actor": "planner@seller.com",
  "units": [
    {
      "unit_id": "u-0001",
      "action": "accept",
      "num_accepted": 10
    },
    {
      "unit_id": "u-0002",
      "action": "counter",
      "counter": {
        "week": "2026-W42",
        "unit_cost": 392.00,
        "num_offered": 8
      },
      "reason": "W41 sold out; offering W42 at adjusted rate"
    },
    {
      "unit_id": "u-0003",
      "action": "reject",
      "reason": "daypart unavailable this week"
    }
  ]
}

Response

202 Response
{
  "order_id": "ord_9f3a12c4b7e1",
  "status": "placed",
  "units_accepted": 10,
  "units_countered": 8,
  "units_rejected": 6
}

status is negotiating when counters were issued. Counters surface on the order as additional line items with status: "countered" and an in_response_to naming the unit they answer; the original line keeps its own result. Consumers treat countered lines as additive: a seller that only approves and rejects today can add counters later without breaking anyone. See Negotiation.