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

# Two-Stage Withdrawal

> Withdraw assets with a two-stage, user-signed authorization flow.

A withdrawal requires authorization from the owner wallet. Stage 1 prepares
the EIP-712 typed data. The user signs the data. Stage 2 sends the signature
and starts the onchain transfer.

Stage 2 returns `202` and an `operationId`. Poll the operation for onchain
confirmation.

* Scope: `portfolios:withdraw` on both stages.
* Idempotency anchor: Stage 1 returns `message.nonce`. The response also shows
  this value as `authorizationId`.
* Authorization TTL: 10 minutes from stage-1 response.
* Request limit: One recipient on one chain. Use separate authorizations for
  multiple chains.

## Sequence

```
Integrator          Glider API                 End-user Wallet
    |                    |                           |
    |--1. POST /v2/portfolios/{id}/withdraw/signature
    |                    |                           |
    |<--200 { typedData, authorizationId, expiresAt }
    |                    |                           |
    |--2. signTypedData(typedData)----------------->|
    |                    |                           |
    |<--signature--------|---------------------------|
    |                    |                           |
    |--3. POST /v2/portfolios/{id}/withdraw
    |      { message: typedData.message, signature }
    |                    |                           |
    |<--202 { operationId, submittedAt }-------------|
    |                    |                           |
    |--4. GET /v2/portfolios/{id}/operations/{opId}  (poll 2–5s)
    |                    |                           |
    |<--200 { state: "completed" }-------------------|
```

## Stage 1: Prepare the authorization

`POST /v2/portfolios/{portfolioId}/withdraw/signature`

Glider validates the request and checks the live onchain balances. Then, it
returns the EIP-712 typed data, a nonce, and a 10-minute expiry.

```bash theme={null}
curl -X POST https://api.glider.fi/v2/portfolios/a1b2c3d4/withdraw/signature \
  -H 'x-api-key: gldr_sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "recipientAccountId": "eip155:1:0x4444444444444444444444444444444444444444",
    "assets": [
      {
        "assetId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
        "amountRaw": "1000500000"
      }
    ]
  }'
```

Response:

```json theme={null}
{
  "success": true,
  "data": {
    "authorizationId": "0xdeadbeef...deadbeef",
    "expiresAt": "2026-04-17T12:10:00.000Z",
    "typedData": {
      "primaryType": "Withdrawal",
      "domain": {
        "name": "Glider Withdrawal Authorization",
        "version": "1",
        "chainId": 1,
        "verifyingContract": "0x2222222222222222222222222222222222222222"
      },
      "types": {
        "Withdrawal": [
          { "name": "portfolioId", "type": "string" },
          { "name": "recipientAccountId", "type": "string" },
          { "name": "assets", "type": "WithdrawalAsset[]" },
          { "name": "nonce", "type": "bytes32" },
          { "name": "expiresAt", "type": "uint256" }
        ],
        "WithdrawalAsset": [
          { "name": "assetId", "type": "string" },
          { "name": "amountRaw", "type": "string" }
        ]
      },
      "message": {
        "portfolioId": "a1b2c3d4",
        "recipientAccountId": "eip155:1:0x4444444444444444444444444444444444444444",
        "assets": [
          {
            "assetId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
            "amountRaw": "1000500000"
          }
        ],
        "nonce": "0xdeadbeef...deadbeef",
        "expiresAt": 1776384600
      }
    }
  }
}
```

### Exceptions to the B2B API conventions

EIP-712 requires two exceptions inside the signed envelope:

* `domain.verifyingContract` is a bare EVM address. It is not a CAIP-10
  identifier. The wallet shows this value during signing.
* `message.expiresAt` is **Unix seconds as a number**, not an ISO-8601
  string. Typed-data hashing is binary.

Do not use these exceptions outside the envelope. The `data.expiresAt` value
is an ISO-8601 string.

### Recipient rules

