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.