Your first order
This guide is the write-path sequel to Getting started. That one takes you from a fresh credential to reading orders correctly; this one takes you from a working credential to an order you placed, sitting in Collaterate with a tracking number on the way. Do that guide first - everything here assumes you can already mint a token and make an authenticated call.
Each step below is deliberately shallow: it shows the one call you make and the one or two facts that stop you making the classic mistake at that step, then links the guide that covers the endpoint in full. Read the deep guide before you build the step for real; read this page to see how the five of them fit together.
Every URL in this guide is the production one. For the sandbox, substitute collaterail.com
for collaterate.com throughout - see Environments.
1. What you need before starting
Section titled “1. What you need before starting”Placing an order touches three scopes, not one:
| Scope | What it gates in this guide |
|---|---|
partner-api/products:read |
the catalog and the quote (steps 2 and 3) |
partner-api/orders:write |
submitting the order and polling its submission (steps 4 and 5) |
partner-api/orders:read |
following the created order afterward (step 6) |
Mint your token asking for all three - the mint itself is
Getting started, step 2, with the
scope parameter carrying the three names space-separated. Then confirm what you actually
hold, because what you asked for and what you got are not necessarily the same
(how scopes resolve):
curl -s -H "Authorization: Bearer $TOKEN" \ https://api.partners.collaterate.com/v1/me | jq{ "partnerId": "1", "partnerName": "Acme", "grantedSiteIds": [7, 8], "authMethod": "COGNITO_M2M", "scopes": [ "partner-api/products:read", "partner-api/orders:read", "partner-api/orders:write" ]}Two things to take from this response before moving on: all three scopes are present, and
grantedSiteIds is non-empty. You need a site id in step 2 and again in step 4, and it must
come from this list - anything else is rejected. If a scope is missing, ask Partner
Integrations to add it now; discovering it at step 4, when the catalog worked fine on
products:read alone, is the slower way to learn the same thing.
2. Find something to order
Section titled “2. Find something to order”An order line must name a productId and a variantId, and
the catalog is the only place either comes from. Fetch a page,
filtered to your site:
curl -s -H "Authorization: Bearer $TOKEN" \ "https://api.partners.collaterate.com/v1/products?siteId=7&limit=50" | jq{ "products": [ { "siteId": 7, "productId": "SLS_918273", "variantId": 55012, "sku": "BC-100-WHT", "name": "Business Cards", "description": "16pt uncoated, full color both sides", "serviceType": "STOCK", "availableInventory": 1450, "inventoryTracked": true, "minQuantity": null, "maxQuantity": null, "shippable": true, "weight": "0.0120000000", "updatedOn": "2026-07-14T09:31:00.000Z", "artworkSource": null }, { "siteId": 7, "productId": "SLS_1", "variantId": null, "sku": null, "name": "Announcements", "description": "Digitally printed, ordered to exact quantity", "serviceType": "POD", "availableInventory": null, "inventoryTracked": false, "minQuantity": 50, "maxQuantity": 5000, "shippable": true, "weight": null, "updatedOn": "2024-08-01T07:32:35.125Z", "artworkSource": "STORED" } ], "nextCursor": "eyJwIjoiU0xTXzkxODI3MyIsInYiOjU1MDEyfQ"}Pick an entry and carry two facts forward, exactly as the catalog stated them:
(productId, variantId)is the key, andvariantId: nullon aPODproduct is part of it. Send the pair back verbatim on the order line - a placeholder, a zero, or an empty string in place of thatnullis a rejected line, not a tolerated approximation.artworkSourcedecides what the line must carry.USER_UPLOADmeans the line needs anartworkUrl;STOREDmeans it needs nothing - artwork is already on file;TEMPLATEmeanstemplateVariables, filled in from the template endpoint. Anullon aPODproduct means not classified - stop and ask Partner Integrations rather than guessing, because the cheap guess (STORED, send nothing) is exactly how a line gets submitted without the artwork it needed.
The catalog deliberately publishes no prices, and this endpoint is paginated the same way
orders are - full details, filters and the (productId, variantId) rule are in
Catalog and products.
3. Get a price
Section titled “3. Get a price”curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"quantity": 250}' \ https://api.partners.collaterate.com/v1/products/SLS_918273/quote{ "productId": "SLS_918273", "quantity": 250, "totalPrice": 89.99, "createdOn": "2026-07-30T00:00:00.000Z" }This is a POST, not a GET - the quantity travels in the body, because the price depends
on it.
Treat the number as informational: it is the site’s price, and it cannot see a specific customer’s negotiated discount, so it may read higher than what would really be charged at checkout. The authoritative money for the order you are about to place arrives later, on the items endpoint in step 6 - not here. What a quote does and does not account for is Pricing.
4. Submit the order
Section titled “4. Submit the order”{ "siteId": 7, "partnerOrderId": "acme-po-4471", "shipTo": { "name": "Jane Doe", "company": "Acme Co", "address1": "123 Main St", "city": "Springfield", "state": "IL", "postalCode": "62701", "country": "US", "phone": "555-0100" }, "lines": [ { "productId": "SLS_918273", "variantId": 55012, "partnerLineId": "acme-line-1", "quantity": 250 } ]}curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d @order.json \ https://api.partners.collaterate.com/v1/ordersNotice there is no price anywhere in the body - Collaterate always prices from its own
catalog, and a unitPrice you send is rejected as an unknown key, not ignored. Two rules to
internalize before your first retry rather than after it:
partnerOrderIdis your idempotency key. A call that times out on your side is safe to resend with the identical body: the record was already durably written, and you get back the original submission rather than a second order. Resending a different body under the same id is rejected as409 partner_order_id_reused- the whole contract is in Submitting orders.- Ship-to goes at the order level, or on every line - never mixed, never neither. Either
is rejected as
invalid_requestbefore a submission record exists.
The per-field limits, the line shape in full, and the reasons behind both rules are in Submitting orders.
5. Read the result
Section titled “5. Read the result”Every response - the POST above and every poll after it - is the same submission resource.
The call waits up to 20 seconds for a terminal state, so roughly 19 in 20 calls come back
finished in one round trip:
{ "submissionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": "completed", "order": { "orderNumber": 1000234, "status": "NEW", "totalPrice": 89.99, "lines": [ { "partnerLineId": "acme-line-1", "quantity": 250, "lineListTotal": 89.99, "lineTotalBeforeAdjustments": 89.99 } ] }}If yours is the twentieth, the same shape arrives with a non-terminal status and a
Retry-After: 2 header. Poll, honoring that header on every non-terminal response, until
status is completed or failed - both are terminal and never revert:
curl -s -H "Authorization: Bearer $TOKEN" \ https://api.partners.collaterate.com/v1/orders/submissions/3fa85f64-5717-4562-b3fc-2c963f66afa6Read the amounts on order.lines[] as a receipt, not as money to reconcile against: they are
whole-line totals as JSON numbers, never per-unit prices, so do not multiply by quantity.
The exact decimal-string amounts live on the items endpoint in the next step. The full
lifecycle - queued through terminal, the failure shapes, and the reconciliation list - is
Submitting orders.
6. Follow it to the door
Section titled “6. Follow it to the door”order.orderNumber from the completed submission is your handle on the order from here on,
under partner-api/orders:read. There is no change feed, so re-request its shipments on your
own schedule:
curl -s -H "Authorization: Bearer $TOKEN" \ "https://api.partners.collaterate.com/v1/orders/1000234/shipments" | jqRead trackingNumbers and nothing else - it is never null, and there is a
precedence rule
behind it you should not reimplement. The invariant that tells you when to stop:
trackingNumbersempty andshippedOn: null- not shipped yet. Keep polling.trackingNumbersnon-empty - you have what you came for. Stop.
GET /v1/orders/1000234/items is the other sub-resource: SKUs, ship state, and the
authoritative per-line money as exact decimal strings - parse them with a decimal type, never
a float, for the same reason as every amount on this API.
Both endpoints, the 404-versus-empty distinction, and the polling pattern in full are in
Tracking and line items.
If you would rather be told than ask, webhooks are signed, best-effort notifications that prompt you to re-read these same endpoints - a nudge, not a record, so the poll above stays your source of truth either way.
- Submitting orders - the full submission contract: limits, failure shapes, and reconciling the submissions you have made.
- Tracking and line items - shipments and per-line detail, in the depth this page deliberately skipped.
- Webhooks - being told about changes instead of polling blind.
- Errors - every code these endpoints return, and what to retry.