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
| Field | Type | Description |
|---|---|---|
seller required | string | The counterparty, from GET /sellers. |
deal_type required | enum | One of the deal types. proposal carries a request instead of line items. |
external_order_id required | string | Your 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 required | string | The card commit you priced against. Version-current cards pass rate match by construction. |
workflow | enum | One of the workflows. Default: catalog. |
line_items | array of object | Catalog orders. One line books one week or one slot; a two-week buy is two lines. Fields below. Omit when deal_type is proposal or workflow is document. |
request | object | Proposal orders: a proposal_request. No package IDs required. |
document | object | Document orders: {upload_id, format}. The parse resumes at needs_confirmation. |
settlement_mode | enum | One of the settlement modes. Default: direct. |
notify_url | string | Webhook target for this order; must be registered and allowlisted. |
buyer_metadata | object | Named keys: advertiser, 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. |
Send exactly one of line_items, request, or document, matched to deal_type and workflow.
line_items[]
| Field | Type | Description |
|---|---|---|
package_id required | string | From the seller's catalog. |
provider, network required | string | Bookable entity and channel, as the catalog lists them. |
week | ISO week | Books 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_id | string | Pins the air date: acme-prime:2026-10-07, daylocked by construction. See slot. |
units | integer | For spot currency. Set units or impressions_goal, not both. |
impressions_goal | integer | For impression currency. |
spot_length required | integer | Seconds; a length the cited rate entry prices. |
rate required | object | The priced cell you are locking: {class, advertiser_class, guarantee_tier, clearance_tier, unit_cost, locked_rate_id, currency}. Take locked_rate_id from avails; it locks the price at order time. |
include_mirror | boolean | Books the package's paired mirror airing. Default: false. |
rc_impressions | object | Impression estimates at order time, per audience code, for buysheet parity. |
copy_id | string | Creative reference; Ad-ID and ISCI ride here. |
Example request
{
"seller": "acme",
"deal_type": "preemptible",
"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-W40",
"units": 10,
"spot_length": 30,
"rate": {
"class": "scatter",
"advertiser_class": "general",
"guarantee_tier": "ng_preemptible",
"clearance_tier": "P2",
"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"
}
],
"buyer_metadata": {
"advertiser": "YourClient",
"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
| Status | Code | When |
|---|---|---|
| 409 | CONFLICT | Same external_order_id, different payload. Identical duplicates replay instead. |
| 422 | INVALID_INPUT | Malformed input, unknown package_id, or a non-allowlisted notify_url. |
| 422 | CREDIT_LIMIT | A cleared order that would breach your credit cap fails fast at create. |
| async | STALE_RATE_CARD | Surfaces as a failed event with a rate_drift diff per line. Refresh the card and resubmit; see the price-clearance invariant. |
| async | INVENTORY_UNAVAILABLE | Availability 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",
"deal_type": "preemptible",
"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",
"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
| Field | Type | Description |
|---|---|---|
action required | enum | confirm or reject. |
actor required | string | Who decided; persisted in the audit trail. For an auto-accepted plan, the recorded actor is the acceptance band. |
plan_id | string | On a proposal confirm: the plan being accepted, so a plan superseded by a fresh one cannot be confirmed by mistake. |
reason | string | Required 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
| Field | Type | Description |
|---|---|---|
action required | enum | accept, counter, or withdraw. |
actor required | string | Who decided; persisted. |
counter | object | Required 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.
Request body
| Field | Type | Description |
|---|---|---|
reason required | string | Persisted; canceled snapshots keep their structured reason. |
actor required | string | Who decided. |
Response
202 Response : order was non-terminal { "order_id": "ord_9f3a12c4b7e1", "status": "canceled" } 202 Response : order was already terminal { "order_id": "ord_9f3a12c4b7e1", "status": "completed", "note": "order already in terminal state; no change made" }
List orders
GET/v1/ordersorders:write
Your order book: the reconciliation sweep. Page with the cursor until exhausted.
Query parameters
| Field | Type | Description |
|---|---|---|
status | enum | One or more order statuses; repeatable or comma-separated. |
seller | string | Filter to one counterparty. |
deal_type | enum | One of the deal types. |
created_after | ISO datetime | Lower bound on creation time. |
after | string | Opaque 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
| Field | Type | Description |
|---|---|---|
status | enum | Any order status. Default: seller_review. |
after | string | Opaque cursor from the prior response. |
Response
200 Response
{
"orders": [
{
"order_id": "ord_9f3a12c4b7e1",
"status": "seller_review",
"buyer": "yourco",
"deal_type": "preemptible",
"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. Shape: order.
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 is a proposal (deal_type: "proposal") 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; where you cannot meet the whole ask, list it in unmet rather than dropping it silently. Posting a new plan supersedes any prior one on the same order (the buyer's stale plan_id then fails to confirm).
Request body
| Field | Type | Description |
|---|---|---|
plan required | object | The 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 required | string | Who assembled the plan; persisted. |
Response
202 Response
{
"order_id": "ord_c19a44e2f0b1",
"status": "needs_confirmation",
"plan_id": "plan_5b2e"
}
Errors
| Status | Code | When |
|---|---|---|
| 409 | INVALID_STATE | The order is not a proposal, or not in a state that accepts a plan. |
| 422 | INVALID_INPUT | A 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
| Field | Type | Description |
|---|---|---|
decision required | enum | approve, reject, or counter: the headline. Per-unit actions below may mix. |
actor required | string | Who decided; persisted in the audit trail. |
units required | array of object | One entry per unit. Fields below. |
units[]
| Field | Type | Description |
|---|---|---|
unit_id required | string | The line being decided. |
action required | enum | accept, counter, or reject. |
num_accepted | integer | On accept: may be fewer than requested (partial acceptance). |
counter | object | Required on counter: {week, unit_cost, num_offered}. Requires the counter_proposals capability. |
reason | string | Required 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.