← Back to product·Simulmedia VAMOS 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.
attachmentsarray of objectOptional paper riding beside line_items on a catalog order: the buysheet your desk works from, traffic instructions, a make-good notice. Never parsed, never a source of order fields, and never a reason for an extra gate. Not one of the three inputs below. See attachments[].
issued_atstringWhen your system issued the order, as an ISO datetime. An offset is fine: 2026-09-10T09:00:00-04:00 is kept as sent rather than rewritten to UTC. Defaults to the platform's created_at, so every order snapshot carries one. On a revision it is also the clock the prior order's notice window is measured against.
flightobject{start, end} as ISO dates. Optional, and when you state one, every date your lines can land on has to fall inside it or the create returns 422 INVALID_INPUT with details.field: "flight" naming the offending line and date. A slot line is checked on its own date; a week line is checked on the dates that week books, which are its Monday, Wednesday, and Friday. Stating a flight is how a mismatch between your header window and your line dates becomes an error you see instead of a call from the seller.
revisionobject{supersedes}: the order_id this order revises. See revisions. version is assigned by the platform; sending one is ignored.
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_entitystringThe acting buyer this order is for. Server-derived from your credential and echoed on every read, so you never need to send it. Sending it asserts which buyer you believe you are acting as: matching the resolved tenant is a no-op, disagreeing returns 403 TENANT_MISMATCH and creates nothing. See acting-buyer identity.
buyer_metadata requiredobjectadvertiser is required: name who the buy is for, or the create returns 422 INVALID_INPUT with details.missing: ["buyer_metadata.advertiser"]. An empty string counts as missing. Without it a seller cannot clear competitive separation, category conflict, or political and LUR compliance, which is work it has to do before approving. Other named keys: 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.

attachments is not a fourth option. It rides beside line_items, so the rule above is unchanged: send one of the three, and add attachments if you also have paper.

attachments[]

Your lines are authoritative and the file rides along. Nothing books on a parse, nothing waits for a confirmation round-trip, and the seller's traffic desk gets the paper it already works from.

Valid only alongside line_items under workflow: "catalog". On a proposal or document order it returns 422 INVALID_INPUT. For the case where the file is the order, see document orders.

FieldTypeDescription
upload_id requiredstringFrom POST /v1/uploads, in status: "uploaded". Must belong to your buyer_entity.
format requiredenumpdf, xlsx, or csv.
role requiredenumbuysheet, traffic_instructions, makegood_notice, or other. A label for the seller's desk; nothing on the platform branches on it.
filename requiredstringWhat the desk should see the file called.
sha256 requiredstringThe upload's stored hash. A value that does not match returns 422 INVALID_INPUT, so an attachment always points at specific bytes rather than at whatever now sits behind an id.
{
  "seller": "acme",
  "workflow": "catalog",
  "external_order_id": "q4-campaign-042",
  "rate_version": "acme-2026q4-v1",
  "buyer_metadata": { "advertiser": "Northwind Coffee" },
  "line_items": [ /* your authoritative lines */ ],
  "attachments": [
    {
      "upload_id": "upl_7f3a9c21b0d4",
      "format": "csv",
      "role": "buysheet",
      "filename": "q4-buysheet.csv",
      "sha256": "ac1f0ea9b25d426c4a16f359265a04dedf88ac0da95a7adf935361929c227d1f"
    }
  ]
}

Every entry is refused with 422 INVALID_INPUT and creates nothing when its upload_id does not resolve, belongs to another buyer, is not yet uploaded, was rejected, or has expired. Those four causes return the same response on purpose: a create is not a way to discover which upload IDs exist. A sha256 mismatch is reported separately, because reaching that check means the upload is already yours.

Attaching pins the upload: an upload an order references stops expiring, for the life of that order. See the upload lifecycle.

Attachments are never interpreted. No parse, no extraction, no field on your order comes from a file. If a buysheet and your lines disagree, your lines win and the disagreement is a warning, never a rejection: a slightly stale PDF cannot block a correct booking.

