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:

PLACEDACCEPTEDPREPARINGDISPATCHEDDELIVERED
PLACEDREJECTED
anyCANCELLED

List Orders

GET/connect/v1/supplier/ordersscope: orders:read

List 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

ParameterTypeDescription
updatedAfterstring (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.
cursorstringBrowse 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.
limitintegerItems per page (1 — 100, default 50).
statusstringFilter 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

GET/connect/v1/supplier/orders/:idscope: orders:read

Retrieve 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

POST/connect/v1/supplier/orders/:id/acceptscope: orders:write

Accept a PLACED order. Reserves stock and moves to ACCEPTED status. Supports If-Match (see Concurrency).

POST/connect/v1/supplier/orders/:id/rejectscope: orders:write

Reject a PLACED order. Releases reserved stock. Supports If-Match (see Concurrency).

Reject Request Body

ParameterTypeDescription
reasonrequiredstringReason 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

POST/connect/v1/supplier/orders/:id/dispatchscope: orders:write

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

ParameterTypeDescription
trackingCodestringTracking code / waybill number from your carrier (max 200 chars).
notesstringFree-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

POST/connect/v1/supplier/orders/:id/cancelscope: orders:write

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

ParameterTypeDescription
reasonstringWhy 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.

POST/connect/v1/supplier/orders/batch/acceptscope: orders:write

Accept multiple PLACED orders in one request (max 50). Body: { orderIds: [1, 2, ...] }.

Body:

json
{
  "orderIds": [12345, 12346, 12347]
}

Response (200 OK):

json
{
  "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" }
  ]
}
POST/connect/v1/supplier/orders/batch/dispatchscope: orders:write

Dispatch multiple orders in one request (max 50). Each item can include its own trackingCode and notes.

Body:

json
{
  "orders": [
    { "orderId": 12345, "trackingCode": "HP001", "notes": "Handed to HayPost" },
    { "orderId": 12346, "trackingCode": "HP002" }
  ]
}