# Withdraw Funds via a Liquidity Provider

Withdrawals via a liquidity provider move funds out of your managed accounts to your external bank account.

:::note
Funds can only be withdrawn from a liquidity provider to a bank account. If the destination is a wallet, the resource should be a [rebalance](./rebalance-funds) (when the wallet is Tesser-provisioned) or a [payment](./send-a-stablecoin-payout/create-a-payout-from-a-wallet) (when the wallet belongs to a third party).
:::

Before creating a withdrawal, review the [Funds Movement Lifecycle and Data Model](/overviews/funds-movement-lifecycle-and-data-model) overview to understand the shared data shape, lifecycle phases, and statuses that apply to withdrawals and Tesser's other funds-movement resources.

## Prerequisites

Liquidity providers require funds movement to and from their platforms to be "first-party" only. For a withdrawal, the destination bank account must be registered both with Tesser and with the liquidity provider executing the withdrawal. 

Where supported, Tesser will register the destination bank account at your enrolled liquidity provider on your behalf. This mirrors how deposits work, except funds flow toward the fiat bank account rather than from it. For more information on account creation, see [Create an account](/how-tos/create-an-account). The `desired.from.account_id` must be one of your managed accounts at a liquidity provider (for example, an OpenFX USD ledger or a Circle USDC ledger) or a self-custodial wallet.

To send funds to a third party, see [Send a Stablecoin Payout](/how-tos/send-a-stablecoin-payout/create-a-stablecoin-payout).

## Withdrawal Workflow

A withdrawal executes over one or more steps.

