REST API
The /api/v1 routes for flows, orders, events, approvals and webhook subscriptions, with request and response shapes.
The REST API is served by the web Worker under /api/v1. Its OpenAPI 3.1 document is public at
GET /api/v1/openapi.json; the OpenAPI explorer renders the same document, built from the
web app’s source. Request bodies in that document are generated from the zod schemas the handlers
validate with (apps/web/app/lib/api-schemas.ts), so they can’t drift. 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 APP_PASSWORD as a break-glass operator). Each route needs
a role: reads need viewer, approval decisions approver, and everything else that changes state
(starting, cancelling, events, webhook subscriptions) operator; a token or person without it gets
403. See Team and roles. An open deployment (no APP_PASSWORD, no
people) needs no token. Two routes are exceptions: GET /api/v1/openapi.json is public, and POST /api/v1/webhooks/events is
authenticated by an HMAC signature instead (see signed events).
Errors are JSON: { "error": "…" }, plus issues (zod issues) on a 400 for an invalid body.
401 means a missing, wrong or revoked token, 403 a role that can’t use the route, 429 too many wrong bearer tokens from one address (more
than 20 non-ol_live_ bearers in 15 minutes, which only guesses at APP_PASSWORD produce; see
Retry-After), 404 an unknown route or order, and 500 an engine error.
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?type=&status=&q= |
{ orders } |
|
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/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 } or 409 |
GET /api/v1/flows lists every flow, most recently updated first, with id, name,
description, orderType, 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": { "type": "b2b_order", "customerId": "cus_northwind", "currency": "USD", "lines": [{ "sku": "WID-100", "qty": 50 }], "meta": {} }}flowIdnames a flow with a published version.versionpins one; it defaults to the latest.input.typeis one ofpurchase_request,b2b_order,reorder,subscription,storefront_order.input.currencydefaults toUSDand must have an exchange rate (EUR,GBPandCADare seeded); another code returns400. See Currency.input.linesare{ sku, name?, qty, unitPrice? }. 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 in R2 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 mock connectors; see Built-in flows).varsseeds the run’s variables.
Idempotency. With an Idempotency-Key header (at most 255 characters), 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 up to 200 rows from the D1 order index, newest first. Filter with
type, status (one of queued, running, waiting, completed, declined, cancelled, failed, rolled_back) and q, which matches the order id, title
or customer id. Each row has id, type, status, title, flowId, flowVersion, instanceId,
generation, customerId, currency, total, waitingOn, createdAt and updatedAt
(epoch milliseconds).
Read an order
Section titled “Read an order”GET /api/v1/orders/:id returns the state its OrderAgent holds, 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 |
chat |
The order’s ops chat |
instanceId, generation |
The Workflow instance driving the order, and its generation |
rollback |
{ outcome, error } once a rollback has run |
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. 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; by defaults to api. The decision takes effect at once;
the dashboard’s undo window does not apply to the API.
Cancel
Section titled “Cancel”POST /api/v1/orders/:id/cancel terminates the order’s Workflow, releases reserved stock, voids
payment authorizations, cancels shipments and sends order.cancelled. reason defaults to
Cancelled via API. Cancelling a finished order does nothing.
Signed events
Section titled “Signed events”External systems resume orders without the admin password by signing the raw request body with the
web Worker’s WEBHOOK_SECRET:
POST /api/v1/webhooks/eventsX-OrderLoop-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(WEBHOOK_SECRET, "<t>.<raw body>")>
{ "orderId": "ord_…", "event": "payment.received", "payload": { "amount": 3106.78 } }signWebhook(secret, rawBody) in @orderloop/core builds the header. Signatures more than five
minutes from now are rejected.
| Status | Meaning |
|---|---|
200 |
Delivered: { delivered: true, nodeId } |
409 |
No node is waiting for that event yet: { delivered: false }. Retry later |
400 |
Body is not { orderId, event, payload? } |
401 |
Missing, malformed, stale or wrong signature |
503 |
WEBHOOK_SECRET is not set |
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.refunded,order.completed,order.declined,order.cancelled,order.failed, or["*"](the default). It returns the subscription and itssecret, once.GETlists subscriptions without their secrets.GET /api/v1/webhook-subscriptions/:id/deliveriesreturns the latest 50 deliveries:id,event,orderId,payload,status(pending,deliveredorfailed),attempts,responseStatus,lastError,createdAtanddeliveredAt.