REST API
The /api/v1 routes for flows, orders, events, approvals and webhook subscriptions, with request and response shapes.
The REST API is served at https://orderloop.app/api/v1. Its OpenAPI 3.1 document is public at
GET /api/v1/openapi.json (cacheable for five minutes), and the OpenAPI explorer renders
it. Its request bodies are generated from the schemas the routes validate with, so they match what
the server accepts. Response bodies are described here.
Authentication
Section titled “Authentication”Send Authorization: Bearer ol_live_…, an API token from the Team page (a signed-in dashboard
session cookie also works, and so does the workspace password, a break-glass operator sign-in set
by your administrator). Each route needs
a role: reads need viewer, approval decisions approver, and everything else that changes state
(starting, cancelling, events, webhook subscriptions, integrations) operator; a token or person without it gets
403. See Team and roles. Four routes are exceptions: GET /api/v1/openapi.json is public, POST /api/v1/webhooks/events and POST /api/v1/webhooks/orders are
authenticated by an HMAC signature instead (see signed events and signed orders) with an integration’s key, and so is POST /api/v1/channels/shopify/orders (see Channel adapters).
Errors are JSON: { "error": "…" }, plus issues (one entry per invalid field, with its path
and message) on a 400 for an invalid body.
401 means a missing, wrong, revoked or expired token (an expired token’s error says when it expired), 403 a role that can’t use the route (or, with a session cookie, a person whose role must use two-factor sign-in and hasn’t set it up, or a cookie request from another origin), 429 too many wrong bearer tokens from one address (more
than 20 bearers that aren’t ol_live_ tokens in 15 minutes, which only guesses at the workspace
password produce; see Retry-After), 404 an unknown route or order, and 413 a request body over its cap (1 MiB; 5 MiB for EDI), and 500 an unexpected error, which answers { "error": "Internal error" } without its details.
Routes
Section titled “Routes”| Method | Path | Body | Response |
|---|---|---|---|
GET |
/api/v1/flows |
{ flows } |
|
POST |
/api/v1/orders |
{ flowId, version?, input, vars? } |
201 { orderId }, or 200 { orderId, replayed: true } |
GET |
/api/v1/orders?model=&channel=&status=&q=&limit=&cursor= |
{ orders, nextCursor } |
|
GET |
/api/v1/orders/:id |
The order’s live state | |
POST |
/api/v1/orders/:id/events |
{ event, payload? } |
{ delivered, nodeId? } |
POST |
/api/v1/orders/:id/approvals/:nodeId |
{ decision, by?, note? } |
{ ok: true } |
POST |
/api/v1/orders/:id/cancel |
{ reason? } |
{ ok: true } |
GET |
/api/v1/audit-events?type=&before=&limit= |
{ events } (operator) |
|
GET |
/api/v1/integrations |
{ integrations } (operator) |
|
POST |
/api/v1/integrations |
{ name, events?, startOrders?, flows?, channel? } |
201 { integration, secret } (operator) |
POST |
/api/v1/integrations/:id/rotate |
{ integration, secret } (operator) |
|
DELETE |
/api/v1/integrations/:id |
{ ok: true } (operator) |
|
GET |
/api/v1/webhook-subscriptions |
{ subscriptions } |
|
POST |
/api/v1/webhook-subscriptions |
{ url, events? } |
201 { subscription, secret } |
DELETE |
/api/v1/webhook-subscriptions/:id |
{ ok: true } |
|
GET |
/api/v1/webhook-subscriptions/:id/deliveries |
{ deliveries } |
|
POST |
/api/v1/webhooks/events |
{ orderId, event, payload? } |
200 { delivered: true, nodeId }, 404 or 409 |
POST |
/api/v1/webhooks/orders |
{ model, channel, externalId, lines, … } |
201 { orderId, flowId }, or 200 on a replay |
POST |
/api/v1/channels/shopify/orders |
A Shopify orders/create payload |
201 { orderId, flowId }, 200 on a replay or { ignored: true, topic } |
POST |
/api/v1/channels/edi/850 |
Raw X12 (application/edi-x12 or text/plain) |
200 { sender, results } (operator) |
GET /api/v1/flows lists every flow, most recently updated first, with id, name,
description, model, channels, publishedVersion (null until the first publish), updatedAt and
nodeCount. Flows are edited and published in the dashboard’s editor; the API starts orders on them.
Start an order
Section titled “Start an order”POST /api/v1/ordersIdempotency-Key: po-NW-7781
{ "flowId": "tpl-b2b-order", "input": { "model": "b2b", "channel": "api", "customerId": "cus_northwind", "currency": "USD", "lines": [{ "sku": "WID-100", "qty": 50 }], "meta": {} }}flowIdnames a flow with a published version.version(a positive integer) pins one; it defaults to the latest.input.modelis required: one ofb2b,b2c,d2c,marketplace,subscription,procurement,replenishment.input.channelis one ofweb,mobile,pos,marketplace,email,edi,api,manual,scheduleand defaults toapi. See Commerce models.input.title(up to 140 characters) andinput.customerIdare optional.input.externalId(up to 200) is the order’s id in its channel;input.contactis{ email, name?, phone? }for a buyer without a customer account;input.fulfillmentis{ method, locationId? }.input.currencydefaults toUSDand must have an exchange rate in the workspace; another code returns400. See Currency.input.linesare{ sku, name?, qty, unitPrice?, sellerId?, locationId? }(locationIdpins the store a line ships from; route fulfillment normally sets it). Pricing nodes fill in prices, in the order’s currency.input.documentText(up to 50,000 characters) is an inbound email or PO for the Extract order node; it is stored with the order’s documents before the run starts.input.metais free-form and is the run context’sinput. Some keys mean something to nodes:shipTo(theno_po_boxpolicy rule) andsimulate(forces a failure path in the built-in commerce steps; see Built-in flows).varsseeds the run’s variables.
Idempotency. With an Idempotency-Key header (at most 255 characters; longer returns 400), a repeat of the key
returns 200 and the order the first request created, even when two requests race. If the first
request failed after recording the order, the repeat finishes starting it. The key is bound to a
SHA-256 fingerprint of the body (flow, version, input and vars, ignoring property order): a repeat
with a different body returns 422 and starts nothing.
List orders
Section titled “List orders”GET /api/v1/orders returns a page of orders, newest first (by created_at, then id). limit is 1 to 200 (default 50); nextCursor is an opaque string to pass back as cursor for the next page, and is null on the last page. A cursor the API did not issue is 400. Filter with
model, channel, status (one of queued, running, waiting, completed, declined, cancelled, failed, rolled_back) and q, which matches the order id, title,
customer id, contact email or external id. Each row has id, model, channel, externalId, contactEmail, status, title, flowId, flowVersion, instanceId,
generation, customerId, currency, total, waitingOn (the open waits’ titles),
waitingAt (the labels of the steps holding them), waitingSince, createdAt and updatedAt
(epoch milliseconds).
Read an order
Section titled “Read an order”GET /api/v1/orders/:id returns the order’s live state, the same state the order page renders:
| Field | Meaning |
|---|---|
orderId, status, reason |
Identity, status, and the reason when cancelled or failed |
order |
The order with computed subtotal, tax and total |
flow |
The pinned flow version, including its graph |
nodes |
Per node: status, runs, summary, output, error, branches, startedAt, endedAt |
edges |
Per edge: pending, active or inactive |
waits |
Open waits: nodeId, kind (approval or event), title, event, assignee, amount, deadline, since |
vars |
Run variables |
timeline |
Entries with at, kind, message, nodeId, data |
instanceId, generation |
The id of the run driving the order, and its generation (it goes up by one on each continue as new) |
rollback |
{ outcome, error } once a rollback has run |
updatedAt |
When the state last changed (epoch milliseconds) |
Node statuses are pending, running, waiting, succeeded, failed, skipped, rolled_back.
Deliver an event
Section titled “Deliver an event”POST /api/v1/orders/ord_…/events
{ "event": "payment.received", "payload": { "amount": 3106.78 } }The event goes to the node waiting for it, and its payload lands in vars under the event name
with every run of non-alphanumerics replaced by _ (payment.received → vars.payment_received).
The response is { delivered: true, nodeId }, or { delivered: false } when no node is waiting for
that event yet. An unknown order is 404. See Approvals and events.
Decide an approval
Section titled “Decide an approval”POST /api/v1/orders/ord_…/approvals/approval
{ "decision": "approved", "by": "dana@example.com", "note": "Paid down last week" }decision is approved or rejected. A person or an API token decides as themselves (by is ignored); only the workspace password may name by, which defaults to api. The decision takes effect at once;
the dashboard’s undo window does not apply to the API. An unknown order is 404, and a node that
isn’t waiting for an approval (already decided, or never reached) is 409.
Cancel
Section titled “Cancel”POST /api/v1/orders/:id/cancel stops the order’s run, releases reserved stock, voids
payment authorizations, cancels shipments and staged pickups, restocks shipped stock, reverses on-account invoice charges, revokes issued digital codes, cancels seller orders, reverses scheduled payouts and sends order.cancelled. reason defaults to
Cancelled via API. Cancelling an order that has already finished changes nothing and returns
409 { error: "The order has already finished" }; an unknown order is 404.
Integrations
Section titled “Integrations”An integration is one external system’s signing key and what it may do. Create one per system, so
a leaked key is revoked on its own and a payment provider can’t start orders. All four routes
need the operator role.
POST /api/v1/integrationstakesname(1 to 100 characters) and the scope:events(event names it may deliver to/api/v1/webhooks/events, or["*"]; default none),startOrders(may post to/api/v1/webhooks/orders; defaultfalse),flows(flow ids an order body may name inflowId, or["*"]; default none) andchannel(only orders on this channel, both those it starts and those it sends events to; defaultnull, any). It returns201 { integration, secret }. Thesecret(whsec_…) is shown once;integration.idgoes in the signature ask=.GET /api/v1/integrationslists integrations, newest first, without secrets. Each hasid,name, the scope,createdAt,createdBy,lastUsedAt,revokedAtandpreviousExpiresAt.POST /api/v1/integrations/:id/rotatereturns a newsecret. The old secret keeps working for 24 hours (previousExpiresAtshows when it stops), so the sender can switch without downtime. Rotating again replaces the overlap.404for an unknown or revoked integration.DELETE /api/v1/integrations/:idrevokes it at once, including any secret still in its overlap. Requests signed with it get401.404if it doesn’t exist or is already revoked.
Creating, rotating and revoking are recorded in the audit log.
Signed events
Section titled “Signed events”External systems resume orders without a token by signing the raw request body with their integration’s secret:
POST /api/v1/webhooks/eventsX-OrderLoop-Signature: t=<unix seconds>,k=<integration id>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
{ "orderId": "ord_…", "event": "payment.received", "payload": { "amount": 3106.78 } }Inbound orders shows how to build the header in a shell and in
JavaScript. Signatures more than five minutes from now are rejected. A delivered event’s signature
is recorded, so the same signed request sent again inside that window is not delivered twice. A 409 is not
recorded and may be retried.
The order routes bound their bodies the same way: POST /api/v1/orders/{id}/events takes event
(1 to 100 characters) and an optional payload of at most 32 KiB of JSON,
POST …/approvals/{nodeId} takes decision, by (at most 200 characters) and note (at most
2000), and POST …/cancel takes reason (at most 500). Anything else is a 400 with issues.
The order page enforces the same bounds.
| Status | Meaning |
|---|---|
200 |
Delivered: { delivered: true, nodeId } |
404 |
No such order |
409 |
No node is waiting for that event yet: { delivered: false }. Retry later |
200 |
A replay: { delivered: true, duplicate: true }, delivering nothing |
400 |
Body is not { orderId, event, payload? } (event is 1 to 100 characters, payload at most 32 KiB of JSON) |
413 |
Body over 1 MiB |
401 |
Missing, malformed, stale or wrong signature; no k=; an unknown or revoked integration |
403 |
The integration may not deliver that event, or is bound to a channel other than the order’s |
A channel-bound integration gets 404 for an order that doesn’t exist, so it can’t probe other channels’ ids.
Signed orders
Section titled “Signed orders”POST /api/v1/webhooks/orders starts an order from a storefront, marketplace, POS or other
channel, signed the same way as events. The body is input plus a required channel and
externalId and optional flowId, source, shipTo and vars; it needs at least one line or documentText. The flow is flowId, else the
workspace’s order routes (set by your administrator), else the model’s default. channel and externalId are the idempotency key.
Statuses: 201 { orderId, flowId } started, 200 { orderId, replayed: true } replay, 400 invalid body or unsupported currency, 401 bad
signature or unknown or revoked integration, 403 the integration may not start orders, is bound to another channel, or may not choose that flowId, 409 flow not published, 422 same order with different contents. See Inbound orders.
Channel adapters
Section titled “Channel adapters”POST /api/v1/channels/shopify/orders takes a Shopify orders/create webhook, verified by the
X-Shopify-Hmac-Sha256 header (base64 HMAC-SHA256 of the raw body with your Shopify app’s
client secret, which your administrator adds to the workspace) rather than a token. Statuses: 201 { orderId, flowId } started, 200 { orderId, replayed: true }
replay, 200 { ignored: true, topic } for any other X-Shopify-Topic, 400 not JSON, unmappable
(a line item without a SKU) or invalid, or an unsupported currency, 401 bad signature, 409 flow not published,
422 same order with different contents, 503 the workspace hasn’t enabled Shopify.
POST /api/v1/channels/edi/850 takes a raw X12 interchange and needs the operator role. The
sender (ISA06) must be in the workspace’s EDI partner list. It answers 200 { sender, results } with one
{ control, poNumber?, status, orderId?, flowId?, replayed?, error? } per transaction; 400 is
not valid X12, 403 an unlisted sender or a caller below operator. Both start orders once per
order:<channel>:<externalId>. See Channel adapters.
Audit events
Section titled “Audit events”GET /api/v1/audit-events returns the security audit log, newest first, for feeding a log
system; it needs the operator role. type filters to auth, team, token, api, oauth or integration; limit is
1 to 500 (default 100); before (milliseconds since the epoch) pages back: pass the oldest at
you have. Each event has id, at, action, actor, actorId, actorKind, target, detail
(an object or null) and ip. See Audit log for the actions.
Webhook subscriptions
Section titled “Webhook subscriptions”See Outbound webhooks for the events, payloads and signatures.
POST /api/v1/webhook-subscriptionstakesurl(https, or plain http forlocalhost,127.0.0.1and[::1]) andevents: any oforder.paid,order.invoiced,order.shipped,order.ready_for_pickup,order.delivered,order.refunded,order.payout_scheduled,order.completed,order.declined,order.cancelled,order.failed, or["*"](the default). It returns the subscription and itssecret, once.GETlists subscriptions without their secrets.DELETE /api/v1/webhook-subscriptions/:iddeletes one (404if it doesn’t exist).GET /api/v1/webhook-subscriptions/:id/deliveriesreturns the latest 50 deliveries, newest first:id,event,orderId,payload,status(pending,deliveredorfailed),attempts,responseStatus,lastError,createdAtanddeliveredAt.
Other endpoints
Section titled “Other endpoints”These aren’t part of /api/v1.
GET /healthanswers{ ok: true, service: "orderloop-web" }, with no authentication.GET /health/deepalso checks the database, document storage and the engine, with no authentication. It answers{ ok, service, checks }, where each check is"ok"or"fail", and503when any check fails./mcp,/a2a,/oauth/*(except the/oauth/authorizeconsent page) and/.well-known/oauth-*are the MCP server, the A2A endpoint and their OAuth endpoints.GET /.well-known/agent-card.jsonis the A2A Agent Card, public and cached for five minutes./agents/order-agent/:idand/agents/assistant-agent/:nameare the dashboard’s live sockets for a signed-in person. They are not a public API; the assistant route always reaches the caller’s own assistant.GET /docs/orders/…serves an order’s stored documents (invoices, POs, inbound documents) to a signed-in person.