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

# Order status

> States and delivery updates

Order status tracks execution from deposit to delivery or recovery. Poll for the order snapshot, receive webhooks, or stream current state over SSE.

## States

In flight:

* `processing`: created or awaiting deposit.
* `confirming`: awaiting source confirmations.
* `awaiting_approval`: pending offer or review.
* `swapping`: executing a swap.
* `bridging`: crossing chains.
* `delivering`: sending to the recipient.
* `refunding`: refund in flight.

### Outcomes

* `completed`: delivered.
* `refunded`: funds returned.
* `failed`: execution or refund failure.
* `expired`: expired without deposit.
* `unfulfilled`: unconfirmed or replaced deposit; can resume.

`awaiting_approval` can mean a pending [ZeroConf offer](/orchestra/zeroconf) or an operator hold. Offer accept/decline applies only to a pending `zeroconfOffer`, not to the state alone. Review outcomes can carry `reviewStatus`; internal `paused` states appear as `processing`.

`unfulfilled` can resume after a late deposit. Keep listening and check `supersededByOperationId`. For failures, read `order.errorCode` when polling or `data.error.code` in webhooks.

A Bitcoin deposit replaced by RBF leaves the original order `unfulfilled` with `supersededByOperationId` pointing at the successor. The successor carries `recoveredFromOperationId`. Each time that link advances, `order.superseded` fires on the original.

## Refunds

Execution outside the quote's bounds can trigger a refund. Supply `refundAddress` on the quote. Without a valid refund target, or while an operator hold applies, recovery may need manual review. See [Errors](/api/errors).

## Stages

The status response includes `stages[]`: execution milestones and their `completedAt` timestamps. Use them for progress detail, not as a replacement for order status.

## Reading status

Use webhooks in production. Use SSE for user-facing progress. Poll as the fallback.

<Tabs>
  <Tab title="Polling">
    A quote becomes an order when the deposit lands, so the first thing to poll is the quote. `GET /v1/orchestration/order` needs a server key and returns `{ quote, order, stages }` with `order` null until the deposit is detected:

    ```bash theme={null}
    curl "https://orchestration.flashnet.xyz/v1/orchestration/order?quoteId=QUOTE_ID" \
      -H "Authorization: Bearer SERVER_KEY"
    ```

    <Info>
      Replace `SERVER_KEY` with your `fn_...` key. Use a new `X-Idempotency-Key` for each operation and reuse it when retrying that same request. Amounts are integer strings in the asset's smallest unit: `"100000"` is 0.001 BTC; `"50000000"` is 50 USDC on Base. Read each asset's `decimals` from `/routes`.
    </Info>

    Status requires authentication and returns `{ order, stages }`; read `order.status` for the current state. Server keys use exactly one of `id`, `quoteId`, or `txHash`; multiple transaction-hash matches return `400 ambiguous_query`. Before an order exists, status returns `404 not_found`:

    ```bash theme={null}
    curl "https://orchestration.flashnet.xyz/v1/orchestration/status?id=ORDER_ID" \
      -H "Authorization: Bearer SERVER_KEY"
    ```

    Client keys need `orders:read` and a matching read token: use `quoteId` with the quote token, or `id` with a submit/onramp token. Send `X-Read-Token` or `?readToken=`. Client keys cannot look up by `txHash`.

    Server keys can list recipient orders through `/v1/orchestration/history?address=RECIPIENT_ADDRESS`, with `status`, `limit` (default 50, max 200), and `offset`. Poll every 3 seconds; see [Rate limits](/api/rate-limits).
  </Tab>

  <Tab title="Webhooks">
    Register an endpoint with a server key. The `secret` is returned once:

    ```bash theme={null}
    curl -X POST "https://orchestration.flashnet.xyz/v1/webhooks" \
      -H "Authorization: Bearer SERVER_KEY" \
      -H "Content-Type: application/json" \
      -H "X-Idempotency-Key: $(uuidgen)" \
      -d '{ "url": "https://example.com/flashnet/webhook" }'
    ```

    Each body is `{ event, timestamp, data }`, with an order snapshot in `data`. Not every transition emits an event. [Webhook events](/api/webhook-events) lists the catalog and the separate recovery subscription.

    `X-Flashnet-Timestamp` is milliseconds since epoch. `X-Flashnet-Signature` is the hex HMAC-SHA256 of `timestamp + "." + rawBody`. Verify raw bytes before parsing JSON. This example uses a receiver-chosen five-minute clock tolerance:

    ```javascript theme={null}
    import { createHmac, timingSafeEqual } from "node:crypto";

    export function verifyFlashnetWebhook(rawBody, headers, secret, now = Date.now()) {
      const timestamp = headers["x-flashnet-timestamp"];
      const signature = headers["x-flashnet-signature"];
      if (typeof timestamp !== "string" || !/^\d+$/.test(timestamp)) return false;
      if (typeof signature !== "string" || !/^[0-9a-f]{64}$/i.test(signature)) return false;
      const sentAt = Number(timestamp);
      if (!Number.isSafeInteger(sentAt) || Math.abs(now - sentAt) > 300_000) return false;

      const expected = createHmac("sha256", secret)
        .update(`${timestamp}.`)
        .update(rawBody)
        .digest("hex");

      const a = Buffer.from(expected, "hex");
      const b = Buffer.from(signature, "hex");
      return a.length === b.length && timingSafeEqual(a, b);
    }
    ```

    Delivery is retried until a 2xx response, after 10s, 30s, 2m, 10m, 30m, 2h, 6h, and 24h, then marked failed. Retries have fresh header timestamps and signatures; the JSON body stays unchanged. Events can duplicate or arrive out of order: durably dedupe on `(data.id, event, data.updatedAt)`, not `timestamp`.
  </Tab>

  <Tab title="SSE">
    Open a stream per order through an authorized backend proxy. Keep server keys on the backend:

    ```javascript theme={null}
    const stream = new EventSource(`/api/orders/${orderId}/events`);

    stream.addEventListener("status", (e) => {
      const { status } = JSON.parse(e.data);
      if (["completed", "failed", "refunded"].includes(status)) stream.close();
    });
    ```

    Connections start with current state, followed by live updates and 15-second heartbeats. Reconnects do not replay missed transitions. Direct client-key streams require `orders:sse` and an order-bound token; quote tokens cannot authorize them. See [SSE](/api/sse) for authentication, frame formats, and close behavior.
  </Tab>
</Tabs>
