Skip to content

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.

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):

Terminal window
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.

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:

Terminal window
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, and variantId: null on a POD product is part of it. Send the pair back verbatim on the order line - a placeholder, a zero, or an empty string in place of that null is a rejected line, not a tolerated approximation.
  • artworkSource decides what the line must carry. USER_UPLOAD means the line needs an artworkUrl; STORED means it needs nothing - artwork is already on file; TEMPLATE means templateVariables, filled in from the template endpoint. A null on a POD product 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.

Terminal window
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.

order.json
{
"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
}
]
}
Terminal window
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @order.json \
https://api.partners.collaterate.com/v1/orders

Notice 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:

  • partnerOrderId is 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 as 409 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_request before a submission record exists.

The per-field limits, the line shape in full, and the reasons behind both rules are in Submitting orders.

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:

Terminal window
curl -s -H "Authorization: Bearer $TOKEN" \
https://api.partners.collaterate.com/v1/orders/submissions/3fa85f64-5717-4562-b3fc-2c963f66afa6

Read 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.

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:

Terminal window
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.partners.collaterate.com/v1/orders/1000234/shipments" | jq

Read 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:

  • trackingNumbers empty and shippedOn: null - not shipped yet. Keep polling.
  • trackingNumbers non-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.