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

# Submit Signed Withdrawal (Stage 2)

> Stage 2 of the two-stage withdrawal flow — submits the user-signed authorization. Accepted operations dispatch async.

Stage 2 of the two-stage withdrawal flow. Accepts the `message` object
returned by stage 1 verbatim, plus the user's `signature` over the
authorization. Verifies the signature against the portfolio owner,
re-checks live onchain balances, and dispatches the transfer onchain.

`body.message` is the structured message from stage 1 — `typedData.message`
for EVM portfolios, `authorization.message` for Solana portfolios (the shape
is identical), including the `liquidate` flag. Echo it back **verbatim**: the
signature was computed over these exact bytes. `body.signature` encoding
depends on how the owner signed:

* **EVM** (and Solana **Model A**, EVM-rooted): `0x…` hex (EIP-712 / EIP-191
  ECDSA; EVM also accepts ERC-1271 smart-contract-wallet signatures).
* **Solana Model B** (Solana-rooted): base58 ed25519.

For Solana, Glider's pooled agent executes the transfer out of the Swig
sub-account and the paymaster sponsors the fee; the verified owner signature
is the authorization. See
[stage 1 → Solana withdrawals](./v2-withdraw-signature#solana-withdrawals).

* Auth: `x-api-key` header (required)
* Scope: `portfolios:withdraw`

<Note>
  **Recipient:** `recipientAccountId` may be any address the owner signs for
  (e.g. a user's smart wallet), on both EVM and Solana — the recipient is
  bound into the owner-signed authorization, so any payout is explicitly
  authorized by the owner. A self-transfer to the smart account / Swig
  sub-account being debited is always rejected (`400 API_211
      INVALID_RECIPIENT`).
</Note>

### Idempotency

Keyed on `nonce` (scoped to your API key). Retries with the **same**
`message` + `signature` replay the cached 202 response. Retries with the
same `nonce` but a **different** body return `409 IDEMPOTENCY_KEY_CONFLICT`
— fix the client, don't retry. Two parallel requests with the same body:
one proceeds, the other sees `409 IDEMPOTENCY_IN_PROGRESS` — retry the
identical body after 2–3 seconds.

If stage 2 fails after accepting the request (e.g., a transient signature
verification error, or balance drops below the requested amount between
stages), the idempotency lock is released so the integrator can re-submit
the same message + signature once the transient issue resolves.

### Liquidation

When the signed `message.liquidate` is `true`, the authorized assets are
swapped to the signed `message.settlementAssetId` settlement asset (USDC by
default; USDT also allowed on EVM) and delivered to the recipient instead of
being transferred as-is. The assets, settlement asset, and recipient must all
reference the same chain; this flow does not perform cross-chain delivery or
bridging. On Solana the swaps and the final delivery run as one engine
operation inside the portfolio's smart account. The returned
`operationId` tracks the swap. Both `liquidate`
and `settlementAssetId` are part of the signed message and cannot be changed here. A
`400 API_219 WITHDRAW_AS_USDC_UNSUPPORTED_CHAIN` is returned when the default
USDC settlement asset is unavailable on the chain; a `400 API_221 UNSUPPORTED_SETTLEMENT_ASSET`
is returned when `settlementAssetId` is not an allowed settlement asset on the chain.

### Polling for onchain status

The response returns `operationId`. Poll
[`GET /v2/portfolios/{portfolioId}/operations/{operationId}`](./v2-get-operation)
until the operation reaches a terminal (`completed` / `failed` / `cancelled`)
state.

### Common error responses

* `400 API_216 WITHDRAWAL_AUTHORIZATION_EXPIRED` — signed authorization's
  `expiresAt` is in the past. Restart the flow via stage 1.
* `400 API_217 INVALID_WITHDRAWAL_SIGNATURE` — signature does not recover
  to the portfolio owner. Almost always a client bug.
* `400 API_210 INSUFFICIENT_BALANCE` — balance dropped below `amountRaw`
  between stage 1 and stage 2. Restart stage 1 to issue a fresh
  authorization.
* `400 API_214 WITHDRAWAL_PORTFOLIO_MISMATCH` — `message.portfolioId`
  doesn't match the URL path.
* `400 API_213 WITHDRAWAL_CHAIN_MISMATCH` — at least one asset is on a
  different chain than `recipientAccountId`.
* `400 API_215 PORTFOLIO_HAS_NO_VAULT_ON_CHAIN` — portfolio has no smart
  account deployed on the recipient's chain.
* `400 API_219 WITHDRAW_AS_USDC_UNSUPPORTED_CHAIN` — `message.liquidate` is
  `true` (with the default USDC settlement asset) but the recipient's chain has no canonical USDC.
* `400 API_221 UNSUPPORTED_SETTLEMENT_ASSET` — `message.settlementAssetId` is not an
  allowed settlement asset on the recipient's chain (configured USDC or USDT
  for an EVM withdrawal, or USDC for a Solana withdrawal).
* `400 API_212 DUPLICATE_WITHDRAW_ASSET` — two or more assets in `assets`
  share the same `assetId`.
* `400 API_218 UNSUPPORTED_WITHDRAW_ASSET` — the requested asset cannot use the
  direct-transfer withdrawal path. For a redemption-required yield share,
  redeem through its registered yield source and then withdraw the underlying
  asset.
* `400 API_211 INVALID_RECIPIENT` — zero address, or a self-transfer to the
  smart account / Swig sub-account being debited.
* `403 API_104 PERMISSION_DENIED` — Solana withdrawal but the `svm_b2b_api`
  feature is not enabled for the tenant.
* `404 API_200 PORTFOLIO_NOT_FOUND` — `portfolioId` doesn't exist or
  your API key cannot access it (can surface at stage 2 if the portfolio
  was archived/reassigned between stages).
* `409 API_007 IDEMPOTENCY_IN_PROGRESS` — another request with the same
  nonce is still running. Retry the identical body.
* `409 API_008 IDEMPOTENCY_KEY_CONFLICT` — nonce reused with a different
  body. Don't retry; restart stage 1.
* `503 API_506 SIGNATURE_VERIFIER_UNAVAILABLE` — signature verification is
  temporarily unavailable. Safe to retry; the idempotency lock has been
  released.

<RequestExample>
  ```bash cURL theme={null}
  curl --request POST \
    --url 'https://api.glider.fi/v2/portfolios/a1b2c3d4/withdraw' \
    --header 'x-api-key: gldr_sk_your_api_key' \
    --header 'Content-Type: application/json' \
    --data '{
      "message": {
        "portfolioId": "a1b2c3d4",
        "recipientAccountId": "eip155:1:0x4444444444444444444444444444444444444444",
        "assets": [
          {
            "assetId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
            "amountRaw": "1000500000"
          }
        ],
        "nonce": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
        "expiresAt": 1744898400
      },
      "signature": "0xabc..."
    }'
  ```

  ```javascript JavaScript theme={null}
  // Typical flow: stage 1 → user signs with wagmi/viem → stage 2.
  import { useSignTypedData } from "wagmi";

  const stage1 = await fetch(`${API}/v2/portfolios/${portfolioId}/withdraw/signature`, {
    method: "POST",
    headers: { "x-api-key": KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      recipientAccountId: `eip155:1:${recipient}`,
      assets: [{ assetId, amountRaw }],
    }),
  }).then((r) => r.json());

  const signature = await signTypedDataAsync(stage1.data.typedData);

  // Stage 2 echoes `typedData.message` back as `body.message`.
  const stage2 = await fetch(`${API}/v2/portfolios/${portfolioId}/withdraw`, {
    method: "POST",
    headers: { "x-api-key": KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ message: stage1.data.typedData.message, signature }),
  }).then((r) => r.json());

  // Poll execution status via GET /v2/portfolios/:id/operations/:operationId
  // until a terminal state is reached.
  const { operationId } = stage2.data;
  ```

  ```javascript JavaScript (Solana, Model B) theme={null}
  import bs58 from "bs58";

  // Stage 1 returns `data.authorization` for Solana portfolios.
  const stage1 = await fetch(`${API}/v2/portfolios/${portfolioId}/withdraw/signature`, {
    method: "POST",
    headers: { "x-api-key": KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      recipientAccountId: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${recipient}`,
      assets: [{ assetId, amountRaw }],
    }),
  }).then((r) => r.json());

  const auth = stage1.data.authorization; // { kind: "solana-message", text, message }

  // The Solana wallet renders `auth.text` and signs its UTF-8 bytes (ed25519).
  const sigBytes = await wallet.signMessage(new TextEncoder().encode(auth.text));
  const signature = bs58.encode(sigBytes);

  // Stage 2 echoes `authorization.message` back as `body.message`.
  const stage2 = await fetch(`${API}/v2/portfolios/${portfolioId}/withdraw`, {
    method: "POST",
    headers: { "x-api-key": KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ message: auth.message, signature }),
  }).then((r) => r.json());

  const { operationId } = stage2.data;
  ```
</RequestExample>

<ResponseExample>
  ```json 202 theme={null}
  {
    "success": true,
    "data": {
      "operationId": "op_01JWZEE2MF30KVRMRX53N88VA4",
      "submittedAt": "2026-04-17T12:05:00.000Z"
    }
  }
  ```

  ```json 400 — expired theme={null}
  {
    "success": false,
    "error": {
      "code": "API_216",
      "message": "Withdrawal authorization expired at 2026-04-17T11:50:00.000Z"
    }
  }
  ```

  ```json 400 — bad signature theme={null}
  {
    "success": false,
    "error": {
      "code": "API_217",
      "message": "Withdrawal signature does not match the portfolio owner"
    }
  }
  ```

  ```json 409 — in flight theme={null}
  {
    "success": false,
    "error": {
      "code": "API_007",
      "message": "Another withdrawal with the same nonce is still in progress"
    }
  }
  ```

  ```json 503 — verifier down theme={null}
  {
    "success": false,
    "error": {
      "code": "API_506",
      "message": "Signature verifier is temporarily unavailable"
    }
  }
  ```
</ResponseExample>