* Use a chain-bound CAIP-10 value (`eip155:<chainId>:<addr>`). A
  chain-agnostic value returns `API_211`.
* Make sure that the recipient chain matches each asset chain. A mismatch
  returns `API_213`.
* Make sure that the portfolio has a smart account on the recipient chain.
  A missing account returns `API_215`.

## Stage 2: Sign and submit

The user signs `typedData` with `signTypedData` in their wallet. Pass the full
object to the wallet library. Viem, ethers, wagmi, and Privy accept this
shape.

```ts theme={null}
// viem
const signature = await walletClient.signTypedData(typedData);

// ethers v6
const signature = await signer.signTypedData(
  typedData.domain,
  { Withdrawal: typedData.types.Withdrawal,
    WithdrawalAsset: typedData.types.WithdrawalAsset },
  typedData.message,
);
```

Send the signature and `typedData.message` to
`POST /v2/portfolios/{portfolioId}/withdraw`. Send `typedData.message`
unchanged as `body.message`.

```bash theme={null}
curl -X POST https://api.glider.fi/v2/portfolios/a1b2c3d4/withdraw \
  -H 'x-api-key: gldr_sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "message": { /* ...typedData.message, unmodified... */ },
    "signature": "0x9412d70d...39e01b"
  }'
```

Response:

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

### Echo the inner message exactly

`body.message` must equal the full stage-1 `typedData.message`. Keep it
**byte-for-byte unchanged**. These changes cause verification to fail:

* Re-serializing `amountRaw` as a number instead of a string.
* Changing `nonce` casing or stripping the `0x` prefix.
* Changing the order of `assets[]`. Array order is part of the hash.
* Recomputing `expiresAt` as an ISO string instead of the original Unix
  seconds integer.

The API accepts EOA (ECDSA) and ERC-1271 signatures.

## Stage 3: Poll for onchain confirmation

Stage 2 returns `202` and starts an asynchronous transfer. Poll the operation:

```bash theme={null}
curl https://api.glider.fi/v2/portfolios/a1b2c3d4/operations/op_01... \
  -H 'x-api-key: gldr_sk_your_api_key'
```

Poll every 2–5 seconds. Stop when the state is `completed`, `failed`, or
`cancelled`.

## Error handling

| Code      | HTTP | Cause                                                   | Retry safe?              |
| --------- | ---- | ------------------------------------------------------- | ------------------------ |
| `API_200` | 404  | Portfolio not found or not owned by tenant              | No                       |
| `API_210` | 400  | Insufficient balance                                    | No                       |
| `API_211` | 400  | Invalid recipient (chain-agnostic, or not EVM)          | No                       |
| `API_212` | 400  | Duplicate `assetId` in the request                      | No                       |
| `API_213` | 400  | Recipient chain does not match asset chain              | No                       |
| `API_214` | 400  | `message.portfolioId` does not match the path parameter | No                       |
| `API_215` | 400  | Portfolio has no smart account on the recipient's chain | No                       |
| `API_216` | 400  | Authorization expired (past `message.expiresAt`)        | No — re-run stage 1      |
| `API_217` | 400  | Signature does not recover to the portfolio owner       | No                       |
| `API_007` | 409  | Same `nonce`, previous call still running               | Yes — back off and retry |
| `API_008` | 409  | Same `nonce`, different body — replay conflict          | No                       |
| `API_506` | 503  | Signature verifier temporarily unavailable              | Yes — retry with backoff |

See [Idempotency](/guides/idempotency) for the full 409 model.

## Common errors

* **Do not omit `message` from stage 2.** Send the full `typedData.message`.
* **Do not change the order of `assets` in stage 2.** Array order changes the
  typed-data hash.
* **Do not wait more than 10 minutes between stages.** An expired
  authorization returns `API_216`. Start again at stage 1.
* **Do not convert `message.expiresAt` to an ISO string.** This field uses
  Unix seconds. Use `data.expiresAt` for an ISO-8601 display value.
