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

# Workflows de fulfillment

> Arma el camino de cocina de un pedido: presets, nodos, webhooks, callbacks, Release checkout y el evento order.changed.

Un **workflow** de fulfillment es el grafo que arranca cuando el cliente coloca un pedido. Cada nodo es un paso (Paid, Injecting, Prepared, …). Las aristas deciden qué sigue. Fire spark siempre avisa al dueño del pedido — no agregas una acción de notify.

> [!TIP]
>
> Crea primero la opción de fulfillment (Delivery, Pickup, Dine-in, …) y luego edita su workflow en el dashboard. Los tipos están en [Fulfillment](https://firespark.cloud/docs/es/concepts/fulfillment.md).

## Crear un fulfillment

1. En el dashboard, abre **Fulfillment** y crea una opción.
2. Elige un **tipo**. El tipo es la promesa al cliente, no el canal.
3. Elige un **preset** (o el grafo por defecto de ese tipo).
4. Asigna **canales** para que la opción aparezca en app, web o kiosco.

Puedes guardar sin grafo, pero el storefront necesita un workflow para colocar pedidos.

## Presets y el grafo

Los presets traen nodos start, state, join y terminal, más las aristas. Los presets prepaid marcan **INJECTED** como Release checkout para que el overlay espere a que la inyección al POS ya haya terminado.

Puedes agregar o quitar nodos después. Exactamente un nodo **start** es obligatorio. Los terminales son desenlaces (`FULFILLED`, `VOIDED`, `REVERSED`, `REFUNDED`).

## Nodos, aristas, webhooks, callbacks

Abre un nodo para configurar:

| Campo | Rol |
| - | - |
| **Status** | Clave de motor que se escribe en el pedido (`PAID`, `INJECTING` o una clave custom). |
| **Label** | Nombre para el cliente. Independiente del status. |
| **Notify webhook** | Dispara solo cuando el trabajo del nodo está cerrado: persistido, liquidado y con los callbacks terminados. Si el run espera, la fila está en `WAITING` antes del webhook. Si el callback falla, el webhook de ese nodo no se envía. |
| **Invoke callback** | Se llama al entrar. Si se agotan los reintentos, el webhook de ese nodo no se envía y el pedido va a Cancelled o Reversed. Solo el webhook del terminal se envía, después de persistirlo y finalizar el run. |
| **Release checkout** | Opcional. Ver abajo. |

Las aristas pueden exigir una condición (prepago vs efectivo). El avance manual desde el POS o el dashboard mueve el pedido cuando el grafo pausa (`WAITING`).

## `order.changed`

Cada nodo se persiste y sus callbacks corren antes de cualquier aviso. Lo que sigue depende del grafo:

* El run espera un avance manual: la fila queda en `WAITING` y el realtime `released` sale; después salen `order.changed` y los webhooks del comercio.
* El siguiente nodo es automático: salen `order.changed` y los webhooks, y después se entra a ese nodo.
* El nodo es terminal: el run se finaliza y después salen `order.changed` y los webhooks.

El `order.changed` del nodo recién entrado se emite junto con la entrega del webhook, después de los callbacks. No se emite cuando el pedido apenas entra al nodo. Si el callback falla, no se envía el webhook de ese nodo. El webhook del terminal (`VOIDED` o `REVERSED`) se envía solo después de persistir ese terminal y finalizar el run.

Un avance que llega mientras un webhook todavía se está entregando queda en cola y corre cuando esa entrega termina. Así el webhook `PAID` termina antes de que se procese `INJECTING`.

`PATCH /orders/{id}/status` reclama la fila mientras está en `WAITING`, encola el avance y responde sin esperar al siguiente nodo. El pedido de la respuesta puede seguir siendo el snapshot de antes de este avance. Un `409` significa que la fila no estaba en `WAITING`.

Solo el dueño del pedido recibe el evento realtime (topic `sf:{merchant}:{env}:c:{customer_id}`).

Suscríbete con `@fire-spark/realtime`:

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

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

En cada `SUBSCRIBED` (conexión inicial y reconexión) el SDK llama [Obtener fulfillment del pedido](https://firespark.cloud/docs/es/storefront-api/orders/\[id]/fulfillment/get) y entrega ese snapshot por el mismo `on.changed`. Un cliente que llega tarde no necesita el evento perdido: alcanza el snapshot.

El token se documenta en [Emitir token realtime](https://firespark.cloud/docs/es/storefront-api/realtime/token/post.md).

### Payload

El evento y el DTO de fulfillment comparten `checkout_state` y `error`. El evento también trae `callbacks[]` — el `callback_run` ya terminado de cada nodo que tenía `invoke_callback` (`response`, `errors[]`, tiempos). No hay un evento por reintento ni al entrar al nodo. Un éxito en `INJECTING` suele traer `{ order_authorization_id, injection_order_id }` en `response`.

Si el payload serializado supera 64 KB, `response` / `body` pasan a `{ truncated: true }`. Lee el snapshot para los objetos completos.

`error` es un resumen para el overlay. El detalle queda en `callbacks[].errors`.

El evento **no** incluye URLs de webhook ni secretos.

## Release checkout

Como máximo un nodo puede tener **Release checkout**. Cero es válido: el checkout termina en cuanto el pedido se acepta (el flujo de hoy, sin socket).

El overlay espera hasta que `checkout_state` deje `pending`. El servidor calcula ese valor:

* Sin nodo release → `released`
* Run `FAILED`, o pedido `VOIDED` / `REVERSED` / `REFUNDED` → `failed`
* Run `WAITING` / `SUCCESS`, o el nodo actual es terminal → `released`
* El nodo release se visitó y su callback (si tiene) terminó sin error → `released`
* El resto → `pending`

En otras palabras: el overlay se libera cuando termina el paso con flag **o** el run pausa para la tienda **o** llega a un terminal — lo primero que ocurra. Una rama COD que pausa en `WAITING` sin flag igual se libera. Los presets prepaid marcan `INJECTED` para esperar a que la inyección ya haya terminado.

Guardar sin flag muestra un aviso: el checkout termina en cuanto el pedido se acepta. Puedes guardar igual.

Si activas el flag en un segundo nodo, el dashboard pide confirmar el traslado. Confirmar limpia el nodo anterior.

## Runtime

**Overlay de checkout**

* `checkout_release === false` en [Listar fulfillment](https://firespark.cloud/docs/es/storefront-api/fulfillment/get.md) → place por HTTP y success, como hoy. Sin socket.
* `true` → `listenOrder` arranca en `onMutate`; el overlay sigue `pending` tras el HTTP.
* Evento o snapshot `released` → success y detalle del pedido.
* `failed` → mensaje de error y detalle (el carrito ya se reemplazó; quedarse en checkout invita a pagar dos veces).
* Error HTTP (pedido no colocado) → failed y vuelta al checkout.
* 60 s aún `pending` → “Sigue confirmándose” y detalle.

**Detalle del pedido**

La pantalla de perfil escucha solo si `run_status` es `PROCESSING` o `WAITING`. Cada evento invalida la query. El socket se cierra al desmontar o cuando el refetch trae `SUCCESS` / `FAILED`.

**POS / dashboard**

Un terminal manual (cancelar, reembolsar, reverse) y la pausa para avanzar emiten el mismo evento. `warnNoEdge` no emite por sí solo: le sigue la pausa, que sí.

## Relacionado

> [card] [Fulfillment](https://firespark.cloud/docs/es/concepts/fulfillment.md)
>
> Tipos y la promesa al cliente

> [card] [Realizar pedidos](https://firespark.cloud/docs/es/storefront-api/guides/placing-orders.md)
>
> Del carrito al ticket

> [card] [Token realtime](https://firespark.cloud/docs/es/storefront-api/realtime/token/post.md)
>
> Topics y eventos

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


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