Skip to content

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.

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.

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.

POST /api/v1/orders
Idempotency-Key: po-NW-7781
{
"flowId": "tpl-b2b-order",
"input": {
"type": "b2b_order",
"customerId": "cus_northwind",
"currency": "USD",
"lines": [{ "sku": "WID-100", "qty": 50 }],
"meta": {}
}
}
  • flowId names a flow with a published version. version pins one; it defaults to the latest.
  • input.type is one of purchase_request, b2b_order, reorder, subscription, storefront_order.
  • input.currency defaults to USD and must have an exchange rate (EUR, GBP and CAD are seeded); another code returns 400. See Currency.
  • input.lines are { 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.meta is free-form and is the run context’s input. Some keys mean something to nodes: shipTo (the no_po_box policy rule) and simulate (forces a failure path in the mock connectors; see Built-in flows).
  • vars seeds 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.

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

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.

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.

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.

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.

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/events
X-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

See Outbound webhooks for the events, payloads and signatures.

  • POST /api/v1/webhook-subscriptions takes url (https, or plain http for localhost, 127.0.0.1 and [::1]) and events: any of order.paid, order.invoiced, order.shipped, order.refunded, order.completed, order.declined, order.cancelled, order.failed, or ["*"] (the default). It returns the subscription and its secret, once.
  • GET lists subscriptions without their secrets.
  • GET /api/v1/webhook-subscriptions/:id/deliveries returns the latest 50 deliveries: id, event, orderId, payload, status (pending, delivered or failed), attempts, responseStatus, lastError, createdAt and deliveredAt.