- A `transfer` step moves funds from one account to another — for example, an on-chain push from a wallet to a provider's ledger, or a fiat transfer from a provider's ledger to your external bank account. A `transfer` step's `from` and `to` may differ in currency: when a provider redeems a stablecoin into fiat as part of a single API call to the provider (e.g., Circle redeeming USDC into USD on the way out to your bank), the redemption is modeled as one cross-currency `transfer` step.
- A `swap` step exchanges currencies within the same account (the step's `estimated.from.account_id` and `estimated.to.account_id` are equal). Tesser uses a standalone `swap` step when the provider's ledger can hold both source and destination currencies (e.g., OpenFX redeeming USDT into USD on its own ledger). For providers whose redemption is bundled with the withdrawal itself (e.g., Circle), the currency change rides on a `transfer` step instead, and the withdrawal has no standalone `swap` step.

Each step has a status. See [Step Statuses](/overviews/funds-movement-lifecycle-and-data-model#step-statuses) for the full status taxonomy. Tesser plans the sequence of steps during the [Planning](/overviews/funds-movement-lifecycle-and-data-model#planning) phase, executes them during the [Execution](/overviews/funds-movement-lifecycle-and-data-model#execution) phase, and populates the top-level `actual.*` overlay when the withdrawal reaches its [Terminal State](/overviews/funds-movement-lifecycle-and-data-model#terminal-state-and-divergence).

## Exchange Rates for Withdrawals

When a withdrawal crosses currencies — whether as a standalone `swap` step (e.g., OpenFX redeeming USDT to USD on its own ledger) or as a cross-currency `transfer` step (e.g., Circle redeeming USDC to USD as part of the withdrawal) — the exchange rate depends on the liquidity provider executing the withdrawal. As with deposits, Tesser does not provide guaranteed exchange rates unless the provider does.

| Type of liquidity provider | Example | Behavior |
|---|---|---|
| Stablecoin issuer | Circle | Exchange rate between USD and USDC always 1:1 |
| On/Off ramp | OpenFX | Exchange rate between sell and buy currencies fluctuates; current rates can be guaranteed for a period of time |
| Exchange | Kraken | Exchange rate fluctuates; no guaranteed rate |

## Withdrawal Creation

Submit a request to [`POST /v1/treasury/withdrawals`](/api/treasury#create-withdrawal).

- If applicable, you should specify on which `tenant`'s behalf you are requesting the withdrawal.
- For the withdrawal, populate the following fields in the `desired` object:
    - `desired.from.account_id`: The identifier of one of your managed accounts at a liquidity provider, for example, an OpenFX or Circle ledger, or a self-custodial wallet.
    - `desired.from.amount`: Amount to withdraw, in `desired.from.currency`.
    - `desired.from.currency`: Currency held at `desired.from.account_id`.
    - `desired.from.network`: Only specified when `desired.from.account_id` is a self-custodial wallet (e.g., `BASE`).
    - `desired.to.account_id`: Identifier of your external destination — the bank account registered with Tesser ahead of time.
    - `desired.to.currency`: Currency to deliver to the destination (e.g., `USD` when withdrawing a stablecoin into a USD bank account).

*Note: The withdrawal's `desired.to.amount` remains `null` throughout the lifecycle; the indicative target amount is supplied in `estimated.to.amount` once the quote is obtained.*

**The examples in this guide show 3 scenarios:**

1. Scenario 1 — Withdrawing USDT from an OpenFX ledger to your USD bank account.
2. Scenario 2 — Withdrawing USDC from a Circle ledger to your USD bank account.
3. Scenario 3 — Withdrawing USDC from a self-custodial wallet via OpenFX to your USD bank account.

<Tabs>
  <TabItem label="Scenario 1">

Example request (USDT from an OpenFX ledger withdrawn to your external USD bank account):

```json
{
  "tenant_id": null,
  "desired": {
    "from": {
      "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
      "amount": "1000",
      "currency": "USDT"
    },
    "to": {
      "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
      "currency": "USD"
    }
  }
}
```

  </TabItem>
  <TabItem label="Scenario 2">

Example request (USDC from a Circle ledger withdrawn via Circle redemption to your external USD bank account):

```json
{
  "tenant_id": null,
  "desired": {
    "from": {
      "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
      "amount": "1000",
      "currency": "USDC"
    },
    "to": {
      "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
      "currency": "USD"
    }
  }
}
```

  </TabItem>
  <TabItem label="Scenario 3">

Example request (USDC on BASE from a self-custodial wallet, withdrawn via OpenFX to your external USD bank account):

```json
{
  "tenant_id": null,
  "desired": {
    "from": {
      "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
      "amount": "1000",
      "currency": "USDC",
      "network": "BASE"
    },
    "to": {
      "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
      "currency": "USD"
    }
  }
}
```

  </TabItem>
</Tabs>

In the API response, Tesser will create and return an `id` for the withdrawal. 

The top-level `balance_status` is initially `unreserved`. For ledger sources (Scenarios 1 and 2), Tesser performs the balance check asynchronously and publishes the outcome via a `withdrawal.balance_updated` webhook shortly after `withdrawal.quote_created`. For wallet sources (Scenario 3), the balance check runs synchronously inside your call to the step-signing API. See [Withdrawal Balance Check](#withdrawal-balance-check-withdrawalbalance_updated) below. 

The `estimated` and `actual` overlays are all-null at creation; `estimated` is populated when `withdrawal.quote_created` fires, and `actual` is populated when the withdrawal reaches its terminal state.

<Tabs>
  <TabItem label="Scenario 1">

Example response (USDT from an OpenFX ledger withdrawn to your external USD bank account):

```json
{
  "data": {
    "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
    "workspace_id": "b53f6690-3242-4942-9907-885779632832",
    "organization_reference_id": null,
    "direction": "outbound",
    "balance_status": "unreserved",
    "balance_reserved_at": null,
    "desired": {
      "from": {
        "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
        "amount": "1000",
        "currency": "USDT",
        "network": null
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": null,
        "currency": "USD",
        "network": null
      }
    },
    "estimated": {
      "from": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      },
      "to": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      }
    },
    "actual": {
      "from": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      },
      "to": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      }
    },
    "steps": [],
    "created_at": "2025-12-01T10:00:00.000Z",
    "updated_at": "2025-12-01T10:00:00.000Z",
    "expires_at": "2025-12-01T14:00:00.000Z"
  }
}
```

  </TabItem>
  <TabItem label="Scenario 2">

Example response (USDC from a Circle ledger withdrawn via Circle redemption to your external USD bank account):

```json
{
  "data": {
    "id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d",
    "workspace_id": "b53f6690-3242-4942-9907-885779632832",
    "organization_reference_id": null,
    "direction": "outbound",
    "balance_status": "unreserved",
    "balance_reserved_at": null,
    "desired": {
      "from": {
        "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
        "amount": "1000",
        "currency": "USDC",
        "network": null
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": null,
        "currency": "USD",
        "network": null
      }
    },
    "estimated": {
      "from": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      },
      "to": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      }
    },
    "actual": {
      "from": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      },
      "to": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      }
    },
    "steps": [],
    "created_at": "2025-12-01T10:00:00.000Z",
    "updated_at": "2025-12-01T10:00:00.000Z",
    "expires_at": "2025-12-01T14:00:00.000Z"
  }
}
```

  </TabItem>
  <TabItem label="Scenario 3">

Example response (USDC on BASE from a self-custodial wallet, withdrawn via OpenFX to your external USD bank account):

```json
{
  "data": {
    "id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
    "workspace_id": "b53f6690-3242-4942-9907-885779632832",
    "organization_reference_id": null,
    "direction": "outbound",
    "balance_status": "unreserved",
    "balance_reserved_at": null,
    "desired": {
      "from": {
        "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
        "amount": "1000",
        "currency": "USDC",
        "network": "BASE"
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": null,
        "currency": "USD",
        "network": null
      }
    },
    "estimated": {
      "from": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      },
      "to": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      }
    },
    "actual": {
      "from": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      },
      "to": {
        "account_id": null,
        "amount": null,
        "currency": null,
        "network": null
      }
    },
    "steps": [],
    "created_at": "2025-12-01T10:00:00.000Z",
    "updated_at": "2025-12-01T10:00:00.000Z",
    "expires_at": "2025-12-01T14:00:00.000Z"
  }
}
```

  </TabItem>
</Tabs>

## Withdrawal Quote Created (`withdrawal.quote_created`)

After your `POST /v1/treasury/withdrawals` request is accepted, Tesser plans the route the funds will take and obtains a reference exchange rate. Tesser then sends a `withdrawal.quote_created` webhook — the first webhook fired for the withdrawal. The payload carries the planned `steps[]` array (each step with `status: "created"`) together with the populated `estimated` overlay at both the withdrawal and step levels. This webhook always fires, even for same-currency withdrawals, to keep the lifecycle uniform across resource types.

For cross-currency withdrawals, the ratio of `estimated.from.amount` to `estimated.to.amount` is the indicative exchange rate at the liquidity provider. Circle redemptions are 1:1, so for Scenario 2 the indicative rate matches the source amount.

<Tabs>
  <TabItem label="Scenario 1">

Example `withdrawal.quote_created` webhook (USDT from an OpenFX ledger withdrawn to your external USD bank account; swap step + transfer step):

```json
{
  "id": "b5e9c3a7-2f4d-48e6-8b1c-7a3f9d2e5b4c",
  "type": "withdrawal.quote_created",
  "created_at": "2025-12-01T10:00:00.700Z",
  "data": {
    "object": {
      "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
      "workspace_id": "b53f6690-3242-4942-9907-885779632832",
      "organization_reference_id": null,
      "direction": "outbound",
      "balance_status": "unreserved",
      "balance_reserved_at": null,
      "desired": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": null,
          "currency": "USD",
          "network": null
        }
      },
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "steps": [
        {
          "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 1,
          "step_type": "swap",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "1000",
              "currency": "USDT",
              "network": null
            },
            "to": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:00.700Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        },
        {
          "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 2,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            },
            "to": {
              "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:00.700Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:00:00.700Z",
      "expires_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

:::note
For Scenario 1, the swap step models OpenFX's USDT-to-USD redemption: USDT is debited and USD is credited at the same OpenFX ledger account (`estimated.from.account_id` and `estimated.to.account_id` both equal the OpenFX ledger UUID). The follow-on transfer step then pushes the redeemed USD from the OpenFX ledger to your external bank account.
:::

  </TabItem>
  <TabItem label="Scenario 2">

Example `withdrawal.quote_created` webhook (USDC from a Circle ledger withdrawn via Circle redemption to your external USD bank account; single cross-currency transfer step):

```json
{
  "id": "c7f3a9d2-5b8e-4f14-a1c6-3d9b2e7f4a5c",
  "type": "withdrawal.quote_created",
  "created_at": "2025-12-01T10:00:01.000Z",
  "data": {
    "object": {
      "id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d",
      "workspace_id": "b53f6690-3242-4942-9907-885779632832",
      "organization_reference_id": null,
      "direction": "outbound",
      "balance_status": "unreserved",
      "balance_reserved_at": null,
      "desired": {
        "from": {
          "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
          "amount": "1000",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": null,
          "currency": "USD",
          "network": null
        }
      },
      "estimated": {
        "from": {
          "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
          "amount": "1000",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "1000",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "steps": [
        {
          "id": "3a8e2f5c-9d4b-4f73-a2c0-6b1d3e9f7c4a",
          "withdrawal_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d",
          "step_sequence": 1,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
              "amount": "1000",
              "currency": "USDC",
              "network": null
            },
            "to": {
              "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
              "amount": "1000",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "circle_mint",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:01.000Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:00:01.000Z",
      "expires_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

:::note
For Scenario 2, Circle's USDC-to-USD redemption is absorbed inside the single `transfer` step at parity (1:1, no slippage): `estimated.from.currency` is `USDC` (debited at the Circle ledger) and `estimated.to.currency` is `USD` (credited at your external bank). There is no standalone `swap` step.
:::

  </TabItem>
  <TabItem label="Scenario 3">

Example `withdrawal.quote_created` webhook (USDC on BASE from a self-custodial wallet, withdrawn via OpenFX to your external USD bank account; on-chain transfer + swap + bank transfer):

```json
{
  "id": "d8e2c5a9-4b7f-4135-9c2d-6f3a1b8e4d7c",
  "type": "withdrawal.quote_created",
  "created_at": "2025-12-01T10:00:01.500Z",
  "data": {
    "object": {
      "id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
      "workspace_id": "b53f6690-3242-4942-9907-885779632832",
      "organization_reference_id": null,
      "direction": "outbound",
      "balance_status": "unreserved",
      "balance_reserved_at": null,
      "desired": {
        "from": {
          "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": null,
          "currency": "USD",
          "network": null
        }
      },
      "estimated": {
        "from": {
          "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "steps": [
        {
          "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c",
          "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
          "step_sequence": 1,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
              "amount": "1000",
              "currency": "USDC",
              "network": "BASE"
            },
            "to": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "1000",
              "currency": "USDC",
              "network": "BASE"
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "turnkey",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:01.500Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        },
        {
          "id": "6f4b9d3e-1a8c-4275-b0e6-5d2f8a3c9b1d",
          "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
          "step_sequence": 2,
          "step_type": "swap",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "1000",
              "currency": "USDC",
              "network": null
            },
            "to": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:01.500Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        },
        {
          "id": "8e5c1f9a-3d7b-4148-a6c0-2f4d8b1e9c3a",
          "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
          "step_sequence": 3,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            },
            "to": {
              "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:01.500Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:00:01.500Z",
      "expires_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

:::note
For Scenario 3, Step 1 is an on-chain transfer that delivers USDC from the wallet to the OpenFX ledger account. Because the funds movement happens on chain, the step's `from.network` and `to.network` are both populated with `BASE` in the `estimated` and `actual` overlays. Step 2 is a swap at the OpenFX ledger that converts USDC into USD (`estimated.from.account_id` and `estimated.to.account_id` both equal the OpenFX ledger UUID); both `network` fields are `null` because the swap is purely a ledger-level adjustment. Step 3 pushes the redeemed USD from the OpenFX ledger to your external bank account.
:::

  </TabItem>
</Tabs>

## Withdrawal Balance Check (`withdrawal.balance_updated`)

Withdrawals debit funds from the managed account at `desired.from.account_id`, so Tesser performs a balance check before the withdrawal executes. When the balance check runs and how it surfaces depends on the type of account:

- **Ledger sources (Scenarios 1 and 2):** the check runs asynchronously after the withdrawal is created. Shortly after `withdrawal.quote_created` fires, Tesser reads the ledger's available balance and emits `withdrawal.balance_updated` with the outcome.
- **Wallet sources (Scenario 3):** the check runs synchronously inside your call to the [step-signing API](/api/treasury#sign-withdrawal-step). On success, the sign endpoint accepts your signature and `withdrawal.balance_updated` fires with `balance_status: "reserved"` from the sign-API path alongside `step.signed`. On insufficient funds, the sign endpoint returns a 4XX response and `withdrawal.balance_updated` fires with `balance_status: "awaiting_funds"`. Tesser monitors the wallet's on-chain balance and republishes `step.signature_requested` (with a refreshed `signature_requested_at`) once the wallet is funded; repeat the signing flow with the refreshed step to retry, until `expires_at`.

In both cases, a `reserved` `balance_status` means execution can proceed and `balance_reserved_at` records when the reservation succeeded. See [Balance Statuses](/overviews/funds-movement-lifecycle-and-data-model#balance-statuses).

:::note
If `desired.from.account_id` does not have sufficient funds when the balance check runs, `balance_status` is set to `awaiting_funds`. For ledger sources, Tesser republishes `withdrawal.balance_updated` as the ledger balance is re-evaluated. For wallet sources, Tesser republishes `step.signature_requested` once on-chain funding is observed, and the next `withdrawal.balance_updated` (with `balance_status: "reserved"`) fires on the subsequent successful sign. The retry loop continues until funds arrive or the withdrawal expires — see [Failure Modes for Withdrawals](#failure-modes-for-withdrawals).
:::

Example `withdrawal.balance_updated` webhook (Scenario 1, balance reserved at the source OpenFX ledger):

```json
{
  "id": "a4b6c8d0-3e5f-4a1b-8c2d-4e6f8a0b1c2d",
  "type": "withdrawal.balance_updated",
  "created_at": "2025-12-01T10:00:01.000Z",
  "data": {
    "object": {
      "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
      "workspace_id": "b53f6690-3242-4942-9907-885779632832",
      "organization_reference_id": null,
      "direction": "outbound",
      "balance_status": "reserved",
      "balance_reserved_at": "2025-12-01T10:00:01.000Z",
      "desired": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": null,
          "currency": "USD",
          "network": null
        }
      },
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "steps": [
        {
          "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 1,
          "step_type": "swap",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "1000",
              "currency": "USDT",
              "network": null
            },
            "to": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:00.700Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        },
        {
          "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 2,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            },
            "to": {
              "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:00.700Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:00:01.000Z",
      "expires_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

## Withdrawal Step Execution

Once funds have been reserved, Tesser begins executing each step in `step_sequence` order. The shape of step execution differs by scenario.

### Scenario 1: Withdrawing USDT from an OpenFX ledger

For Scenario 1, Step 1 is a swap at the OpenFX ledger that redeems USDT into USD. Tesser submits the trade, observes its acceptance, and observes its fill in close succession, so `step.submitted`, `step.confirmed`, and `step.completed` arrive close together. The swap's `estimated.from.account_id` and `estimated.to.account_id` are equal — both are the OpenFX ledger UUID `2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b`.

Step 2 is the bank transfer that pushes the redeemed USD from the OpenFX ledger to your external bank account. It follows the standard step lifecycle: `step.submitted` fires when OpenFX accepts the withdrawal instruction; `step.confirmed` fires when OpenFX's local payment network confirms acceptance of the outbound push; `step.completed` fires when funds settle at the external bank account.

Example `step.completed` webhook for Scenario 1, swap step (step 1):

```json
{
  "id": "a4b9c5e2-7d3f-4218-9a6c-5e1f8d3b2a7c",
  "type": "step.completed",
  "created_at": "2025-12-01T10:02:00.500Z",
  "data": {
    "object": {
      "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d",
      "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
      "step_sequence": 1,
      "step_type": "swap",
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.425",
          "currency": "USD",
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "provider_key": "openfx",
      "status": "completed",
      "status_reasons": [],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:02:00.500Z",
      "submitted_at": "2025-12-01T10:02:00.000Z",
      "confirmed_at": "2025-12-01T10:02:00.300Z",
      "completed_at": "2025-12-01T10:02:00.500Z",
      "failed_at": null
    }
  }
}
```

:::note
For ledger-internal swap steps, `transaction_hash` is always `null` — the redemption happens entirely within the provider's books and never touches a chain.
:::

Example `step.completed` webhook for Scenario 1, transfer step (step 2; terminal step):

```json
{
  "id": "e1d6f4a3-9c8b-4f25-a7c0-3b1d8e9f4c5a",
  "type": "step.completed",
  "created_at": "2025-12-01T11:30:00.500Z",
  "data": {
    "object": {
      "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a",
      "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
      "step_sequence": 2,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.425",
          "currency": "USD",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.425",
          "currency": "USD",
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "provider_key": "openfx",
      "status": "completed",
      "status_reasons": [],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T11:30:00.500Z",
      "submitted_at": "2025-12-01T10:02:30.000Z",
      "confirmed_at": "2025-12-01T11:25:00.000Z",
      "completed_at": "2025-12-01T11:30:00.500Z",
      "failed_at": null
    }
  }
}
```

### Scenario 2: Withdrawing USDC from a Circle ledger

For Scenario 2, Tesser initiates the withdrawal via Circle's API as a single call. The withdrawal executes over **one** `transfer` step that moves USDC from the Circle ledger account to your external USD bank account, absorbing the USDC→USD redemption inside the step. `estimated.from.currency` is `USDC` (debited at the Circle ledger) and `estimated.to.currency` is `USD` (credited at your bank). Circle's redemption is 1:1, so `estimated.to.amount` matches `estimated.from.amount` at the indicative quote.

Circle accepts the withdrawal synchronously, so `step.submitted` and `step.confirmed` fire in close succession. Final settlement at the receiving bank lands later via Circle's terminal webhook, at which point `step.completed` fires.

Example `step.completed` webhook for Scenario 2, cross-currency transfer step (the only step; terminal step):

```json
{
  "id": "c5f2a9d7-4b8e-4137-a6c0-3d1f8b9e7c5a",
  "type": "step.completed",
  "created_at": "2025-12-01T11:30:00.500Z",
  "data": {
    "object": {
      "id": "3a8e2f5c-9d4b-4f73-a2c0-6b1d3e9f7c4a",
      "withdrawal_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d",
      "step_sequence": 1,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
          "amount": "1000",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "1000",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
          "amount": "1000",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "1000",
          "currency": "USD",
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "provider_key": "circle_mint",
      "status": "completed",
      "status_reasons": [],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T11:30:00.500Z",
      "submitted_at": "2025-12-01T10:02:00.700Z",
      "confirmed_at": "2025-12-01T10:02:00.800Z",
      "completed_at": "2025-12-01T11:30:00.500Z",
      "failed_at": null
    }
  }
}
```

### Scenario 3: Withdrawing USDC from a self-custodial wallet via OpenFX

When the `desired.from.account_id` is a self-custodial wallet, Tesser cannot submit the on-chain transaction on its own — you must locally sign the unsigned transaction with your wallet's signing key. Tesser emits `step.signature_requested` once Step 1 is prepared, with the unsigned transaction available on the step DTO as `unsigned_transaction` (also retrievable via `GET /v1/treasury/withdrawals/{withdrawalId}`). You sign the unsigned transaction client-side, then submit the signature to `POST /v1/treasury/withdrawals/{withdrawalId}/steps/{stepId}/sign`. Tesser validates the signed transaction targets the prepared step, producing `step.signed`. Tesser then broadcasts on-chain, producing `step.submitted`, `step.confirmed` (with `transaction_hash`), and finally `step.completed` for Step 1. If the same wallet has an earlier transfer awaiting broadcast, this one queues behind it — the sign call returns 200 with `transaction_hash: null` and the step stays `signed` until its turn; see [signing a wallet step](/how-tos/sign-a-wallet-step).

Once Step 1 settles, Step 2 (swap at the OpenFX ledger, USDC → USD) executes synchronously: `step.submitted`, `step.confirmed`, and `step.completed` arrive in close succession. Step 3 (USD push from the OpenFX ledger to your external bank account) follows the standard step lifecycle.

Example `step.signature_requested` webhook (Scenario 3, on-chain transfer from wallet to OpenFX ledger):

```json
{
  "id": "e4f6a8b0-1c3d-4e5f-9a7b-2c4d6e8f0a1b",
  "type": "step.signature_requested",
  "created_at": "2025-12-01T10:00:30.000Z",
  "data": {
    "object": {
      "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c",
      "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
      "step_sequence": 1,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        },
        "to": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "unsigned_transaction": "0x02abcdef0123",
      "provider_key": "turnkey",
      "status": "signature_requested",
      "status_reasons": [],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:00:30.000Z",
      "submitted_at": null,
      "confirmed_at": null,
      "completed_at": null,
      "failed_at": null
    }
  }
}
```

To sign the step, build a `StepForSigning` from the webhook step and pass it to `LocalSigner.signStep`. It needs three values: the `unsigned_transaction`, the source wallet's on-chain address (`crypto_wallet_address` from `GET /v1/accounts/{estimated.from.account_id}`), and the network. Submit the returned `signature` to `POST /v1/treasury/withdrawals/{withdrawalId}/steps/{stepId}/sign` to execute the withdrawal step. See [Sign a Wallet Step](/how-tos/sign-a-wallet-step#sign-the-step-with-the-localsigner-sdk) for install and the full SDK reference.

<Tabs>
  <TabItem label="TypeScript">

```typescript
import { LocalSigner, type StepForSigning } from "@tesser-payments/sdk-ts";

const signer = new LocalSigner({
  signing: {
    publicKey: process.env.SIGNING_PUBLIC_KEY!,
    privateKey: process.env.SIGNING_PRIVATE_KEY!,
    enclaveId: process.env.SIGNING_ENCLAVE_ID!,
  },
});

// 1. Extract the step from a step.signature_requested webhook event.
const step = webhookPayload.data.object;

// 2. Resolve the source wallet's on-chain address.
const account = await fetch(
  `https://api.tesser.xyz/v1/accounts/${step.estimated.from.account_id}`,
  { headers: { Authorization: `Bearer ${token}` } }
).then((res) => res.json());

// 3. Sign locally.
const toSign: StepForSigning = {
  unsignedTransaction: step.unsigned_transaction,
  signWith: account.crypto_wallet_address,
  network: step.estimated.from.network,
};
const { signature } = await signer.signStep(toSign);

// 4. Submit the signature to execute the step.
await fetch(
  `https://api.tesser.xyz/v1/treasury/withdrawals/${step.withdrawal_id}/steps/${step.id}/sign`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ signature }),
  }
);
```

  </TabItem>
  <TabItem label="Kotlin">

```kotlin
import kotlinx.coroutines.runBlocking
import xyz.tesser.sdk.LocalSigner
import xyz.tesser.sdk.SigningConfig
import xyz.tesser.sdk.StepForSigning

val signer = LocalSigner(
  SigningConfig(
    publicKey = System.getenv("SIGNING_PUBLIC_KEY"),
    privateKey = System.getenv("SIGNING_PRIVATE_KEY"),
    enclaveId = System.getenv("SIGNING_ENCLAVE_ID"),
  ),
)

runBlocking {
  // `step` is the step.signature_requested webhook's data.object (parsed).
  // `fetchAccount` / `submitSignature` wrap your own HTTP client.
  val signWith = fetchAccount(step.estimated.from.accountId).cryptoWalletAddress

  val toSign = StepForSigning(
    id = step.id,
    transferId = step.withdrawalId,
    unsignedTransaction = step.unsignedTransaction,
    signWith = signWith,
    network = step.estimated.from.network,
  )

  val signed = signer.signStep(toSign)

  // POST /v1/treasury/withdrawals/{withdrawalId}/steps/{stepId}/sign
  submitSignature(toSign.transferId, toSign.id, signed.signature)
}
```

  </TabItem>
</Tabs>

`signStep` returns a `signature` to send to the withdrawal sign endpoint to execute the step, plus `metadata` (`stampHeaderName`, `stampHeaderValue`, `body`) for debugging that stays client-side. (The Kotlin result also echoes `unsignedTransaction` for audit trails.)

Tesser then broadcasts the signed transaction. You will receive `step.signed`, followed by `step.submitted`, `step.confirmed` (with `transaction_hash` populated), and `step.completed` for Step 1. For wallet-originated on-chain steps, `step.submitted` is an event-only marker — see [Step Statuses](/overviews/funds-movement-lifecycle-and-data-model#step-statuses) in the lifecycle overview.

Example `step.completed` webhook for Scenario 3, on-chain transfer step (step 1):

```json
{
  "id": "a5b7c9d1-3e5f-4a7b-8c9d-0e1f2a3b4c5d",
  "type": "step.completed",
  "created_at": "2025-12-01T10:05:35.000Z",
  "data": {
    "object": {
      "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c",
      "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
      "step_sequence": 1,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        },
        "to": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        }
      },
      "actual": {
        "from": {
          "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        },
        "to": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        }
      },
      "fees": [],
      "transaction_hash": "0xabcdef0123",
      "provider_key": "turnkey",
      "status": "completed",
      "status_reasons": [],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:05:35.000Z",
      "submitted_at": "2025-12-01T10:04:05.000Z",
      "confirmed_at": "2025-12-01T10:04:07.500Z",
      "completed_at": "2025-12-01T10:05:35.000Z",
      "failed_at": null
    }
  }
}
```

Example `step.completed` webhook for Scenario 3, swap step (step 2; USDC → USD at OpenFX):

```json
{
  "id": "f7c3a8d4-2b9e-4516-a0c6-3d1f8b2e7c5a",
  "type": "step.completed",
  "created_at": "2025-12-01T10:06:00.500Z",
  "data": {
    "object": {
      "id": "6f4b9d3e-1a8c-4275-b0e6-5d2f8a3c9b1d",
      "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
      "step_sequence": 2,
      "step_type": "swap",
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.425",
          "currency": "USD",
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "provider_key": "openfx",
      "status": "completed",
      "status_reasons": [],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:06:00.500Z",
      "submitted_at": "2025-12-01T10:06:00.000Z",
      "confirmed_at": "2025-12-01T10:06:00.300Z",
      "completed_at": "2025-12-01T10:06:00.500Z",
      "failed_at": null
    }
  }
}
```

Example `step.completed` webhook for Scenario 3, transfer step (step 3; terminal step):

```json
{
  "id": "d2e8f5a3-4b9c-4617-a0c6-1f3d8b2e7c4a",
  "type": "step.completed",
  "created_at": "2025-12-01T11:30:00.500Z",
  "data": {
    "object": {
      "id": "8e5c1f9a-3d7b-4148-a6c0-2f4d8b1e9c3a",
      "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
      "step_sequence": 3,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.425",
          "currency": "USD",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.425",
          "currency": "USD",
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "provider_key": "openfx",
      "status": "completed",
      "status_reasons": [],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T11:30:00.500Z",
      "submitted_at": "2025-12-01T10:06:30.000Z",
      "confirmed_at": "2025-12-01T11:25:00.000Z",
      "completed_at": "2025-12-01T11:30:00.500Z",
      "failed_at": null
    }
  }
}
```

## Withdrawal Info After Completion

When the last step reaches a terminal state, Tesser populates the top-level `actual.*` overlay and emits a `withdrawal.updated` webhook carrying the full updated Withdrawal object. You can also retrieve the withdrawal at any time via [`GET /v1/treasury/withdrawals/{withdrawalId}`](/api/treasury#get-withdrawal-by-id) — the response payload below matches the body of the terminal `withdrawal.updated` webhook for each scenario.


<Tabs>
  <TabItem label="Scenario 1">

Example `GET /v1/treasury/withdrawals/{withdrawalId}` response (Scenario 1, complete):

```json
{
  "data": {
    "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
    "workspace_id": "b53f6690-3242-4942-9907-885779632832",
    "organization_reference_id": null,
    "direction": "outbound",
    "balance_status": "reserved",
    "balance_reserved_at": "2025-12-01T10:00:01.000Z",
    "desired": {
      "from": {
        "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
        "amount": "1000",
        "currency": "USDT",
        "network": null
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": null,
        "currency": "USD",
        "network": null
      }
    },
    "estimated": {
      "from": {
        "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
        "amount": "1000",
        "currency": "USDT",
        "network": null
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": "999.475",
        "currency": "USD",
        "network": null
      }
    },
    "actual": {
      "from": {
        "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
        "amount": "1000",
        "currency": "USDT",
        "network": null
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": "999.425",
        "currency": "USD",
        "network": null
      }
    },
    "steps": [
      {
        "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d",
        "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
        "step_sequence": 1,
        "step_type": "swap",
        "estimated": {
          "from": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "1000",
            "currency": "USDT",
            "network": null
          },
          "to": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "999.475",
            "currency": "USD",
            "network": null
          }
        },
        "actual": {
          "from": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "1000",
            "currency": "USDT",
            "network": null
          },
          "to": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "999.425",
            "currency": "USD",
            "network": null
          }
        },
        "fees": [],
        "transaction_hash": null,
        "provider_key": "openfx",
        "status": "completed",
        "status_reasons": [],
        "created_at": "2025-12-01T10:00:00.000Z",
        "updated_at": "2025-12-01T10:02:00.500Z",
        "submitted_at": "2025-12-01T10:02:00.000Z",
        "confirmed_at": "2025-12-01T10:02:00.300Z",
        "completed_at": "2025-12-01T10:02:00.500Z",
        "failed_at": null
      },
      {
        "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a",
        "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
        "step_sequence": 2,
        "step_type": "transfer",
        "estimated": {
          "from": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "999.475",
            "currency": "USD",
            "network": null
          },
          "to": {
            "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
            "amount": "999.475",
            "currency": "USD",
            "network": null
          }
        },
        "actual": {
          "from": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "999.425",
            "currency": "USD",
            "network": null
          },
          "to": {
            "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
            "amount": "999.425",
            "currency": "USD",
            "network": null
          }
        },
        "fees": [],
        "transaction_hash": null,
        "provider_key": "openfx",
        "status": "completed",
        "status_reasons": [],
        "created_at": "2025-12-01T10:00:00.000Z",
        "updated_at": "2025-12-01T11:30:00.500Z",
        "submitted_at": "2025-12-01T10:02:30.000Z",
        "confirmed_at": "2025-12-01T11:25:00.000Z",
        "completed_at": "2025-12-01T11:30:00.500Z",
        "failed_at": null
      }
    ],
    "created_at": "2025-12-01T10:00:00.000Z",
    "updated_at": "2025-12-01T11:30:00.700Z",
    "expires_at": "2025-12-01T14:00:00.000Z"
  }
}
```

  </TabItem>
  <TabItem label="Scenario 2">

Example `GET /v1/treasury/withdrawals/{withdrawalId}` response (Scenario 2, complete):

```json
{
  "data": {
    "id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d",
    "workspace_id": "b53f6690-3242-4942-9907-885779632832",
    "organization_reference_id": null,
    "direction": "outbound",
    "balance_status": "reserved",
    "balance_reserved_at": "2025-12-01T10:00:01.000Z",
    "desired": {
      "from": {
        "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
        "amount": "1000",
        "currency": "USDC",
        "network": null
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": null,
        "currency": "USD",
        "network": null
      }
    },
    "estimated": {
      "from": {
        "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
        "amount": "1000",
        "currency": "USDC",
        "network": null
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": "1000",
        "currency": "USD",
        "network": null
      }
    },
    "actual": {
      "from": {
        "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
        "amount": "1000",
        "currency": "USDC",
        "network": null
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": "1000",
        "currency": "USD",
        "network": null
      }
    },
    "steps": [
      {
        "id": "3a8e2f5c-9d4b-4f73-a2c0-6b1d3e9f7c4a",
        "withdrawal_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d",
        "step_sequence": 1,
        "step_type": "transfer",
        "estimated": {
          "from": {
            "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
            "amount": "1000",
            "currency": "USDC",
            "network": null
          },
          "to": {
            "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
            "amount": "1000",
            "currency": "USD",
            "network": null
          }
        },
        "actual": {
          "from": {
            "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
            "amount": "1000",
            "currency": "USDC",
            "network": null
          },
          "to": {
            "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
            "amount": "1000",
            "currency": "USD",
            "network": null
          }
        },
        "fees": [],
        "transaction_hash": null,
        "provider_key": "circle_mint",
        "status": "completed",
        "status_reasons": [],
        "created_at": "2025-12-01T10:00:00.000Z",
        "updated_at": "2025-12-01T11:30:00.500Z",
        "submitted_at": "2025-12-01T10:02:00.700Z",
        "confirmed_at": "2025-12-01T10:02:00.800Z",
        "completed_at": "2025-12-01T11:30:00.500Z",
        "failed_at": null
      }
    ],
    "created_at": "2025-12-01T10:00:00.000Z",
    "updated_at": "2025-12-01T11:30:00.700Z",
    "expires_at": "2025-12-01T14:00:00.000Z"
  }
}
```

  </TabItem>
  <TabItem label="Scenario 3">

Example `GET /v1/treasury/withdrawals/{withdrawalId}` response (Scenario 3, complete):

```json
{
  "data": {
    "id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
    "workspace_id": "b53f6690-3242-4942-9907-885779632832",
    "organization_reference_id": null,
    "direction": "outbound",
    "balance_status": "reserved",
    "balance_reserved_at": "2025-12-01T10:04:05.000Z",
    "desired": {
      "from": {
        "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
        "amount": "1000",
        "currency": "USDC",
        "network": "BASE"
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": null,
        "currency": "USD",
        "network": null
      }
    },
    "estimated": {
      "from": {
        "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
        "amount": "1000",
        "currency": "USDC",
        "network": "BASE"
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": "999.475",
        "currency": "USD",
        "network": null
      }
    },
    "actual": {
      "from": {
        "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
        "amount": "1000",
        "currency": "USDC",
        "network": "BASE"
      },
      "to": {
        "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
        "amount": "999.425",
        "currency": "USD",
        "network": null
      }
    },
    "steps": [
      {
        "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c",
        "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
        "step_sequence": 1,
        "step_type": "transfer",
        "estimated": {
          "from": {
            "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
            "amount": "1000",
            "currency": "USDC",
            "network": "BASE"
          },
          "to": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "1000",
            "currency": "USDC",
            "network": "BASE"
          }
        },
        "actual": {
          "from": {
            "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
            "amount": "1000",
            "currency": "USDC",
            "network": "BASE"
          },
          "to": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "1000",
            "currency": "USDC",
            "network": "BASE"
          }
        },
        "fees": [],
        "transaction_hash": "0xabcdef0123",
        "provider_key": "turnkey",
        "status": "completed",
        "status_reasons": [],
        "created_at": "2025-12-01T10:00:00.000Z",
        "updated_at": "2025-12-01T10:05:35.000Z",
        "submitted_at": "2025-12-01T10:04:05.000Z",
        "confirmed_at": "2025-12-01T10:04:07.500Z",
        "completed_at": "2025-12-01T10:05:35.000Z",
        "failed_at": null
      },
      {
        "id": "6f4b9d3e-1a8c-4275-b0e6-5d2f8a3c9b1d",
        "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
        "step_sequence": 2,
        "step_type": "swap",
        "estimated": {
          "from": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "1000",
            "currency": "USDC",
            "network": null
          },
          "to": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "999.475",
            "currency": "USD",
            "network": null
          }
        },
        "actual": {
          "from": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "1000",
            "currency": "USDC",
            "network": null
          },
          "to": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "999.425",
            "currency": "USD",
            "network": null
          }
        },
        "fees": [],
        "transaction_hash": null,
        "provider_key": "openfx",
        "status": "completed",
        "status_reasons": [],
        "created_at": "2025-12-01T10:00:00.000Z",
        "updated_at": "2025-12-01T10:06:00.500Z",
        "submitted_at": "2025-12-01T10:06:00.000Z",
        "confirmed_at": "2025-12-01T10:06:00.300Z",
        "completed_at": "2025-12-01T10:06:00.500Z",
        "failed_at": null
      },
      {
        "id": "8e5c1f9a-3d7b-4148-a6c0-2f4d8b1e9c3a",
        "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
        "step_sequence": 3,
        "step_type": "transfer",
        "estimated": {
          "from": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "999.475",
            "currency": "USD",
            "network": null
          },
          "to": {
            "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
            "amount": "999.475",
            "currency": "USD",
            "network": null
          }
        },
        "actual": {
          "from": {
            "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
            "amount": "999.425",
            "currency": "USD",
            "network": null
          },
          "to": {
            "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
            "amount": "999.425",
            "currency": "USD",
            "network": null
          }
        },
        "fees": [],
        "transaction_hash": null,
        "provider_key": "openfx",
        "status": "completed",
        "status_reasons": [],
        "created_at": "2025-12-01T10:00:00.000Z",
        "updated_at": "2025-12-01T11:30:00.500Z",
        "submitted_at": "2025-12-01T10:06:30.000Z",
        "confirmed_at": "2025-12-01T11:25:00.000Z",
        "completed_at": "2025-12-01T11:30:00.500Z",
        "failed_at": null
      }
    ],
    "created_at": "2025-12-01T10:00:00.000Z",
    "updated_at": "2025-12-01T11:30:00.700Z",
    "expires_at": "2025-12-01T14:00:00.000Z"
  }
}
```

  </TabItem>
</Tabs>

## Webhook Events by Scenario

Below are the webhook events you will observe for each of the three withdrawal scenarios in this guide.

<Tabs>
  <TabItem label="Scenario 1">

Cross-currency withdrawal at OpenFX (USDT → USD → bank):

| # | What happens | What you receive / do | Fields populated |
|---|---|---|---|
| 1 | You submit the withdrawal request | HTTP response with `id`; `balance_status: "unreserved"` | `desired.from.amount` |
| 2 | Tesser plans the route and obtains a quote | `withdrawal.quote_created` webhook fires with 2 steps and the populated `estimated` overlay | Steps array; `estimated.from.amount` and `estimated.to.amount` at withdrawal and step level (reflects indicative rate) |
| 3 | Tesser reserves USDT at the `desired.from.account_id` | `withdrawal.balance_updated` webhook with `balance_status: "reserved"` | `balance_status`, `balance_reserved_at` |
| 4 | OpenFX redeems USDT for USD on its books | `step.submitted`, `step.confirmed`, and `step.completed` fire on the swap step in close succession | Step 1: `actual.from.amount`, `actual.to.amount` (reflects actual fill rate), all timestamps |
| 5 | OpenFX initiates the push of funds to the external bank | `step.submitted` on the transfer step | Step 2: `submitted_at`, `actual.from.amount` |
| 6 | OpenFX confirms acceptance | `step.confirmed` on the transfer step | Step 2: `confirmed_at` |
| 7 | Funds settle at the external bank; withdrawal is complete | `step.completed` on the transfer step, followed by `withdrawal.updated` with the terminal Withdrawal object | Step 2: `completed_at`, `actual.to.amount`. Withdrawal-level `actual.from.amount` and `actual.to.amount`. |

  </TabItem>
  <TabItem label="Scenario 2">

Cross-currency withdrawal at Circle (USDC → USD → bank):

| # | What happens | What you receive / do | Fields populated |
|---|---|---|---|
| 1 | You submit the withdrawal request | HTTP response with `id`; `balance_status: "unreserved"` | `desired.from.amount` |
| 2 | Tesser plans the route and obtains a quote | `withdrawal.quote_created` webhook fires with 1 cross-currency transfer step and the populated `estimated` overlay | Steps array; `estimated.from.amount` and `estimated.to.amount` at withdrawal and step level (1:1 Circle redemption) |
| 3 | Tesser reserves USDC at the `desired.from.account_id` | `withdrawal.balance_updated` webhook with `balance_status: "reserved"` | `balance_status`, `balance_reserved_at` |
| 4 | Tesser submits the withdrawal to Circle | `step.submitted` on the transfer step | Step 1: `submitted_at` |
| 5 | Circle accepts the withdrawal request | `step.confirmed` on the transfer step (fires close to `step.submitted` since Circle accepts synchronously) | Step 1: `confirmed_at` |
| 6 | Circle's withdrawal completes; funds settle at the external bank | `step.completed` on the transfer step, followed by `withdrawal.updated` with the terminal Withdrawal object | Step 1: `completed_at`, `actual.from.amount` (USDC at Circle ledger), `actual.to.amount` (USD at bank). Withdrawal-level `actual.from.amount` and `actual.to.amount`. |

  </TabItem>
  <TabItem label="Scenario 3">

On-chain transfer from a self-custodial wallet, then withdrawal at OpenFX:

| # | What happens | What you receive / do | Fields populated |
|---|---|---|---|
| 1 | You submit the withdrawal request | HTTP response with `id`; `balance_status: "unreserved"` | `desired.from.amount` |
| 2 | Tesser plans the route and obtains a quote | `withdrawal.quote_created` webhook fires with 3 steps and the populated `estimated` overlay | Steps array; `estimated.from.amount` and `estimated.to.amount` at withdrawal and step level |
| 3 | Tesser asks you to sign the on-chain transfer | `step.signature_requested` on Step 1 | Step 1: `status` updates; signing payload supplied via the API |
| 4 | You submit the signature | `POST /v1/treasury/withdrawals/{withdrawalId}/steps/{stepId}/sign` with `{signature}`; `step.signed` fires; `withdrawal.balance_updated` with `balance_status: "reserved"` once the signed step is accepted | `balance_status`, `balance_reserved_at`; Step 1 `status: "signed"` |
| 5 | Tesser broadcasts the signed transaction | `step.submitted` on Step 1 | Step 1: `submitted_at` |
| 6 | The on-chain transaction is broadcast and accepted into the network's mempool (not yet in a block) | `step.confirmed` on Step 1 | Step 1: `confirmed_at`, `transaction_hash` |
| 7 | The transfer reaches finality at the OpenFX ledger | `step.completed` on Step 1 | Step 1: `completed_at`, `actual.from.amount`, `actual.to.amount` |
| 8 | OpenFX redeems USDC for USD on its books | `step.submitted`, `step.confirmed`, and `step.completed` fire on the swap step in close succession | Step 2: `actual.from.amount`, `actual.to.amount` (reflects actual fill rate), all timestamps |
| 9 | OpenFX initiates the push of funds to the external bank | `step.submitted` on the transfer step | Step 3: `submitted_at`, `actual.from.amount` |
| 10 | OpenFX confirms acceptance | `step.confirmed` on the transfer step | Step 3: `confirmed_at` |
| 11 | Funds settle at the external bank; withdrawal is complete | `step.completed` on the transfer step, followed by `withdrawal.updated` with the terminal Withdrawal object | Step 3: `completed_at`, `actual.to.amount`. Withdrawal-level `actual.from.amount` and `actual.to.amount`. |

  </TabItem>
</Tabs>

## Failure Modes for Withdrawals

If a withdrawal fails, the top-level `actual.*` fields populate only when at least one step has reached `step.status = completed`. With one or more completed steps, the top-level `actual.from` matches the first completed step's `actual.from`, and the top-level `actual.to` matches the last completed step's `actual.to`. With no completed step, the top-level `actual.*` stays all-null — `desired.*` and `estimated.*` describe the originally requested and quoted state, while step-level `status_reasons` carries the cause of the failure. Failed steps always have all-null `actual.*`. Any steps subsequent to the failure step also transition to `failed` with null `actual.*` fields.

### Swap Fails (Scenarios 1 and 3 only)

If the liquidity provider's swap step cannot complete before the withdrawal's `expires_at` timestamp, the swap step transitions to `failed` and the subsequent transfer step also fails because there were no funds to push to the external bank. This failure mode applies only to scenarios with a standalone `swap` step — **Scenario 1** (OpenFX redeems USDT→USD as a swap step at its ledger) and **Scenario 3** (OpenFX redeems USDC→USD as a swap step at its ledger). Scenario 2 has no standalone swap step (Circle's redemption is absorbed into the single `transfer` step), so this failure mode does not apply to Sc 2 — see [Fiat Funds Transfer Fails (All Scenarios)](#fiat-funds-transfer-fails-all-scenarios) below for the Sc 2 failure path.

In Scenario 1 (where the swap is the only execution work before the bank push), no step reaches `step.status = completed` — top-level `actual.*` stays all-null per the Failure Modes rule. In Scenario 3, the prior on-chain transfer step (Step 1) had already completed, so top-level `actual.from` and `actual.to` populate from Step 1's `actual.*` (USDC at the OpenFX ledger).

What you will observe (illustration uses Scenario 1 — OpenFX swap give-up):

- `step.failed` on the swap step. Step-level `actual.*` is null; `status_reasons` carries the failure detail.
- `step.failed` on the transfer step. This step never entered `submitted`, because there were no funds to push to the external bank.
- The withdrawal's top-level `actual.*` is all null because no step reached `completed`. `desired.to.account_id` stays as the originally requested external bank account (client intent is never overwritten); `desired.from` and `estimated.*` describe what was requested and planned.
- A `withdrawal.updated` webhook fires alongside the terminal `step.failed` events, carrying the full updated Withdrawal object.
- The USDT balance at the OpenFX ledger remains in your account and is available to use for a future rebalance or withdrawal.

<Tabs>
  <TabItem label="Step 1 (swap)">

Example `step.failed` webhook on Step 1 (swap, never filled):

```json
{
  "id": "f2c8e4a3-9b7d-4f15-a6c0-2d3f8e1b9c4a",
  "type": "step.failed",
  "created_at": "2025-12-01T14:00:00.000Z",
  "data": {
    "object": {
      "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d",
      "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
      "step_sequence": 1,
      "step_type": "swap",
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "provider_key": "openfx",
      "status": "failed",
      "status_reasons": [
        {
          "error_code": "transfers-9304",
          "error_message": "The currency conversion trade failed"
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T14:00:00.000Z",
      "submitted_at": "2025-12-01T10:32:18.000Z",
      "confirmed_at": null,
      "completed_at": null,
      "failed_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

  </TabItem>
  <TabItem label="Step 2 (bank transfer)">

Example `step.failed` webhook on Step 2 (fiat transfer, never submitted):

```json
{
  "id": "a9d3e7c4-2f8b-4516-a0c6-3e1f9b2a8c5d",
  "type": "step.failed",
  "created_at": "2025-12-01T14:00:00.100Z",
  "data": {
    "object": {
      "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a",
      "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
      "step_sequence": 2,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "provider_key": "openfx",
      "status": "failed",
      "status_reasons": [],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T14:00:00.100Z",
      "submitted_at": null,
      "confirmed_at": null,
      "completed_at": null,
      "failed_at": "2025-12-01T14:00:00.100Z"
    }
  }
}
```

  </TabItem>
</Tabs>

Example `withdrawal.updated` webhook (Scenario 1, swap give-up; USDT remains at OpenFX ledger):

```json
{
  "id": "b3c9f4e7-2d8a-4561-9b0c-4d3f1e9b2a7c",
  "type": "withdrawal.updated",
  "created_at": "2025-12-01T14:00:00.300Z",
  "data": {
    "object": {
      "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
      "workspace_id": "b53f6690-3242-4942-9907-885779632832",
      "organization_reference_id": null,
      "direction": "outbound",
      "balance_status": "unreserved",
      "balance_reserved_at": null,
      "desired": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": null,
          "currency": "USD",
          "network": null
        }
      },
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "steps": [
        {
          "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 1,
          "step_type": "swap",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "1000",
              "currency": "USDT",
              "network": null
            },
            "to": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "failed",
          "status_reasons": [
            {
              "error_code": "transfers-9304",
              "error_message": "The currency conversion trade failed"
            }
          ],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T14:00:00.000Z",
          "submitted_at": "2025-12-01T10:32:18.000Z",
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": "2025-12-01T14:00:00.000Z"
        },
        {
          "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 2,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            },
            "to": {
              "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "failed",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T14:00:00.100Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": "2025-12-01T14:00:00.100Z"
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T14:00:00.300Z",
      "expires_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

### Fiat Funds Transfer Fails (All Scenarios)

The fiat funds `transfer` step (the step that delivers funds to your external bank account) may fail:

- **Between submission and confirmation** — Tesser called the provider, but the provider returned a non-200 response (or no response within Tesser's timeout). `submitted_at` is populated; `confirmed_at` is null; `failed_at` is populated. The upstream balance reservation is rolled back. Funds remain at the upstream source.
- **After confirmation** — The provider accepted the request (so `confirmed_at` is populated), but the provider's terminal webhook later reported failure. `submitted_at` and `confirmed_at` are populated; `completed_at` is null; `failed_at` is populated. What happened to the funds at the provider between confirmation and the terminal webhook depends on the provider's internal handling — for Circle, the redeemed USD typically returns to the Circle ledger; for OpenFX, the USD typically remains at the OpenFX ledger.

Step-level `actual.*` is null on the failed transfer step regardless of sub-case (failed steps never populate `actual.*`).

Top-level `actual.*` is determined by the last completed step:

- **Scenario 1:** Step 1 (swap) had completed, so top-level `actual.from` reflects Step 1's `actual.from` (USDT at the OpenFX ledger) and top-level `actual.to` reflects Step 1's `actual.to` (USD at the OpenFX ledger). USD remains at the OpenFX ledger.
- **Scenario 2:** No upstream step completed (Sc 2 has only the one step that just failed), so top-level `actual.*` stays all-null. USDC remains at the Circle ledger.
- **Scenario 3:** Step 1 (on-chain transfer) and Step 2 (swap) had completed, so top-level `actual.from` reflects Step 1's `actual.from` (USDC at the source wallet) and top-level `actual.to` reflects Step 2's `actual.to` (USD at the OpenFX ledger). USD remains at the OpenFX ledger.

What you will observe (illustration uses Scenario 2 — Circle rejected the withdrawal between submission and confirmation):

- `step.failed` on the transfer step. `submitted_at` is populated; `confirmed_at` is null; `status_reasons` carries the provider's failure detail; step-level `actual.*` is null.
- The withdrawal's top-level `actual.*` is all-null because no step reached `completed`. `desired.to.account_id` stays as the originally requested external bank account.
- A `withdrawal.updated` webhook fires alongside the terminal `step.failed`, carrying the full updated Withdrawal object.
- The USDC balance at the Circle ledger remains in your account and is available to use for a future rebalance or withdrawal.

Example `step.failed` webhook (Scenario 2; Circle rejected the withdrawal between submission and confirmation):

```json
{
  "id": "d4e8f2a7-3c9b-4615-a6c0-1d2f3e9b4c7a",
  "type": "step.failed",
  "created_at": "2025-12-01T10:02:35.000Z",
  "data": {
    "object": {
      "id": "3a8e2f5c-9d4b-4f73-a2c0-6b1d3e9f7c4a",
      "withdrawal_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d",
      "step_sequence": 1,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
          "amount": "1000",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "1000",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "provider_key": "circle_mint",
      "status": "failed",
      "status_reasons": [
        {
          "error_code": "transfers-9302",
          "error_message": "The provider payout failed"
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:02:35.000Z",
      "submitted_at": "2025-12-01T10:02:30.000Z",
      "confirmed_at": null,
      "completed_at": null,
      "failed_at": "2025-12-01T10:02:35.000Z"
    }
  }
}
```

Example `withdrawal.updated` webhook (Scenario 2, withdrawal rejected; USDC remains at Circle ledger):

```json
{
  "id": "e5f3a7d2-4b8c-4561-9a0d-2c3f1e9b8a4c",
  "type": "withdrawal.updated",
  "created_at": "2025-12-01T10:02:35.100Z",
  "data": {
    "object": {
      "id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d",
      "workspace_id": "b53f6690-3242-4942-9907-885779632832",
      "organization_reference_id": null,
      "direction": "outbound",
      "balance_status": "unreserved",
      "balance_reserved_at": null,
      "desired": {
        "from": {
          "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
          "amount": "1000",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": null,
          "currency": "USD",
          "network": null
        }
      },
      "estimated": {
        "from": {
          "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
          "amount": "1000",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "1000",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "steps": [
        {
          "id": "3a8e2f5c-9d4b-4f73-a2c0-6b1d3e9f7c4a",
          "withdrawal_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d",
          "step_sequence": 1,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca",
              "amount": "1000",
              "currency": "USDC",
              "network": null
            },
            "to": {
              "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
              "amount": "1000",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "circle_mint",
          "status": "failed",
          "status_reasons": [
            {
              "error_code": "transfers-9302",
              "error_message": "The provider payout failed"
            }
          ],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:02:35.000Z",
          "submitted_at": "2025-12-01T10:02:30.000Z",
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": "2025-12-01T10:02:35.000Z"
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:02:35.100Z",
      "expires_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

### On-Chain Transfer From Wallet Fails (Scenario 3)

If Step 1's on-chain transfer from the self-custodial wallet to the OpenFX ledger never enters `submitted` — for example, because the customer never signs the step before `expires_at`, or because the sign endpoint rejects the signature — the withdrawal terminates with USDC remaining at the source wallet. The subsequent swap and transfer steps never start. (Post-broadcast failure modes — chain reorg, gas exhaustion, nonce conflict on a signed-and-broadcast transaction — are covered separately.)

What you will observe:

- `step.failed` on Step 1 (on-chain transfer). Step-level `actual.*` is null; `status_reasons` is `[]` because the step never started — the failure cause lives at the resource level (the withdrawal reached `expires_at` waiting for the customer signature, or the sign endpoint rejected the signature).
- `step.failed` on Step 2 (swap) and Step 3 (bank transfer). Neither step was ever submitted. Step-level `actual.*` is null; `status_reasons` is `[]`.
- The withdrawal's top-level `actual.*` is all null because no step reached `completed`. `desired.to.account_id` stays as the originally requested external bank account; `desired.from` and `estimated.*` describe what was requested and planned.
- A `withdrawal.updated` webhook fires alongside the terminal `step.failed` events, carrying the full updated Withdrawal object.

Example `step.failed` webhook for Step 1 (on-chain transfer from BASE wallet, never confirmed):

```json
{
  "id": "fb4c5d6e-7f8a-4b9c-9d0e-1f2a3b4c5d6e",
  "type": "step.failed",
  "created_at": "2025-12-01T14:00:00.000Z",
  "data": {
    "object": {
      "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c",
      "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
      "step_sequence": 1,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        },
        "to": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "fees": [],
      "transaction_hash": null,
      "provider_key": "turnkey",
      "status": "failed",
      "status_reasons": [],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T14:00:00.000Z",
      "submitted_at": null,
      "confirmed_at": null,
      "completed_at": null,
      "failed_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

Example `withdrawal.updated` webhook (Scenario 3, on-chain transfer failed; USDC remains at the source wallet):

```json
{
  "id": "ec7d3a9f-2b4c-4561-a0d8-3e1f5b9c4a7d",
  "type": "withdrawal.updated",
  "created_at": "2025-12-01T14:00:00.300Z",
  "data": {
    "object": {
      "id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
      "workspace_id": "b53f6690-3242-4942-9907-885779632832",
      "organization_reference_id": null,
      "direction": "outbound",
      "balance_status": "unreserved",
      "balance_reserved_at": null,
      "desired": {
        "from": {
          "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": null,
          "currency": "USD",
          "network": null
        }
      },
      "estimated": {
        "from": {
          "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
          "amount": "1000",
          "currency": "USDC",
          "network": "BASE"
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "steps": [
        {
          "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c",
          "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
          "step_sequence": 1,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
              "amount": "1000",
              "currency": "USDC",
              "network": "BASE"
            },
            "to": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "1000",
              "currency": "USDC",
              "network": "BASE"
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "turnkey",
          "status": "failed",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T14:00:00.000Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": "2025-12-01T14:00:00.000Z"
        },
        {
          "id": "6f4b9d3e-1a8c-4275-b0e6-5d2f8a3c9b1d",
          "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
          "step_sequence": 2,
          "step_type": "swap",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "1000",
              "currency": "USDC",
              "network": null
            },
            "to": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "failed",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T14:00:00.100Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": "2025-12-01T14:00:00.100Z"
        },
        {
          "id": "8e5c1f9a-3d7b-4148-a6c0-2f4d8b1e9c3a",
          "withdrawal_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c",
          "step_sequence": 3,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            },
            "to": {
              "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "failed",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T14:00:00.100Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": "2025-12-01T14:00:00.100Z"
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T14:00:00.300Z",
      "expires_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

### Insufficient Funds

If the `desired.from.account_id` does not have enough balance to cover `desired.from.amount` when the balance check runs, the withdrawal enters `balance_status: "awaiting_funds"`. For ledger sources (Scenarios 1 and 2), the async check after creation detects the shortfall and Tesser fires `withdrawal.balance_updated` with `balance_status: "awaiting_funds"`; Tesser then retries the reservation as the balance at `desired.from.account_id` changes (e.g., as a deposit lands or another withdrawal frees funds). For wallet sources (Scenario 3), the sync check inside the step-signing API returns a 4XX rejecting the signature and `withdrawal.balance_updated` fires shortly afterward with `balance_status: "awaiting_funds"`; Tesser monitors the wallet's on-chain balance and republishes `step.signature_requested` (with a refreshed `signature_requested_at`) once the wallet is funded, so you can retry the signing flow with the refreshed step. If the reservation does not succeed before `expires_at`, the withdrawal times out.

What you will observe:

- `withdrawal.balance_updated` with `balance_status: "awaiting_funds"` shortly after `withdrawal.quote_created` (ledger sources) or shortly after a rejected sign call (wallet sources).
- (Optional) further `withdrawal.balance_updated` events as balance changes are detected.
- At `expires_at`: `step.failed` on every step that was still in `created` status. Step-level `actual.*` is null; `status_reasons` is `[]` because the step never started — the failure cause lives at the resource level (the withdrawal timed out after remaining in `awaiting_funds` until `expires_at`).
- The withdrawal's top-level `actual.*` is all null because no step reached `step.status = completed` — no funds ever moved.
- A terminal `withdrawal.updated` webhook fires alongside the `step.failed` events, carrying the full updated Withdrawal object reflecting the terminal state.

The example below illustrates Scenario 1; the same pattern applies in Scenarios 2 and 3, with one `failed` step entry per never-started step.

Example `withdrawal.balance_updated` webhook (Scenario 1, source ledger short):

```json
{
  "id": "ac1b3d5e-7f9a-4b2c-8d4e-6f8a0b2c4d6e",
  "type": "withdrawal.balance_updated",
  "created_at": "2025-12-01T10:00:01.000Z",
  "data": {
    "object": {
      "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
      "workspace_id": "b53f6690-3242-4942-9907-885779632832",
      "organization_reference_id": null,
      "direction": "outbound",
      "balance_status": "awaiting_funds",
      "balance_reserved_at": null,
      "desired": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": null,
          "currency": "USD",
          "network": null
        }
      },
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "steps": [
        {
          "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 1,
          "step_type": "swap",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "1000",
              "currency": "USDT",
              "network": null
            },
            "to": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:00.700Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        },
        {
          "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 2,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            },
            "to": {
              "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "created",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:00:00.700Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": null
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:00:01.000Z",
      "expires_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```

Example `withdrawal.updated` webhook (Scenario 1, source ledger remained underfunded; withdrawal expired):

```json
{
  "id": "ed6f8a0b-2c4e-4f6a-9b0c-3d5f7a9b1c3e",
  "type": "withdrawal.updated",
  "created_at": "2025-12-01T14:00:00.500Z",
  "data": {
    "object": {
      "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
      "workspace_id": "b53f6690-3242-4942-9907-885779632832",
      "organization_reference_id": null,
      "direction": "outbound",
      "balance_status": "unreserved",
      "balance_reserved_at": null,
      "desired": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": null,
          "currency": "USD",
          "network": null
        }
      },
      "estimated": {
        "from": {
          "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
          "amount": "1000",
          "currency": "USDT",
          "network": null
        },
        "to": {
          "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
          "amount": "999.475",
          "currency": "USD",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        },
        "to": {
          "account_id": null,
          "amount": null,
          "currency": null,
          "network": null
        }
      },
      "steps": [
        {
          "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 1,
          "step_type": "swap",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "1000",
              "currency": "USDT",
              "network": null
            },
            "to": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "failed",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T14:00:00.000Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": "2025-12-01T14:00:00.000Z"
        },
        {
          "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a",
          "withdrawal_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
          "step_sequence": 2,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            },
            "to": {
              "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f",
              "amount": "999.475",
              "currency": "USD",
              "network": null
            }
          },
          "actual": {
            "from": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            },
            "to": {
              "account_id": null,
              "amount": null,
              "currency": null,
              "network": null
            }
          },
          "fees": [],
          "transaction_hash": null,
          "provider_key": "openfx",
          "status": "failed",
          "status_reasons": [],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T14:00:00.100Z",
          "submitted_at": null,
          "confirmed_at": null,
          "completed_at": null,
          "failed_at": "2025-12-01T14:00:00.100Z"
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T14:00:00.500Z",
      "expires_at": "2025-12-01T14:00:00.000Z"
    }
  }
}
```
