Skip to content

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.

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.

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

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.

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

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

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

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

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.

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.

fetch_all_orders.py
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 orders

The same loop in shell, if that is a closer fit:

fetch-all-orders.sh
#!/usr/bin/env bash
set -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.

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

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

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:

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

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.