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

# Withdrawal lifecycle

> Settlement windows per rail, and an adaptive polling pattern for the time between submission and settlement.

Withdrawals don't complete instantly. The transition timing depends on the rail: seconds-to-minutes for stablecoins, hours-to-days for fiat. Use this page to know how long settlement typically takes, and how to poll without burning rate-limit budget waiting on an ACH wire.

<Note title="Why this matters">
  You submit a withdrawal and then wait for it to settle. Surfacing the
  distinction between "still in flight" and "settled" lets you tell a user what
  to expect next.
</Note>

## Status

`status` is a plain string reflecting the withdrawal's current lifecycle state at the moment of response (e.g. `COMPLETED` once funds have left OpenFX). Read the current value from `GET /v3/fx/withdrawals/{id}` or the resource embedded in your webhook payload rather than assuming a fixed set of intermediate states client-side.

<Note>
  **v2 origin.** v3 unifies what v2 split across [`POST
      /v2/.../withdrawal`](/v2/api-reference/withdrawals/initiate-stablecoin-withdrawal)
  (stablecoin) and [`POST
      /v2/.../fiat_withdrawal`](/v2/api-reference/withdrawals/initiate-fiat-withdrawal)
  (fiat). The rail is inferred from the verified withdrawal account. Look up a
  withdrawal by ID via [`GET /v2/brokerage/{orgId}/withdrawal/{withdrawalId}
      `](/v2/api-reference/withdrawals/get-withdrawal-by-id) on v2, or `GET
      /v3/fx/withdrawals/{id}` on v3.
</Note>

## Where money goes

The rail is bound to the verified withdrawal account. You don't pick it on the request. Look up the rail or network on the account record, then consult the appropriate reference page for chain or rail detail.

* **Fiat withdrawals.** See [Settlement times](/v3/settlement-times) for the submission cut-off per currency and rail. Submissions after a cut-off are processed the next banking day.
* **Stablecoin withdrawals.** See [Supported networks](/v3/supported-networks) for the per-stablecoin chain matrix. Once broadcast, `transactionHash` on the withdrawal identifies the on-chain transaction.

<Note>
  **Each verified withdrawal account = one destination × one network × one
  asset.** USDC and USDT on the same Ethereum address are two separate
  `withdrawalAccountId` records; Ethereum-mainnet USDC and Polygon USDC are
  likewise two separate records. This is intentional — it protects clients
  funding from exchanges (where the same nominal address may not actually be
  controllable across all chains, and tokens may have distinct deposit addresses
  even on the same chain) from sending to an unreachable destination. Add each
  destination explicitly via the [dashboard](https://app.openfx.com); there is
  no bulk-add.
</Note>

## Polling pattern with adaptive backoff

Don't poll every 5 seconds for fiat; you'll burn rate-limit budget waiting on a rail whose settlement spans banking hours. Adaptive backoff (start short, grow longer):

```javascript theme={null}
// `signRequest({ method, url, body })` mints a fresh JWT AND a fresh X-Request-Signature
// per call. JWT TTL is 60s; the schedule below reaches 5min ticks and ~25 min total wall-clock,
// so a captured JWT/signature would expire mid-loop. See Rate limiting → Handling rate limits pattern:
// https://docs.openfx.com/v3/rate-limiting#handling-rate-limits-pattern
async function waitForWithdrawal(withdrawalId, signRequest) {
  // Start at 5s; double up to 5min cap. Total wait ~25 min before giving up.
  const delays = [
    5_000, 10_000, 20_000, 40_000, 80_000, 160_000, 300_000, 300_000, 300_000,
  ];
  const url = `https://api.openfx.com/v3/fx/withdrawals/${withdrawalId}`;
  for (const delay of delays) {
    // Mint per tick: even the first interval (5s) is fine, but by the second iteration
    // we are well past the 60s JWT TTL — and the signature is bound to the JWT's nonce.
    const signed = signRequest({ method: "GET", url });
    const res = await fetch(url, { headers: signed.headers });
    const w = await res.json();
    if (w.data?.status === "COMPLETED") return w;
    await new Promise((r) => setTimeout(r, delay));
  }
  throw new Error(
    "Withdrawal not COMPLETED after backoff exhausted; consider webhook delivery.",
  );
}
```

<Tip>
  **Polling is a fallback.** Where possible, prefer receiving webhook events
  over polling for terminal status. See [Webhook
  authentication](/v3/webhooks/authentication) for the current webhook contract.
</Tip>

## When a withdrawal is stuck

### Step 1 — read the trace ID

Every response carries `X-Trace-Id`. Share that ID with OpenFX support if you escalate — it reconstructs the full request server-side. Capture it before doing anything else.

### Step 2 — re-fetch the resource

```bash theme={null}
# Also requires an `X-Request-Signature` header — see /v3/authentication.
curl https://api.openfx.com/v3/fx/withdrawals/<uuid> \
  -H "Authorization: Bearer $OPENFX_JWT" \
  -H "X-Request-Signature: $OPENFX_SIGNATURE"