Both order reads echo attachments verbatim, sha256 included. The event that moves the order to seller_review carries the upload_id list, so a traffic desk knows paper exists before it opens the order: the seller receives that event as order.needs_review, the buyer as order.status_changed.

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, and neither format is validated. Optional at create and through every state before placed, and required by placed: the seller's decision that would place the order returns 422 INVALID_INPUT with details.missing naming each line that has none, and the order stays in seller_review. So you can book while still waiting on your agency for the code, and you cannot end up booked with committed air dates and no creative on file.
copy_namestringWhat the spot is called, beside the code the seller's systems key on. Optional, and read by people rather than by the platform: a traffic desk looking at ISCI-QSR-15-001 cannot tell by eye whether it is the right spot.

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",
      "units": 10,
      "spot_length": 30,
      "rate": {
        "class": "scatter",
        "advertiser_class": "general",
        "buy_type": "audience_guaranteed",
        "rate_basis": "per_unit",
        "unit_cost": 435.20,
        "guaranteed_impressions": 128000,
        "audience_code": "hh",
        "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",
  "buyer_entity": "yourco",
  "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.
403TENANT_MISMATCHA buyer_entity in the body that disagrees with the acting buyer your credential resolves to. Nothing is created.
403FORBIDDENThe seller has suspended this acting buyer, or runs an allowlist and has not allowlisted it. Nothing is created; see the tenant roster.
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",
  "buyer_metadata": { "advertiser": "Northwind Coffee" },
  "issued_at": "2026-09-10T09:00:00Z",
  "flight": { "start": "2026-10-05", "end": "2026-10-11" },
  "revision": { "version": 1, "supersedes": null },
  "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-W41",
      "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.

VAMOS 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. An order belonging to a different acting buyer reads as 404 rather than 403, so a known order ID never confirms whose it is.
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.

Revisions

Real buys get revised. A revision is a new order with its own external_order_id, naming the order it replaces:

{
  "seller": "acme",
  "external_order_id": "q4-campaign-042-r2",
  "rate_version": "acme-2026q4-v1",
  "issued_at": "2026-09-18T11:30:00Z",
  "buyer_metadata": { "advertiser": "Northwind Coffee" },
  "revision": { "supersedes": "ord_9f3a12c4b7e1" },
  "line_items": [ ... ]
}

Do not reuse the original external_order_id to revise: a changed payload under the same key is the 409 CONFLICT it has always been, and a new key with no revision block is an unrelated second order. The lineage travels in revision and nowhere else.

The new order and the prior order's cancellation commit together, so there is never a moment when both are bookable:

OrderAfter the revision commits
The revisionrevision.version is the prior order's plus one, and revision.supersedes names it. Otherwise an ordinary new order: it enters at submitted and runs the same lifecycle.
The prior ordercanceled, with cancellation.reason: "superseded_by_revision" and revision.superseded_by naming the revision. Its external_order_id stays claimed, so a byte-identical resubmission of the original still replays the original response.

The prior order is canceled by the ordinary cancel path, which means the notice window applies exactly as it would to a cancel you sent yourself: a revision landing inside the window carries substatus: "late_notice" and the same notice block on the order.canceled event. The clock is your issued_at when you state one. There is no separate substatus for a supersede: the lineage is in revision.superseded_by, and the cancellation reads like any other.

You can supersede an order in any live state: submitted, validating, needs_confirmation, confirmed, negotiating, seller_review, or placed. A finished order cannot be revised.

StatusCodeWhen
404NOT_FOUNDsupersedes names an order outside your credentials. As everywhere else, not a 403: a known order ID never confirms whose it is.
422INVALID_INPUTThe prior order is in a state that cannot be superseded, with details.status naming it.
409CONFLICTThe prior order was already superseded, with details.superseded_by naming the revision that won. Two revisions of one order leave exactly one winner.

Nothing is created in any of the three cases, so the revision's own external_order_id is still free once you have fixed the problem.

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",
      "buyer_entity": "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.
advertiserstringWho the buy is for, lifted to the top level beside buyer so your desk reads it without unpacking buyer_metadata. The same value the metadata carries, never a second one. Present on every order; null only on an order booked before advertiser became required. The buyer's own GET /v1/orders/{id} does not carry it: the echo is for your decision view.
buyer_entitystringThe acting buyer, so an order is attributable when one connection carries several buyer organisations. Also on every row of the approval queue, and it is the value the tenant roster is keyed by.
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.

Tenant roster

GET/v1/me/tenantsorders:approve

Every acting buyer this connection has carried, so one integration can serve several buyer organisations and you can still tell them apart. One entry per distinct buyer_entity, created the first time an order arrives from it, or the first time you allowlist it.

Response

200 Response
{
  "tenants": [
    {
      "buyer_entity": "yourco",
      "status": "active",
      "allowlisted": false,
      "first_seen_at": "2026-10-01"
    }
  ],
  "total": 1
}
FieldTypeDescription
buyer_entitystringThe acting buyer, as it appears on every order and event from it.
statusenumactive or suspended. A suspended tenant's orders are refused at intake.
allowlistedbooleanWhether you have admitted this tenant explicitly. Only consulted under tenant_policy.mode: "allowlist".
reasonstringThe note you attached when suspending. Absent on an active tenant.
first_seen_atdateWhen this tenant first appeared on your connection.

status and allowlisted are independent, and all four combinations are legal. Suspension refuses regardless of the allowlist, so allowlisting a suspended tenant sets the flag and leaves the refusal in place until you unsuspend. Under tenant_policy.mode: "allowlist" a tenant that is not allowlisted is refused regardless of status.

Suspend and unsuspend a tenant

POST/v1/me/tenants/suspendorders:approve

POST/v1/me/tenants/unsuspendorders:approve

Stop taking orders from one acting buyer without touching the others on the same connection, and start again later. Both touch status only: a tenant's allowlisted flag survives a suspension untouched, because stopping a buyer is not the same as un-inviting it.

Request body

FieldTypeDescription
buyer_entity requiredstringThe acting buyer, from the roster. It travels in the body and never in the path, so a tenant identifier never lands in a URL, an access log, or a cache key.
reasonstringsuspend only: your note, persisted on the entry and dropped when you unsuspend.

Response

200 Response
{
  "buyer_entity": "yourco",
  "status": "suspended",
  "allowlisted": false,
  "reason": "credit hold",
  "first_seen_at": "2026-10-01"
}

Both are idempotent and neither has a precondition: unsuspending an active tenant returns its current state rather than an error. A suspended tenant's POST /v1/orders returns 403 and creates nothing.

Allowlist a tenant

POST/v1/me/tenants/alloworders:approve

Admit one acting buyer explicitly. Touches allowlisted only, never status. Permitted for a tenant that has never sent an order, which is the point under tenant_policy.mode: "allowlist": you have to be able to open the door before the first knock.

Request body

FieldTypeDescription
buyer_entity requiredstringThe acting buyer to admit. In the body, never in the path.

Response

200 Response
{
  "buyer_entity": "yourco",
  "status": "active",
  "allowlisted": true,
  "first_seen_at": "2026-10-01"
}

Idempotent: allowlisting an already-allowlisted tenant returns its current state. There is no de-allowlisting operation; suspend instead.

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.