Inbound orders
Start orders from a storefront, marketplace, POS or other channel with a signed POST, route them to a flow, and send them safely more than once.
Systems that take orders elsewhere (a storefront checkout, a marketplace, a till) start them with
POST /api/v1/webhooks/orders. The request is signed with an integration’s key instead of
carrying a token. The order starts on a published
flow and shows up in the dashboard under its channel, with the sender’s id as its external id.
Shopify and EDI X12 850 senders can post their native payloads instead; see
Channel adapters, which share this page’s routing and idempotency.
Payload
Section titled “Payload”The body is the order input from Start an order plus the fields that identify the order and pick the flow:
{ "model": "b2c", "channel": "web", "externalId": "SHOP-10442", "source": "shopify", "customerId": "cus_ada", "currency": "USD", "lines": [{ "sku": "MON-27", "qty": 1 }], "shipTo": { "line1": "1 Main St", "city": "Austin", "region": "TX", "postalCode": "78701", "country": "US" }}| Field | Notes |
|---|---|
model |
Required: one of b2b, b2c, d2c, marketplace, subscription, procurement, replenishment. See Commerce models |
channel |
Required (no default here): one of web, mobile, pos, marketplace, email, edi, api, manual, schedule |
externalId |
Required, 1 to 200 characters: the order’s id in the sending system. With channel it identifies the order |
lines |
{ sku, name?, qty, unitPrice?, sellerId? }. Required unless documentText is sent for the Extract order node |
flowId |
Optional. Runs this flow instead of the routed one |
source |
Optional, up to 60 characters. The sending system, for display (shopify); kept in meta.source |
shipTo |
Optional. An address string (up to 500 characters) or { name?, line1, line2?, city, region?, postalCode, country, latitude?, longitude? }. Kept in meta.shipTo, where the no_po_box policy rule reads it. latitude (-90 to 90) and longitude (-180 to 180) let route fulfillment ship from the nearest store |
vars |
Optional. Seeds the run’s variables |
The rest (title, customerId, currency, contact, fulfillment, documentText,
documentKey, meta) are the fields of input in the REST API. currency defaults to USD and
must have an exchange rate, or the request returns 400. The server sets meta.via to webhook.
The Webhook trigger summarises the order as
<channel label> · <externalId>, plus (<source>) when the order carries a source that differs from the channel, and outputs { channel, externalId, source } (source falls back to the node’s configured one).
Signing
Section titled “Signing”Sign the raw body exactly as for signed events:
X-OrderLoop-Signature: t=<unix seconds>,k=<integration id>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>,
with a timestamp within five minutes. k is the id of the integration whose secret signs the request. Sign the bytes you send; re-serialising the JSON afterwards
breaks the signature.
BODY='{"model":"b2c","channel":"web","externalId":"SHOP-10442","lines":[{"sku":"MON-27","qty":1}]}'T=$(date +%s)SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$ORDERLOOP_SIGNING_SECRET" -hex | sed 's/^.* //')curl -X POST https://orderloop.app/api/v1/webhooks/orders \ -H "X-OrderLoop-Signature: t=$T,k=$ORDERLOOP_INTEGRATION_ID,v1=$SIG" -H 'content-type: application/json' \ -d "$BODY"From Node:
import { createHmac } from 'node:crypto';
const body = JSON.stringify({ model: 'b2c', channel: 'web', externalId: 'SHOP-10442', lines: [{ sku: 'MON-27', qty: 1 }],});// integrationId and secret come from creating the integration.const t = Math.floor(Date.now() / 1000);const v1 = createHmac('sha256', secret).update(`${t}.${body}`).digest('hex');const res = await fetch('https://orderloop.app/api/v1/webhooks/orders', { method: 'POST', headers: { 'content-type': 'application/json', 'x-orderloop-signature': `t=${t},k=${integrationId},v1=${v1}` }, body,});What an integration may start
Section titled “What an integration may start”An operator creates one integration per sending system and scopes it
(Integrations). To start orders it needs startOrders: true.
If it is bound to a channel, every order it posts must be on that channel. If the body names a
flowId, that flow must be in the integration’s flows (or flows is ["*"]); a body without
flowId is routed as below whatever flows says. Anything outside the scope is a 403.
Routing
Section titled “Routing”The flow that runs is, in order:
- The body’s
flowId. - The
<model>:<channel>entry in the workspace’s order routes, else in the built-in routes. - The
<model>:*entry, likewise. - The model’s default built-in flow:
tpl-b2b-orderforb2b,tpl-storefront-orderforb2c,tpl-omnichannel-orderford2c,tpl-marketplace-orderformarketplace,tpl-subscription,tpl-purchase-requestandtpl-reorderfor the others.
The built-in routes send till sales to the point-of-sale flow: b2c:pos and d2c:pos run
tpl-pos-sale (see Fulfillment).
The workspace’s order routes are set by your OrderLoop administrator: a map from model:channel
or model:* to a flow id, which overrides the built-in routes key by key. Keys are lowercase
letters, digits and underscores (* is allowed after the colon). For example:
{ "b2c:pos": "flow-pos-order", "marketplace:*": "flow-marketplace-order" }Here a b2c order from pos runs flow-pos-order instead of tpl-pos-sale, any marketplace order runs
flow-marketplace-order, and everything else runs its built-in route or its model’s default. A new order’s flow must have
a published version, or the request returns 409 and starts nothing. A retry of an order already
started finds it first, so it still returns 200 after its flow is unpublished.
Idempotency
Section titled “Idempotency”channel and externalId are the idempotency key (order:<channel>:<externalId>), so a sender
can retry freely. A repeat that matches the first request returns 200 with the existing order.
The match is a fingerprint of the body you sent, not of the flow it was routed to, so a retry
still finds its order after the order routes change. The same
pair with different contents returns 422 and starts nothing. To change an accepted order,
deliver events to it.
Responses
Section titled “Responses”| Status | Meaning |
|---|---|
201 |
Started: { orderId, flowId } |
200 |
A replay of an order already started: { orderId, replayed: true } |
400 |
Body is not JSON, is not a valid order (with issues), or uses a currency with no exchange rate |
401 |
Missing, malformed, stale or wrong signature; no k=; an unknown or revoked integration |
403 |
The integration may not start orders, is bound to another channel, or may not choose that flowId |
409 |
A new order’s flow has no published version |
422 |
The same channel and externalId already arrived with different contents |
500 |
Engine error |
Errors are { "error": "…" }. Treat 201 and 200 as accepted; retry on 5xx with the same body.