```

If the row is missing entirely (404), the original POST never landed — replay with the same `Idempotency-Key` (see [Idempotency persistence](/v3/idempotency#a-persistence-schema-you-can-copy)).

### Step 3 — match the state against the rail clock

Not-yet-settled past the rail cut-off is the most common cause of "stuck." See [Settlement times](/v3/settlement-times) for per-rail submission windows. Common cases:

* **Fiat USD via Fedwire:** submissions after 4:30 PM ET batch the next business day.
* **Fiat EUR via SEPA:** submissions after 12:30 PM London time batch the next business day.
* **Stablecoin on Ethereum:** network congestion can delay confirmation 10+ minutes.

A withdrawal still unsettled overnight on a Friday after a Thursday-evening submission is **expected**, not stuck.

### Step 4 — check for background blockers

| Condition                           | Symptom                                         | Resolution                                           |
| ----------------------------------- | ----------------------------------------------- | ---------------------------------------------------- |
| Account de-verified after the POST  | Withdrawal never settles                        | `GET /v3/fx/withdrawal-accounts` — `status: ACTIVE`? |
| Balance changed (intervening trade) | Rejected with `WITHDRAWAL_INSUFFICIENT_BALANCE` | Re-quote, retry with fresh key                       |
| Address verification revoked        | Rejected with `WITHDRAWAL_ADDRESS_NOT_VERIFIED` | Re-verify via dashboard                              |

### Step 5 — check the error catalog for synchronous failures

For failures surfaced synchronously on the POST, the error catalog ([Errors](/v3/errors)) lists every code's retry semantics. Quick guide:

* **Validation codes** (`WITHDRAWAL_INVALID_*`, `WITHDRAWAL_ADDRESS_*`): fix root cause, retry with **new** `Idempotency-Key`. The original key + original body would replay the same failure.
* **State codes** (`WITHDRAWAL_INSUFFICIENT_BALANCE`, `WITHDRAWAL_PENDING_DEPOSIT_REQUIRED`): clear the blocking state, retry with new key.
* **Write-path 500s** (`WITHDRAWAL_INITIATION_FAILED`): **indeterminate** — `GET /v3/fx/withdrawals/{id}` first to confirm whether the withdrawal was actually created before retrying with the same key. See [Idempotency → Crash recovery](/v3/idempotency#crash-recovery).
* **`INTERNAL_ERROR` (500)**: follow its cataloged retry strategy and include the response `X-Trace-Id` if you contact support.

### Step 6 — file a support ticket

Include in the ticket:

* The `X-Trace-Id` from the original POST
* The `id` (readable, `wtd_`-prefixed) of the stuck withdrawal
* Current `status`
* The `Idempotency-Key` used
* Timestamps: `createdAt` from the resource and your client-side request time

A trace ID lets support correlate the request. Do not include request bodies or credentials unless support asks for a specific field through an approved channel.

### Don't do this

* **Replay a rejected withdrawal with the same `Idempotency-Key`.** The cached response is the rejection. Use a fresh key after fixing the root cause.
* **Poll faster than every 5s.** It won't make a SWIFT wire arrive sooner; it will trip `RATE_LIMIT_EXCEEDED`.
* **Assume unsettled means stuck.** Settlement times defines the windows; treat unsettled past the window as the alert threshold, not unsettled itself.

## Common mistakes

* **Fixed-interval polling at 5s.** Fine for the first minute; wasteful for a fiat withdrawal that spans banking hours.
* **Polling without an upper bound.** Always have a "give up and alert" condition.

## What's next

<CardGroup cols={2}>
  <Card title="Trade Settlement" icon="wallet" href="/v3/trade-settlement">
    The full deposit / balance / withdrawal model.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/v3/errors">
    Recovery patterns for rejected withdrawals.
  </Card>

  <Card title="Idempotency" icon="repeat" href="/v3/idempotency">
    7-day TTL on withdrawal keys (carried forward from v2).
  </Card>

  <Card title="Webhooks setup" icon="webhook" href="/v3/webhooks/setup">
    Receive `withdrawals` event notifications instead of polling.
  </Card>
</CardGroup>
