Order API
Receive and manage incoming orders from buyers on the marketplace.
Order Lifecycle
Orders follow a strict state machine. The Connect API allows you to transition orders through these states:
PLACED → REJECTED
any → CANCELLED
List Orders
/connect/v1/supplier/ordersscope: orders:readList orders for your tenant. Runs in one of two modes: browse (newest first) or change feed (resumable). Pick the mode that matches what you are building.
Which mode do I need?
Browse (?cursor=<orderId>) walks backwards through history, newest first. It answers “show me the last N orders”. It is not resumable: an order created after you stored the cursor has a higher id and will never appear, and a status change on an order you already read is not re-emitted. Storing a browse cursor as a sync checkpoint silently loses orders.
Change feed (?updatedAfter=<instant>, then the returned nextCursor) walks forwards in (updatedAt, id) order. Every mutation bumps updatedAt, so a changed order re-appears ahead of your position and is delivered again. This is the mode ERP and sync integrations must use. Delivery is at-least-once — dedupe by order id + status on your side.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
updatedAfter | string (ISO-8601 instant) | Start the change feed at this point in time, e.g. 1970-01-01T00:00:00Z for a full backfill. Presence of this parameter selects feed mode. |
cursor | string | Browse mode: the numeric id of the last order from the previous page. Feed mode: the opaque nextCursor from the previous page — treat it as a token, do not parse it. An opaque cursor also selects feed mode on its own, so you only need updatedAfter on the very first call. |
limit | integer | Items per page (1 — 100, default 50). |
status | string | Filter by status: PLACED, ACCEPTED, PREPARING, DISPATCHED, DELIVERED, REJECTED, CANCELLED. |
# --- Change feed: first call starts at the beginning of time ---
curl "https://api.scoperly.com/connect/v1/supplier/orders?updatedAfter=1970-01-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer $TOKEN"
# Response (sanitized OrderSummaryResponse — branch / full address stripped):
# {
# "items": [
# {
# "id": 42,
# "uuid": "e3f5a2c1-...-...-...-...",
# "orderNumber": "SCP-2025-000042",
# "status": "PLACED",
# "buyerName": "Café Araks",
# "supplierName": "Avan Dairy",
# "totalAmount": 1250.00,
# "currency": "AMD",
# "paymentStatus": "UNPAID",
# "createdAt": "2025-03-20T09:15:00Z",
# "updatedAt": "2025-03-20T09:15:00Z",
# "deliveredAt": null,
# "cancelledAt": null,
# "version": "1742461200000000"
# }
# ],
# "nextCursor": "1742461200000000_42",
# "hasMore": true
# }
# --- Subsequent calls: pass nextCursor back, unchanged ---
curl "https://api.scoperly.com/connect/v1/supplier/orders?cursor=1742461200000000_42&limit=50" \
-H "Authorization: Bearer $TOKEN"
# nextCursor is ALWAYS returned, including on an empty page (it echoes your own
# position back). Persist it after you have durably stored the page — never before.
# hasMore=false means "you are caught up", not "the feed ended": keep polling with
# the same cursor and new changes will arrive.
# --- Browse mode (newest first, NOT resumable) ---
curl "https://api.scoperly.com/connect/v1/supplier/orders?status=PLACED&limit=10" \
-H "Authorization: Bearer $TOKEN"Order Detail
/connect/v1/supplier/orders/:idscope: orders:readRetrieve full order details including sanitized line items, a reduced delivery location (city + country only), and — when the buyer matches your supplier's CRM — a nested internalPartner block (your own classification of the buyer, exposed only as externalId / name / category / route).
curl "https://api.scoperly.com/connect/v1/supplier/orders/12345" \
-H "Authorization: Bearer $TOKEN"
# Response:
# {
# "order": {
# "orderNumber": "SCP-2025-000042",
# "status": "PLACED",
# "buyerName": "Café Araks",
# "supplierName": "Avan Dairy",
# "lines": [
# {
# "id": 981,
# "productId": 5512,
# "sellerExternalId": "SKU-001",
# "productName": "Organic Green Tea 500g",
# "unitOfMeasure": "pcs",
# "quantity": 50,
# "unitPrice": 12.50,
# "vatRate": 20.00,
# "vatAmount": 125.00,
# "lineTotal": 625.00,
# "lineTotalInclVat": 750.00,
# "currency": "AMD"
# }
# ],
# # sellerExternalId is YOUR catalog id — the exact externalId you sent in the
# # catalog upsert — snapshotted when the order was placed. Join on this, not on
# # productId: it survives a later catalog re-import that reassigns external ids.
# # It is null for products created directly in the supplier portal.
# "subtotal": 625.00,
# "deliveryFee": 0,
# "deliveryFeeSource": "AUTO_RATE",
# "vatTotal": 125.00,
# "totalAmount": 625.00,
# "currency": "AMD",
# # Note: totalAmount is the NET total (subtotal + deliveryFee − discount).
# # VAT is denormalised in vatTotal for the buyer-facing breakdown.
# # Buyer pays subtotal + deliveryFee + vatTotal − discount = the gross amount.
# "amendmentStatus": null,
# "deliveryCity": "Yerevan",
# "deliveryCountry": "AM",
# "requestedDeliveryDate": "2025-03-22",
# "createdAt": "2025-03-20T09:15:00Z"
# },
# "internalPartner": {
# "externalId": "CLI-0042",
# "name": "Café Araks",
# "category": "HORECA",
# "route": "Yerevan-Center-Mon"
# }
# }
#
# If the buyer is NOT matched in your supplier's internal partner list,
# "internalPartner" will be null.
#
# The sanitized order intentionally omits internal tenant IDs, branch,
# and the full delivery address PII.Acting on the order you actually read
Every order carries a version — an opaque token that changes whenever the order does. Send it back as an If-Match header on accept, reject, dispatch or cancel, and the request is refused with 409 ORDER_VERSION_CONFLICT if the order moved in the meantime — the buyer cancelled it, or its lines were amended — instead of applying to an order you were no longer looking at.
The header is optional: omit it and behaviour is unchanged. Treat the token as opaque — compare it, do not parse it.
# Read the order, keep its version
VERSION=$(curl -s "https://api.scoperly.com/connect/v1/supplier/orders/12345" \
-H "Authorization: Bearer $TOKEN" | jq -r '.order.version')
# Act on exactly that version
curl -X POST https://api.scoperly.com/connect/v1/supplier/orders/12345/dispatch \
-H "Authorization: Bearer $TOKEN" \
-H "If-Match: $VERSION" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "trackingCode": "HP123456789AM" }'
# 409 if it changed:
# {
# "type": "ORDER_VERSION_CONFLICT",
# "status": 409,
# "detail": "The order changed since you read it. Re-read it and decide again — the current version is 1742461200000000."
# }Accept / Reject
/connect/v1/supplier/orders/:id/acceptscope: orders:writeAccept a PLACED order. Reserves stock and moves to ACCEPTED status. Supports If-Match (see Concurrency).
/connect/v1/supplier/orders/:id/rejectscope: orders:writeReject a PLACED order. Releases reserved stock. Supports If-Match (see Concurrency).
Reject Request Body
| Parameter | Type | Description |
|---|---|---|
reasonrequired | string | Reason for rejection (shown to buyer). |
# Accept an order
curl -X POST https://api.scoperly.com/connect/v1/supplier/orders/12345/accept \
-H "Authorization: Bearer sck_live_abc123..." \
-H "Idempotency-Key: $(uuidgen)"
# Reject an order
curl -X POST https://api.scoperly.com/connect/v1/supplier/orders/12345/reject \
-H "Authorization: Bearer sck_live_abc123..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "reason": "Product temporarily out of stock" }'Dispatch
/connect/v1/supplier/orders/:id/dispatchscope: orders:writeMark an ACCEPTED or PREPARING order as dispatched. Optionally include tracking information. If the order has a buyer-pending amendment (amendmentStatus = PENDING_BUYER_CONFIRMATION) the call returns 422 AMENDMENT_IN_FLIGHT — wait for the buyer to accept or reject before dispatching. Supports If-Match (see Concurrency).
Request Body (optional)
| Parameter | Type | Description |
|---|---|---|
trackingCode | string | Tracking code / waybill number from your carrier (max 200 chars). |
notes | string | Free-text notes shown to the buyer (max 1000 chars). |
curl -X POST https://api.scoperly.com/connect/v1/supplier/orders/12345/dispatch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"trackingCode": "HP123456789AM",
"notes": "Handed to HayPost, ETA Mon evening."
}'Cancel
/connect/v1/supplier/orders/:id/cancelscope: orders:writeCancel an order you have already taken. Allowed while the order is PLACED, ACCEPTED or PREPARING — once DISPATCHED the goods are with the buyer and cancellation is a dispute, not a state transition. Stock and credit are compensated exactly as they are for a cancellation made in the supplier portal. Supports If-Match (see Concurrency).
Request Body (optional)
| Parameter | Type | Description |
|---|---|---|
reason | string | Why the order was cancelled (max 500 chars). Shown to the buyer. Defaults to "Cancelled via API". |
Use this whenever your own system cancels an order that came from the marketplace. If you skip it, the two systems diverge silently: your ERP shows the order cancelled while the buyer still sees an order being fulfilled. Returns 422 ORDER_CANNOT_CANCEL if the order has moved past PREPARING, and 422 AMENDMENT_IN_FLIGHT while a buyer amendment is awaiting confirmation.
curl -X POST https://api.scoperly.com/connect/v1/supplier/orders/12345/cancel \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "reason": "Warehouse shortage — cannot fulfil" }'Batch Operations
Both batch endpoints accept up to 50 orders per call and support Idempotency-Key. Partial failures are reported per-item with a structured errorCode (e.g. ORDER_NOT_FOUND, CONFLICT) — the overall request still returns 200 OK.
/connect/v1/supplier/orders/batch/acceptscope: orders:writeAccept multiple PLACED orders in one request (max 50). Body: { orderIds: [1, 2, ...] }.
Body:
{
"orderIds": [12345, 12346, 12347]
}Response (200 OK):
{
"succeeded": 2,
"failed": 1,
"results": [
{ "orderId": 12345, "status": "accepted" },
{ "orderId": 12346, "status": "accepted" },
{ "orderId": 12347, "status": "failed", "errorCode": "ORDER_NOT_FOUND", "error": "Order not found: 12347" }
]
}/connect/v1/supplier/orders/batch/dispatchscope: orders:writeDispatch multiple orders in one request (max 50). Each item can include its own trackingCode and notes.
Body:
{
"orders": [
{ "orderId": 12345, "trackingCode": "HP001", "notes": "Handed to HayPost" },
{ "orderId": 12346, "trackingCode": "HP002" }
]
}