Skip to main content
This guide takes one order — ORD-10042 for a brand called acme-outdoors — through the Temporal Engine’s order-fulfillment workflow: reserve inventory → NSR gate → (human review) → dispatch. You create the brand and its API key, connect a connector, check readiness, stream the workflow’s history as server-sent events, and signal the review decision. The last section deliberately breaks things so you can see what durability buys you. Base URL https://api.workstream.stateset.com; auth x-api-key: <key> on every request; the workflow id is always order-fulfillment-{brand_uuid}-{order_id}.
Steps 1–2 need a tenant-scoped or global key (creating a brand checks tenant access, and a minted key is never broader than its minter). From step 3 on, the brand key from step 2 is enough. The overview explains the two scopes.
1

Create the brand

A brand is the unit of configuration — connectors, policies and workflow bindings hang off it. tenant_id, slug and display_name are required; status defaults to draft and routing_mode to legacy. Send an Idempotency-Key header so a retried create returns the same brand (idempotent_replay: true) instead of a slug conflict.
Keep brand.id — it is $BRAND_ID below. The reference for POST /v1/brands lists the optional config fields (region, default_locale, policy_set_key, quotas, …); Control plane covers draft → active.
2

Mint a brand-scoped API key

The body is optional — label defaults to "<slug> key".
token is returned once — only its hash is stored, and GET …/api-keys never returns it. Export it as $STATESET_API_KEY now. A lost key is revoked and re-minted.
3

Connect a connector and check readiness

/connect stores the credentials encrypted, re-points the brand’s connectors, and with activate: true attempts activation in the same call. It accepts gorgias, shopify and recharge blocks; the same body sent to /onboarding/{brand_id}/test first is a read-only probe that stores nothing and returns { "<type>": { "healthy", "error", "latency_ms" } }.
If activation is blocked, activated is false and readiness is { "blocked": "<error>" }; the credentials are still stored. The go-live gate live-probes every connector and is ready only when all are healthy and there is at least one:
4

Start the order-fulfillment workflow

fulfillment_tool is the sync-server tool that dispatches to your 3PL (it varies per connector); reservation_params and fulfillment_params pass through to create_inventory_reservation and to fulfillment_tool. review_cap_cents overrides the default autonomy cap of **50,000 cents (500)totalsatoroverthecapalwaysstopatthereviewgate,asthis500)** — totals at or over the cap always stop at the review gate, as this 689 order will.
The workflow id is deterministic per (brand, order_id). POST the same payload again while the run is open and you get the same workflow_id and the existing run_id back — a duplicate checkout webhook cannot start a second fulfillment. POST a different payload under the same order id and the engine answers 409 IDEMPOTENCY_CONFLICT (workflow_id already exists with a different start payload): either the order changed after checkout (cancel and restart) or two producers disagree about it.
5

Watch the status and the review queue

The status route runs the workflow’s current_status query and returns a JSON string "<status>:<phase>". Phases are init, reserve_inventory, nsr_gate, review_gate, dispatch; status stays running until a terminal one lands.
Poll the review queue rather than remembered workflow ids — it lists everything parked at running:review_gate for the brand.
6

Stream the event history

GET /v1/workflows/{workflow_id}/events tails Temporal history as server-sent events. event: is the kind, id: the history event id, data: a JSON WorkflowEvent (event_id, kind, ts, plus activity, attempt, signal, preview or error when relevant). A keepalive arrives every 15 seconds and the stream closes after a terminal kind.
Event 1 was workflow_started; after 15 comes silence: the NSR verdict was execute, but 68900 >= 50000 promoted it to an escalation and the workflow is sleeping on a durable timer. If the connection drops, reconnect with Last-Event-ID: 15 and the engine replays from 16.
The streaming route authorises by workflow-id prefix, and order-fulfillment- is not in its brand-scoped allowlist (rav2-, cp-response-automation-v2-, connector-, snooze-, sandbox-agent-, active-horizon-agent-), so a brand key gets 400 workflow_id prefix does not grant streaming access to this principal. Stream with a global key; status, review and cancel all work with the brand key.
7

Approve at the review gate

The body is a ReviewDecision: approved is required; reason, feedback and edited_text are optional. Signal routes answer 202 Accepted with an empty body — the decision is delivered to the workflow, not applied synchronously.
On the stream the signal lands as signal_received with "signal":"set_review_decision", dispatch runs, and the workflow completes with the OrderFulfillmentResult in preview:
GET …/status now returns "fulfilled:dispatch" and the order has left the review queue; decision_id is the NSR decision that authorised the dispatch (policy engine).

Failure, cancellation and replay

A connector blips mid-run. Every activity runs under the engine’s API retry policy: up to 8 attempts, exponential backoff from 1 s capped at 30 s (roughly a 90-second window). On the stream that is activity_failed with the upstream error, then activity_started with attempt: 2, 3, … — the workflow’s own state is untouched between attempts. Non-retryable errors (4xx from the tool) fail fast and the run ends with workflow_failed and error set. Someone cancels while it waits for review.
The gate wakes on the cancel signal, status becomes "cancelled:review_gate", and the result carries the compensation:
Every terminal status that never dispatched — blocked (NSR said no, or the reviewer denied), escalated_expired (nobody answered within the 7-day review timeout), cancelled — goes through the same chokepoint and releases the reservation; if the release itself fails the run keeps its original status and reports { "released": false, "error": … } in detail.compensation. The whole run has a 14-day execution timeout. The worker restarts. Temporal rebuilds the workflow’s state by replaying its history: completed activities are not re-executed, and a run parked at the review gate is still parked afterwards. The activities that could run twice carry deterministic idempotency keys ({brand}-{order}-reserve, -dispatch, -release) so the sync server deduplicates them — the same contract the Order Operations guide asks of your own integrations.

What you built

Troubleshooting

x-api-key is missing or revoked — keys are hashed at rest, so mint a new one. POST /v1/brands needs tenant access for the body’s tenant_id, and a brand key can only mint keys for its own brand: use the tenant or global credential for steps 1–2.
Expected with a brand key on an order-fulfillment- id — see the warning in step 6. Use a global key for the stream; status, review and cancel work with the brand key.
Read connectors[].error — each entry is a live probe, and a brand with zero connectors is never ready. Re-probe with /onboarding/{brand_id}/test until healthy: true, then /connect.
Stuck at the gate is the design: it waits up to 7 days for a review signal and never times out into dispatching, so alert on the length of GET …/reviews/pending rather than on failures. Smaller orders escalate only when the NSR gate says so or is unreachable (it fails closed). A 404 means no run with that id exists — check the brand uuid is the compact 32-hex form and order_id matches; /start again is safe because the id is deterministic.

Next steps

Workflow anatomy

The primary response-automation-v2 workflow — phases, signals, and the review gate pattern this guide reused.

Return workflow

The same start / status / review / cancel shape for returns, plus receipt and inspection signals.

Control plane

Multi-tenancy, the outbox and DLQ, and how brand config is versioned and activated.

Order to cash

Where fulfillment sits in the wider commerce flow from order to settlement.
Last modified on August 31, 2026