Create a fulfillment
- In the dashboard, open Fulfillment and create an option.
- Pick a type. The type is the customer promise, not the channel.
- Choose a preset (or start from the default graph for that type).
- Assign channels so the option appears on app, web, or kiosk.
Presets and the graph
Presets give you start, state, join, and terminal nodes plus the edges between them. Prepaid presets mark INJECTED as Release checkout so the overlay waits until POS injection has finished. You can add or remove nodes after applying a preset. Exactly one start node is required. Terminals are outcomes (FULFILLED, VOIDED, REVERSED, REFUNDED).
Nodes, edges, webhooks, callbacks
Open a node to set:
Edges can require a condition (prepaid vs cash). Manual advance from the POS or dashboard moves the order when the graph pauses (
WAITING).
order.changed
Each node is persisted and its callbacks run before anything is notified. The next step depends on the graph:
- The run waits for a manual advance: the row is saved as
WAITINGand realtimereleasedgoes out, thenorder.changedand the merchant webhooks. - The next node is automatic:
order.changedand the webhooks go out, then that next node is entered. - The node is terminal: the run is finalized, then
order.changedand the webhooks go out.
order.changed for the node that was just entered is emitted with the webhook delivery, after callbacks. It is not emitted when the order first steps into the node. A failed callback does not send that node’s webhook. The terminal webhook (VOIDED or REVERSED) is sent only after that terminal is persisted and the run is finalized.
An advance that arrives while a webhook is still being delivered stays queued and runs after that delivery returns, so a PAID webhook finishes before INJECTING is processed.
PATCH /orders/{id}/status claims the row while it is WAITING, queues the advance, and returns without waiting for the next node. The order in the response can still be the snapshot from before this advance. A 409 means the row was not WAITING.
The owner of the order is the only storefront client that receives the realtime event (topic sf:{merchant}:{env}:c:{customer_id}).
Subscribe with @fire-spark/realtime:
SUBSCRIBED (first connect and reconnect) the SDK calls Get order fulfillment and delivers that snapshot through the same on.changed handler. A late client does not need the missed event — the snapshot is enough.
Token minting is documented at Issue realtime token.
Payload
The event and the fulfillment DTO sharecheckout_state and error. The event also includes callbacks[] — the finished callback_run for each node that had invoke_callback (response, errors[], timing). You do not get a separate event per retry or when the node is first entered. An INJECTING success typically has { order_authorization_id, injection_order_id } in response.
If the serialized payload exceeds 64 KB, response / body become { truncated: true }. Read the snapshot for the full objects.
error is a short summary for the checkout overlay. Full failures stay in callbacks[].errors.
The event does not include webhook URLs or secrets.
Release checkout
At most one node can have Release checkout. Zero is valid: checkout then finishes as soon as the order is accepted (today’s flow, no socket). The overlay waits untilcheckout_state leaves pending. The server computes that value:
- No release node →
released - Run
FAILED, or orderVOIDED/REVERSED/REFUNDED→failed - Run
WAITING/SUCCESS, or the current node is a terminal →released - The release node was entered and its callback (if any) finished without error →
released - Otherwise →
pending
WAITING without a flag still releases. Prepaid presets flag INJECTED so the overlay waits until injection has completed.
Saving without a flag shows a warning: checkout finishes as soon as the order is accepted. You can save anyway.
If you toggle the flag on a second node, the dashboard asks you to move it. Confirming clears the previous node.
Runtime
Checkout overlaycheckout_release === falseon List fulfillment → place over HTTP, then success, as today. No socket.true→listenOrderstarts inonMutate, overlay stayspendingafter HTTP success.- Event or snapshot
released→ success, then order detail. failed→ error message, then order detail (the cart was already replaced; staying on checkout invites a second payment).- HTTP error (order not placed) → failed, back to checkout.
- 60 s still
pending→ “Still confirming”, then order detail.
run_status is PROCESSING or WAITING. Each event invalidates the order query. The socket closes on unmount or when the refetch returns SUCCESS / FAILED.
POS / dashboard
Manual terminal (cancel, refund, reverse) and pause-for-advance emit the same event. warnNoEdge does not emit by itself — pause follows and does.
Related
Fulfillment
Types and the customer promise
Placing orders
Cart to kitchen ticket
Realtime token
Topics and events
Get order fulfillment
Snapshot with checkout_state