Getting started
This guide takes you from a credential you have just been given to a pagination loop you can leave running. It assumes the bearer-token path, which is how most partners are set up; if you were issued a client certificate instead, read Authentication first and rejoin at step 3.
1. Check the API is reachable
Section titled “1. Check the API is reachable”GET /v1/ping needs no credential at all. Run it before you have anything else working, so
that when a later call fails you already know whether the network and the edge are healthy.
curl -s https://api.partners.collaterate.com/v1/ping{ "status": "ok" }This endpoint touches nothing: no database, no authorizer, no lookup of you. That is what
makes it useful. A failing /v1/me next to a healthy /v1/ping tells you the problem is
your credential, not the network - and that distinction is worth wiring into your own
monitoring rather than discovering it during an incident.
2. Mint an access token
Section titled “2. Mint an access token”Your Partner Integrations contact provisions three things: a client_id and client_secret,
a grant of one or more site ids, and the scopes your client may request. For order access
that scope is partner-api/orders:read.
TOKEN=$(curl -s -X POST \ "https://api.partners.collaterate.com/oauth2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=$CLIENT_ID" \ -d "client_secret=$CLIENT_SECRET" \ -d "scope=partner-api/orders:read" \ | jq -r .access_token)The token endpoint is on the same host as every other call, so there is only one hostname to
configure per environment. Tokens are short-lived. Mint a new one when a call returns 401
rather than retrying the same token, and cache the one you hold rather than minting per
request - see Authentication.
3. Confirm who you are
Section titled “3. Confirm who you are”GET /v1/me is the right first authenticated call in any new integration or environment. It
requires a valid credential but no particular scope, so it isolates “my credential works”
from “my credential can do this specific thing”.
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/orders:read"]}grantedSiteIds is authoritative. If you filter GET /v1/orders by siteId, the value must
appear in this list. scopes is what you actually hold, which is not necessarily what you
asked for - see how scopes resolve.
4. Fetch a page of orders
Section titled “4. Fetch a page of orders”curl -s -H "Authorization: Bearer $TOKEN" \ "https://api.partners.collaterate.com/v1/orders?limit=2" | jq{ "orders": [ { "orderNumber": 1000234, "status": "SHIPPED", "placedOn": "2026-06-01T14:22:05.000Z", "updatedOn": "2026-06-01T14:22:05.000Z", "shippedOn": "2026-06-03T09:10:00.000Z", "purchaseOrder": "PO-99182", "projectName": "Q3 Retail Refresh", "siteId": 7, "totals": { "item": "1024.5000000000", "shipping": "42.0000000000", "tax": "81.9600000000", "order": "1148.4600000000" }, "closed": true } ], "nextCursor": "eyJ0IjoxNzc5MDMyMTI1MDAwLCJpIjoxMDAwMjM0fQ"}Two things to get right now, because they are what partners most often get wrong and only discover much later.
Query parameters
Section titled “Query parameters”| Parameter | Type | Behavior |
|---|---|---|
siteId |
integer | Narrows to one site. Must be one of your grantedSiteIds; anything else is a 400, never a silently-widened or silently-empty result. |
status |
string | Exact match on the order’s status value, as returned in the status field. Not a prefix or case-insensitive match. |
cursor |
string | Opaque token from a previous response’s nextCursor. |
limit |
integer | Page size. Defaults to 50. Values above 200 are capped at 200. A value that is not a positive integer falls back to 50 rather than erroring. |
5. The pagination loop
Section titled “5. The pagination loop”This is the first thing every integration builds and there are exactly two ways to get it
wrong: stopping too early, or never stopping. The single correct termination condition is
nextCursor is null. Not falsy, not empty string, not “this page returned fewer than
limit rows”.
Results are ordered by (placedOn, orderNumber). placedOn is set once at order creation
and never moves, which is what makes it safe to paginate over.
import requests
BASE_URL = "https://api.partners.collaterate.com/v1"
def fetch_all_orders(token: str, site_id: int | None = None) -> list[dict]: orders: list[dict] = [] cursor: str | None = None
while True: params: dict[str, str | int] = {"limit": 100} if site_id is not None: params["siteId"] = site_id if cursor is not None: params["cursor"] = cursor
response = requests.get( f"{BASE_URL}/orders", headers={"Authorization": f"Bearer {token}"}, params=params, timeout=10, ) response.raise_for_status() body = response.json()
orders.extend(body["orders"])
# JSON null decodes to None. This is the only correct stop condition: a page can # return fewer than `limit` rows and still carry a non-null cursor. cursor = body["nextCursor"] if cursor is None: break
return ordersThe same loop in shell, if that is a closer fit:
#!/usr/bin/env bashset -euo pipefail
BASE_URL="https://api.partners.collaterate.com/v1"CURSOR=""OUT=$(mktemp)echo "[]" > "$OUT"
while : ; do QS="limit=100" [ -n "$CURSOR" ] && QS="$QS&cursor=$CURSOR"
RESPONSE=$(curl -sf -H "Authorization: Bearer $TOKEN" "$BASE_URL/orders?$QS")
jq -s '.[0] + .[1].orders' "$OUT" <(echo "$RESPONSE") > "$OUT.tmp" mv "$OUT.tmp" "$OUT"
NEXT=$(echo "$RESPONSE" | jq -r '.nextCursor') [ "$NEXT" = "null" ] && break CURSOR="$NEXT"done
echo "Fetched $(jq 'length' "$OUT") orders."Note what neither loop does. It does not use an offset, and it does not infer “no more data” from a short page.
6. Fetch one order
Section titled “6. Fetch one order”curl -s -H "Authorization: Bearer $TOKEN" \ https://api.partners.collaterate.com/v1/orders/1000234 | jqThe response body is a single Order object, the same shape as an element of the orders
array above.
A 404 here has two indistinguishable causes: no such order, or an order that exists in a
site you are not granted. That is deliberate, and it changes how you should handle it - see
Tenancy and access.
7. Fetch its shipments and its line items
Section titled “7. Fetch its shipments and its line items”The order object is a header. Two sub-resources carry the detail most integrations need next,
both under the same partner-api/orders:read scope and both paginated the same way:
curl -s -H "Authorization: Bearer $TOKEN" \ https://api.partners.collaterate.com/v1/orders/1000234/shipments | jqcurl -s -H "Authorization: Bearer $TOKEN" \ https://api.partners.collaterate.com/v1/orders/1000234/items | jq/shipments is where tracking numbers live, and there is a published precedence rule for which
of two sources a number comes from, plus one invariant that tells you when it is safe to stop
polling. /items is where SKUs, quantities and per-line money live, and it returns cancelled
lines rather than dropping them. Both are covered in
Tracking and line items, which you should read before
building either.
One thing to note now: on both endpoints, an empty array is not the same as a 404. An empty
array means the order resolved and has nothing to show yet; a 404 means the order number does
not resolve for you at all.
8. What this API cannot do
Section titled “8. What this API cannot do”There is no change feed, and no updatedSince filter. Sending updatedSince is rejected
with a 400, on purpose, rather than ignored.
The updatedOn field is present but is not a watermark. It does not move when a shipment
posts, a tracking number is added, or item quantities change on an order that already exists;
an audit found 25.6% of orders already have a child record newer than the parent. A separate
internal process can also move an order into your site without touching it. A polling loop
keyed on updatedOn will silently miss real changes, including the shipment and tracking
updates most partners want most.
GET /v1/orders ordered by placedOn is therefore good for discovering new orders and
useless for detecting changes to ones you already hold.
What is missing is only the ability to ask which orders changed. Seeing current state for an
order you already know about works fine: GET /v1/orders/{orderNumber} and its /shipments and
/items sub-resources always reflect the present. So the supported pattern is to discover new
orders by paginating this endpoint, keep your own list of the ones still open, and re-request
those individually on your own schedule - see
Tracking and line items.
If you need push notification rather than polling, ask Partner Integrations what is currently
available; do not approximate it with updatedOn.
- Tracking and line items - shipments, tracking numbers, SKUs, and how to poll for them.
- Errors - the problem document, every code, and what to retry.
- Rate limits and quotas - build backoff before you need it.