Quickstart: place your first order
Ten minutes, sandbox credentials, one order on air (in the sandbox sense). The sandbox seller is Acme Broadcasting; it behaves like a real seller, including the approval pause. This walks the catalog path because it is the fastest way to see an order move; it is not the only front door. When a seller withholds avails or prices per campaign, or when you would rather send a brief than pick line items, start with a proposal instead. Both are first-class.
1. Get a token
curl -s https://auth.sandbox.switchboard.simulmedia.com/oauth/token \ -d grant_type=client_credentials \ -d client_id=$SWB_CLIENT_ID -d client_secret=$SWB_CLIENT_SECRET
2. Browse the catalog
curl -s https://api.sandbox.switchboard.simulmedia.com/v1/sellers/acme/catalog \ -H "Authorization: Bearer $TOKEN" { "packages": [ { "package_id": "acme-prime", "type": "daypart", "definition": { "selling_title": "Prime", "days": ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"], "start": "20:00", "end": "23:00" }, "taxonomy_version": "acme-2026q4-v1", "network": "ACME", "currencies": ["spots","hh","p2plus","a25_54"], "buy_types": ["preemptible","non_preemptible","fixed_position","audience_guaranteed"] } ] }
3. Check avails and rates
curl -s "https://api.sandbox.switchboard.simulmedia.com/v1/sellers/acme/avails?package_id=acme-prime&weeks=2026-W41&spot_length=30" \ -H "Authorization: Bearer $TOKEN" { "avails": [ { "week": "2026-W41", "grain": "week", "state": "available", "basis": "actual", "spots": { "available": 42, "total": 80 }, "sellout_level": 0.62, "rates": [ { "class": "scatter", "advertiser_class": "general", "spot_length": 30, "buy_type": "preemptible", "clearance_tier": "P2", "rate_basis": "per_unit", "gross_rate_unit": 405.00, "locked_rate_id": "lr_88c1" }, { "class": "scatter", "advertiser_class": "general", "spot_length": 30, "buy_type": "non_preemptible", "rate_basis": "per_unit", "gross_rate_unit": 520.00, "locked_rate_id": "lr_88c2" }, { "class": "scatter", "advertiser_class": "general", "spot_length": 30, "buy_type": "fixed_position", "rate_basis": "per_unit", "gross_rate_unit": 640.00, "locked_rate_id": "lr_88c4" }, { "class": "scatter", "advertiser_class": "general", "spot_length": 30, "buy_type": "audience_guaranteed", "rate_basis": "cpm", "gross_rate_cpm": 3.40, "audience_code": "hh", "locked_rate_id": "lr_88c3" } ] } ] }
4. Place the order
Catalog orders reference package IDs and locked rates; because you ordered against published availability and a current rate version, there is no confirmation round-trip.
curl -s https://api.sandbox.switchboard.simulmedia.com/v1/orders \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "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" } }], "buyer_metadata": { "advertiser": "YourClient", "campaign": "Q4-2026" }, "notify_url": "https://yourapp.example.com/webhooks/switchboard" }' { "order_id": "ord_9f3a12c4b7e1", "status": "submitted" }
5. Watch it move
Your notify_url receives events as the order validates and enters seller_review (a person at the seller approves; in the sandbox this takes about a minute). At any time the snapshot is authoritative:
curl -s https://api.sandbox.switchboard.simulmedia.com/v1/orders/ord_9f3a12c4b7e1 \ -H "Authorization: Bearer $TOKEN"
On approval you receive a placement_receipt (the booking receipt: the seller's order ID, daylocks applied, timestamp) as the order lands in placed: the buy is booked. When the flight has run, a final completed event closes the order; the sandbox compresses that wait to about a minute. Discovered, ordered, approved, placed. No email involved.
Things to try
The sandbox seller publishes four packages across the whole quarter, 2026-W40 through 2026-W52, so there is a second question worth asking. Below is one runnable request per outcome, in the order of the table. The sandbox tutorial walks them with the responses and the reasoning.
| Outcome | What you get |
|---|---|
Full acceptance to placed | units_accepted: 10, an allocation, and a placement_receipt |
| Partial acceptance | The order is placed; the unit is partial with num_accepted: 6 |
INVENTORY_UNAVAILABLE | rejected from a well-formed request, because 2026-W43 is dark |
STALE_RATE_CARD | failed, with rate_drift per unit: what you locked against what the card says now |
A countered unit | negotiating, and the unit reads countered with num_accepted: 0 |
| Two buy types on one order | placed at 11049.60. No buy type on the order; each line carries its own |
| Two posting policies on one order | placed at 5964.00, each line reconciling under its own package's policy |
Three variables first. The seller secret is a second credential: the approval decision is a different actor, and the sandbox will not let the buyer token make it. The session id keeps your run separate from everyone else's, and both sides must send the same one.
export SWB=https://api.sandbox.switchboard.simulmedia.com/v1 export TOKEN=<your buyer secret> export SELLER_TOKEN=<your seller secret> export SWB_SESSION=$(uuidgen)
1. Full acceptance. Approve the order you placed in step 4. The order id is derived from your external_order_id, so the same key always resolves to the same order.
curl -s -X POST $SWB/me/orders/ord_9f3a12c4b7e1/decide \
-H "Authorization: Bearer $SELLER_TOKEN" -H "X-Sandbox-Session: $SWB_SESSION" \
-H "Content-Type: application/json" \
-d '{ "decision": "approve", "actor": "sales@acmebroadcasting.example",
"units": [ { "unit_id": "u-0001", "action": "accept", "num_accepted": 10 } ] }'
{ "order_id": "ord_9f3a12c4b7e1", "status": "placed",
"units_accepted": 10, "units_countered": 0, "units_rejected": 0 }
2. Partial acceptance. Ask for 10 and have the seller take 6. Plan against num_accepted, never against what you asked for.
curl -s -X POST $SWB/orders -H "Authorization: Bearer $TOKEN" \
-H "X-Sandbox-Session: $SWB_SESSION" -H "Content-Type: application/json" \
-d '{
"seller": "acme", "workflow": "catalog",
"external_order_id": "try-partial",
"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" } }]
}'
{ "order_id": "ord_dd4117e6dfa2", "status": "submitted" }
curl -s -X POST $SWB/me/orders/ord_dd4117e6dfa2/decide \
-H "Authorization: Bearer $SELLER_TOKEN" -H "X-Sandbox-Session: $SWB_SESSION" \
-H "Content-Type: application/json" \
-d '{ "decision": "approve", "actor": "sales@acmebroadcasting.example",
"units": [ { "unit_id": "u-0001", "action": "accept", "num_accepted": 6 } ] }'
{ "order_id": "ord_dd4117e6dfa2", "status": "placed",
"units_accepted": 6, "units_countered": 0, "units_rejected": 4 }
3. Inventory unavailable. 2026-W43 is dark. Availability is the only business reason an order is rejected outright, and the check runs at create.
curl -s -X POST $SWB/orders -H "Authorization: Bearer $TOKEN" \
-H "X-Sandbox-Session: $SWB_SESSION" -H "Content-Type: application/json" \
-d '{
"seller": "acme", "workflow": "catalog",
"external_order_id": "try-unavailable",
"rate_version": "acme-2026q4-v1",
"line_items": [
{ "package_id": "acme-prime", "provider": "National ACME", "network": "ACME",
"week": "2026-W43", "units": 4, "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_pr43p", "currency": "USD" } }]
}'
curl -s "$SWB/events?order=ord_b7afeb158ee0&after_seq=2" \
-H "Authorization: Bearer $TOKEN" -H "X-Sandbox-Session: $SWB_SESSION"
{ "order_id": "ord_b7afeb158ee0", "events": [
{ "event_type": "order.status_changed", "seq": 3, "status": "rejected",
"substatus": null, "artifact": null,
"error": { "error_code": "INVENTORY_UNAVAILABLE",
"error_message": "Requested week or slot lacks availability",
"stage": "availability", "recoverable": true, "details": {} },
"occurred_at": "2026-10-01T14:02:13Z" } ], "last_seq": 3 }
4. Stale rate card. Send a rate_version behind the current one and the platform refuses to guess which price you meant. rate_drift is the useful half: per unit, what you locked against what the card says now.
curl -s -X POST $SWB/orders -H "Authorization: Bearer $TOKEN" \
-H "X-Sandbox-Session: $SWB_SESSION" -H "Content-Type: application/json" \
-d '{
"seller": "acme", "workflow": "catalog",
"external_order_id": "try-stale",
"rate_version": "acme-2026q4-v0",
"line_items": [
{ "package_id": "acme-prime", "provider": "National ACME", "network": "ACME",
"week": "2026-W41", "units": 4, "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" } }]
}'
curl -s "$SWB/events?order=ord_3ec0e342f256&after_seq=2" \
-H "Authorization: Bearer $TOKEN" -H "X-Sandbox-Session: $SWB_SESSION"
{ "order_id": "ord_3ec0e342f256", "events": [
{ "event_type": "order.status_changed", "seq": 3, "status": "failed",
"substatus": null, "artifact": null,
"error": { "error_code": "STALE_RATE_CARD",
"error_message": "rate_version acme-2026q4-v0 is behind acme-2026q4-v1",
"stage": "validation", "recoverable": true,
"details": { "rate_drift": [ { "unit_id": "u-0001",
"locked_rate_id": "lr_88c1",
"your_cost": 405, "current_cost": 405 } ] } },
"occurred_at": "2026-10-01T14:02:13Z" } ], "last_seq": 3 }
5. A countered unit. On the cheaper daypart, have the seller answer with a price instead of a yes.
curl -s -X POST $SWB/orders -H "Authorization: Bearer $TOKEN" \
-H "X-Sandbox-Session: $SWB_SESSION" -H "Content-Type: application/json" \
-d '{
"seller": "acme", "workflow": "catalog",
"external_order_id": "try-counter",
"rate_version": "acme-2026q4-v1",
"line_items": [
{ "package_id": "acme-daytime", "provider": "National ACME", "network": "ACME",
"week": "2026-W41", "units": 4, "spot_length": 30,
"rate": { "class": "scatter", "advertiser_class": "general",
"buy_type": "preemptible", "clearance_tier": "P2",
"rate_basis": "per_unit", "unit_cost": 120.00,
"locked_rate_id": "lr_dt41p", "currency": "USD" } }]
}'
{ "order_id": "ord_3699c15718f3", "status": "submitted" }
curl -s -X POST $SWB/me/orders/ord_3699c15718f3/decide \
-H "Authorization: Bearer $SELLER_TOKEN" -H "X-Sandbox-Session: $SWB_SESSION" \
-H "Content-Type: application/json" \
-d '{ "decision": "counter", "actor": "sales@acmebroadcasting.example",
"units": [ { "unit_id": "u-0001", "action": "counter",
"reason": "Rate is below card for this week",
"counter": { "week": "2026-W41", "unit_cost": 135.00,
"num_offered": 4 } } ] }'
{ "order_id": "ord_3699c15718f3", "status": "negotiating",
"units_accepted": 0, "units_countered": 4, "units_rejected": 0 }
6. Two buy types on one order. A fixed position in the tentpole plus a CPM guarantee on Prime. One order, two ways of buying, and no buy type on the order itself.
curl -s -X POST $SWB/orders -H "Authorization: Bearer $TOKEN" \
-H "X-Sandbox-Session: $SWB_SESSION" -H "Content-Type: application/json" \
-d '{
"seller": "acme", "workflow": "catalog",
"external_order_id": "try-mixed-buy-types",
"rate_version": "acme-2026q4-v1",
"line_items": [
{ "package_id": "acme-gameday", "provider": "National ACME", "network": "ACME",
"week": "2026-W47", "units": 2, "spot_length": 30,
"rate": { "class": "scatter", "advertiser_class": "general",
"buy_type": "fixed_position", "rate_basis": "per_unit",
"unit_cost": 2600.00, "locked_rate_id": "lr_gd47f",
"currency": "USD" } },
{ "package_id": "acme-prime", "provider": "National ACME", "network": "ACME",
"week": "2026-W47", "impressions_goal": 1280000, "spot_length": 30,
"rate": { "class": "scatter", "advertiser_class": "general",
"buy_type": "audience_guaranteed", "rate_basis": "cpm",
"cpm": 4.57, "locked_rate_id": "lr_pr47g",
"currency": "USD" } }]
}'
{ "order_id": "ord_f944243d5f02", "status": "submitted" }
curl -s -X POST $SWB/me/orders/ord_f944243d5f02/decide \
-H "Authorization: Bearer $SELLER_TOKEN" -H "X-Sandbox-Session: $SWB_SESSION" \
-H "Content-Type: application/json" \
-d '{ "decision": "approve", "actor": "sales@acmebroadcasting.example",
"units": [ { "unit_id": "u-0001", "action": "accept", "num_accepted": 2 },
{ "unit_id": "u-0002", "action": "accept", "num_accepted": 8 } ] }'
{ "order_id": "ord_f944243d5f02", "status": "placed",
"units_accepted": 10, "units_countered": 0, "units_rejected": 0 }
7. Two posting policies on one order. Prime posts over four weeks at 90 percent; the syndicated sponsorship over two at 95. Mixed-policy orders are legal, and each line reconciles under its own package's terms.
curl -s -X POST $SWB/orders -H "Authorization: Bearer $TOKEN" \
-H "X-Sandbox-Session: $SWB_SESSION" -H "Content-Type: application/json" \
-d '{
"seller": "acme", "workflow": "catalog",
"external_order_id": "try-mixed-policies",
"rate_version": "acme-2026q4-v1",
"line_items": [
{ "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" } },
{ "package_id": "acme-morning-syndicated", "provider": "Acme Syndication",
"network": "ACME", "week": "2026-W41",
"impressions_goal": 620000, "spot_length": 30,
"rate": { "class": "scatter", "advertiser_class": "general",
"buy_type": "audience_guaranteed", "rate_basis": "cpm",
"cpm": 2.60, "locked_rate_id": "lr_88d3",
"currency": "USD" } }]
}'
{ "order_id": "ord_4415819986b9", "status": "submitted" }
curl -s -X POST $SWB/me/orders/ord_4415819986b9/decide \
-H "Authorization: Bearer $SELLER_TOKEN" -H "X-Sandbox-Session: $SWB_SESSION" \
-H "Content-Type: application/json" \
-d '{ "decision": "approve", "actor": "sales@acmebroadcasting.example",
"units": [ { "unit_id": "u-0001", "action": "accept", "num_accepted": 10 },
{ "unit_id": "u-0002", "action": "accept", "num_accepted": 6 } ] }'
{ "order_id": "ord_4415819986b9", "status": "placed",
"units_accepted": 16, "units_countered": 0, "units_rejected": 0 }
Reading the quarter is cheaper still, and needs no order at all:
curl -s "$SWB/sellers/acme/avails?package_id=acme-prime&weeks=2026-W40&weeks=2026-W47" \ -H "Authorization: Bearer $TOKEN" # 320 a spot against 545 curl -s "$SWB/sellers/acme/catalog?buy_type=fixed_position" \ -H "Authorization: Bearer $TOKEN" # acme-daytime drops out curl -s "$SWB/sellers/acme/catalog?distribution=syndication" \ -H "Authorization: Bearer $TOKEN" # the syndication layer alone curl -s "$SWB/sellers/acme/avails?package_id=acme-gameday&weeks=2026-W47&weeks=2026-W51" \ -H "Authorization: Bearer $TOKEN" # two dates, and nowhere else
POST /v1/webhooks/endpoints; unregistered hosts fail order creation fast with a 422 so you never wonder why events did not arrive.