> ## Documentation Index
> Fetch the complete documentation index at: https://firespark.cloud/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Fulfillment workflows

> Build the kitchen path for an order: presets, nodes, webhooks, callbacks, Release checkout, and the order.changed event.

A fulfillment **workflow** is the graph that starts when a customer places an order. Each node is a step (Paid, Injecting, Prepared, …). Edges decide what happens next. Fire spark always tells the order owner what changed — you do not add a notify action.

> [!TIP]
>
> Create the fulfillment option first (Delivery, Pickup, Dine-in, …), then edit its workflow in the dashboard. Types are explained in [Fulfillment](https://firespark.cloud/docs/concepts/fulfillment.md).

## Create a fulfillment

1. In the dashboard, open **Fulfillment** and create an option.
2. Pick a **type**. The type is the customer promise, not the channel.
3. Choose a **preset** (or start from the default graph for that type).
4. Assign **channels** so the option appears on app, web, or kiosk.

Saving without a graph is possible, but the storefront needs a workflow to place orders.

## 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:

| Field | Role |
| - | - |
| **Status** | Engine key written on the order (`PAID`, `INJECTING`, a custom key). |
| **Label** | Customer-facing name. Independent of status. |
| **Notify webhook** | Fire only after this node's work is closed: persisted, settled, and callbacks finished. A waiting node is `WAITING` before the webhook. A failed callback does not send this node's webhook. |
| **Invoke callback** | Called when the order enters this step. If retries run out, this node's webhook is skipped and the order goes to Cancelled or Reversed. Only that terminal webhook is sent, after the terminal is persisted and the run is finalized. |
| **Release checkout** | Optional. See below. |

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 `WAITING` and realtime `released` goes out, then `order.changed` and the merchant webhooks.
* The next node is automatic: `order.changed` and the webhooks go out, then that next node is entered.
* The node is terminal: the run is finalized, then `order.changed` and 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`:

```ts theme={null}
import { realtime } from "@fire-spark/realtime";

const stop = realtime.listenOrder({
  orderId,
  on: {
    changed: payload => {
      // payload.checkout_state is pending | released | failed
    }
  }
});
```

On every `SUBSCRIBED` (first connect and reconnect) the SDK calls [Get order fulfillment](https://firespark.cloud/docs/storefront-api/orders/\[id]/fulfillment/get) 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](https://firespark.cloud/docs/storefront-api/realtime/token/post.md).

### Payload

The event and the fulfillment DTO share `checkout_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 until `checkout_state` leaves `pending`. The server computes that value:

* No release node → `released`
* Run `FAILED`, or order `VOIDED` / `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`

In other words: the overlay releases when the flagged step finishes **or** the run pauses for the store **or** the run reaches a terminal — whichever comes first. A COD branch that pauses at `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 overlay**

* `checkout_release === false` on [List fulfillment](https://firespark.cloud/docs/storefront-api/fulfillment/get.md) → place over HTTP, then success, as today. No socket.
* `true` → `listenOrder` starts in `onMutate`, overlay stays `pending` after 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.

**Order detail**

The profile screen listens only while `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

> [card] [Fulfillment](https://firespark.cloud/docs/concepts/fulfillment.md)
>
> Types and the customer promise

> [card] [Placing orders](https://firespark.cloud/docs/storefront-api/guides/placing-orders.md)
>
> Cart to kitchen ticket

> [card] [Realtime token](https://firespark.cloud/docs/storefront-api/realtime/token/post.md)
>
> Topics and events

> [card] [Get order fulfillment](https://firespark.cloud/docs/storefront-api/orders/\[id]/fulfillment/get)
>
> Snapshot with checkout\_state


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.