Buyers place orders in their trade portal. Suppliers handle invoicing. They can optionally offer Stripe-hosted customer payments after confirming the final total. Order requests and trade credit remain available. Use the API to pull orders into your ERP or accounts system and to keep their status up to date.
Order statusesLink to this section#
| Status | Meaning |
|---|---|
new | Placed |
pending | On hold |
acknowledged | Seen and being picked |
processed | Ready for dispatch |
dispatched | Sent by the supplier |
completed | Supplier marked complete |
cancelled | Canceled with a customer-facing reason |
Staff may skip intermediate picking stages. Moving backwards or reopening a completed or canceled order requires an internal correctionReason. Previous history stays. Corrections never alter payment state. Read revision and send expectedRevision with updates to reject stale edits.
Create an order for a customerLink to this section#
POST/api/v1/orders/previewwrite key
Requires orders.create and access to the customer. Send customerAccountId and lines containing productId and integer qty (1 to 1,000,000). Choose each ordinary product once, at most 100 lines. A line may include optional configuration containing the returned measurement and choice keys; different configurations of one product remain separate. Optional lineKey gives each preview line a stable caller identity. An additional additionalDiscountBp needs pricing permission and discountReason. Configured order reads include an immutable configuration snapshot with inputs, specification, version, rounding and charges. The preview returns total, fingerprint, current customer prices and currency. Paused accounts, unavailable products and quantities below the minimum are refused. A prices-only catalog still accepts an order entered here; buyers cannot send one themselves while that is off.
POST/api/v1/orderswrite key
Send the same customer and lines, expectedTotal from the preview, expectedBasket set to its fingerprint, and a fresh UUID requestKey. Optional poNumber (60 characters) and customer-visible notes (2,000 characters) are supported. Reuse exactly the same request and key after an uncertain response: a successful retry returns the original order rather than creating another. Changed prices require another review. The result is placed with orderId and reference, or prices_changed, empty or unavailable.
Staff act under their own identity. The customer's cart is untouched. Customer email contacts may receive a copy; this does not charge payment. Sales Rep has creation access by default; processing and delivery updates require the separate orders.process permission.
Delivery and trackingLink to this section#
PATCH/api/v1/orders/{id}/deliverywrite key
Requires orders.process. Send deliveryMethod: unspecified, courier, own_delivery or collection. Optional courierName (100 characters), trackingReference (120 characters) and trackingUrl (1,000 characters) are customer-visible. Tracking links must use HTTPS without embedded login credentials. Omitted optional details clear them. Send expectedRevision from the current order. There is one delivery record per order; carrier integrations and split shipments are not included. Blank courier details never imply own delivery.
List ordersLink to this section#
GET/api/v1/orders
Orders newest first. The list covers the 200 most recent orders that match your filters. To pick up new orders, poll with status=new.
| Name | Type | Required | Description |
|---|---|---|---|
status | string | no | One of the statuses above. |
customer | string | no | Customer id or account code. |
limit | integer | no | 1 to 100. Default 100. |
offset | integer | no | Rows to skip. Default 0. |
curl "https://tradecatalog.app/api/v1/orders?status=new" \
-H "Authorization: Bearer $TRADECATALOG_API_KEY"
const res = await fetch('https://tradecatalog.app/api/v1/orders?status=new', {
headers: { Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}` },
})
const { data: orders } = await res.json()
{
"data": [
{
"id": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e",
"reference": "SO-000142",
"status": "new",
"total": 31460,
"currency": "GBP",
"poNumber": "PO-88231",
"createdAt": "2026-09-27T07:48:12.000Z",
"customer": "Oakfield Joinery"
}
],
"nextOffset": null
}
| Field | Type | Description |
|---|---|---|
id | string | Order id. |
reference | string | Order number buyers see, like SO-000142. |
status | string | See statuses. |
total | integer | Order total as priced (see taxLabel), in minor units of currency. |
currency | string | Fixed when the order was placed. |
poNumber | string or null | The buyer’s purchase order number. |
createdAt | string | When it was placed. |
customer | string | Customer name. Get the account code from GET /orders/{id}. |
Get one orderLink to this section#
GET/api/v1/orders/{id}
The order with its lines and status history.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | Order id. |
curl https://tradecatalog.app/api/v1/orders/5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e \
-H "Authorization: Bearer $TRADECATALOG_API_KEY"
const res = await fetch(`https://tradecatalog.app/api/v1/orders/${orderId}`, {
headers: { Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}` },
})
const { data: order } = await res.json()
{
"data": {
"id": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e",
"customerAccountId": "7d2e4f0a-1b3c-4d5e-8f90-a1b2c3d4e5f6",
"placedBy": "Fq3L0cZ7tN1pW9aYx2Rk",
"reference": "SO-000142",
"poNumber": "PO-88231",
"notes": "Deliver to the Hebden Road site, gate 2.",
"status": "new",
"cancelReason": null,
"currency": "GBP",
"total": 31460,
"createdAt": "2026-09-27T07:48:12.000Z",
"updatedAt": "2026-09-27T07:48:12.000Z",
"customer": { "name": "Oakfield Joinery", "accountCode": "OAK01" },
"lines": [
{
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"orderId": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e",
"productId": "3f8a2c1e-9b4d-4e6f-a7c8-1d2e3f4a5b6c",
"sku": "HX-8510",
"name": "Hex bolt M10 x 50 zinc",
"unitLabel": "Box of 100",
"qty": 20,
"unitPrice": 1573,
"listPrice": 1850,
"priceLabel": "Brand discount −15%",
"lineTotal": 31460
}
],
"events": [
{
"status": "new",
"note": null,
"createdAt": "2026-09-27T07:48:12.000Z",
"actor": "Sam Patel"
}
]
}
}
| Field | Description |
|---|---|
customerAccountId | The customer’s id. |
placedBy | Id of the person who entered it. source distinguishes buyer and staff orders. |
notes | The buyer’s delivery or order notes. |
cancelReason | Why it was canceled, if it was. |
lines | What was ordered at what price. A snapshot: later price changes never alter it. productId is null if the product has since been deleted. |
lines[].priceLabel | The rule that set the price, as on Prices. |
lines[].priceLabel is Price on request | The product has no price: unitPrice and lineTotal are 0 and the line isn’t in total. The supplier confirms its price. |
events | Status history, oldest first. actor is the person’s name, or null. |
Update or correct an orderLink to this section#
PATCH/api/v1/orders/{id}write key
Changes an order’s status. Returns the updated order in the same shape as GET /orders/{id}.
- Normal forward moves follow the fulfillment stages above. Other moves require a private correction reason; a repeated current status makes no change.
- Canceling needs a
note, which is shown to the buyer as the reason. - Status changes may email customer contacts, including the public note. A generated email does not prove delivery.
- The change is recorded in the order’s history against the person who made the key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | Order id. |
status | body | string | yes | The new status. |
note | body | string | no | Up to 1,000 characters. Required when canceling. |
curl -X PATCH https://tradecatalog.app/api/v1/orders/5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e \
-H "Authorization: Bearer $TRADECATALOG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "dispatched", "note": "DPD tracking 1554 2210 0931" }'
const res = await fetch(`https://tradecatalog.app/api/v1/orders/${orderId}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.TRADECATALOG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: 'acknowledged' }),
})
const { data: order } = await res.json()
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The move isn’t allowed, e.g. “An order that is received can’t be marked completed”, or canceling without a note. |
| 403 | insufficient_scope | The key is read-only. |
| 404 | not_found | No such order in this workspace. |
Retrying a PATCH that already succeeded returns 400 invalid_request, because the order has moved on. Check the order’s status before treating that as a failure.
Not availableLink to this section#
The API can’t place orders for a buyer, or change an order’s lines, quantities or prices. Buyers place orders in their trade portal; changes to an order are agreed with the customer and invoiced in your own system.
Order lists also include paymentStatus: null for usual account terms, or the optional Stripe payment status. Fulfillment status is separate and never proves payment. The order detail page is the place to review the final confirmed total and approve payment. The API cannot create or approve a payment.
internalNote (2,000 characters) and correctionReason (1,000 characters) are staff-only history fields. note remains public, including old notes. Send expectedRevision to reject a concurrent edit with an invalid request. Staff API reads include private history; buyer tools and portal reads exclude staff identities and private fields. Canceling still requires a public reason and withdrawal or full refund of an outstanding payment.