Skip to main content
Withdrawing funds from a portfolio requires an authorization signed by the end-user’s owner wallet. Stage 1 prepares the full EIP-712 typed-data envelope; the user signs it in their wallet; stage 2 submits the signature and dispatches the onchain transfer. Stage 2 responds 202 and returns an operationId you poll for onchain confirmation.
  • Scope: portfolios:withdraw on both stages.
  • Idempotency anchor: message.nonce returned by stage 1 (also surfaced as authorizationId).
  • Authorization TTL: 10 minutes from stage-1 response.
  • One request = one recipient on one chain. Multi-chain withdrawals require separate authorizations.

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 Stateless. The server validates the request, checks live onchain balances, generates a 32-byte nonce, sets a 10-minute expiry, and returns the full EIP-712 typed-data envelope.
curl -X POST https://api.glider.fi/v2/portfolios/pf_01.../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:
{
  "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": "pf_01JWZG3KH9P4N5QXJVNK7M3WTV",
        "recipientAccountId": "eip155:1:0x4444444444444444444444444444444444444444",
        "assets": [
          {
            "assetId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
            "amountRaw": "1000500000"
          }
        ],
        "nonce": "0xdeadbeef...deadbeef",
        "expiresAt": 1776384600
      }
    }
  }
}

Documented exceptions to the v2 conventions

Inside the signed envelope only, two fields break the usual v2 naming rules because EIP-712 requires them:
  • domain.verifyingContract is a bare EVM address, not a CAIP-10. This is the value the user’s wallet renders during signing.
  • message.expiresAt is Unix seconds as a number, not an ISO-8601 string. Typed-data hashing is binary.
Do not propagate these exceptions outside the envelope. The response-level expiresAt at data.expiresAt is still an ISO-8601 string.

Recipient rules

  • Must be chain-bound CAIP-10 (eip155:<chainId>:<addr>). Chain-agnostic form is rejected with API_211.
  • The recipient chain must match every asset’s chain. Otherwise API_213.
  • The portfolio must have a vault on the recipient’s chain. Otherwise API_215.

Stage 2 — Have the user sign, then submit

The user signs typedData with signTypedData in their wallet. Pass the full object directly — viem, ethers, wagmi, and privy all accept the standard shape:
// 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,
);
Submit to POST /v2/portfolios/{portfolioId}/withdraw with the signature and typedData.message echoed verbatim as body.message.
curl -X POST https://api.glider.fi/v2/portfolios/pf_01.../withdraw \
  -H 'x-api-key: gldr_sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "message": { /* ...typedData.message, unmodified... */ },
    "signature": "0x9412d70d...39e01b"
  }'
Response:
{
  "success": true,
  "data": {
    "operationId": "op_01JWZEE2MF30KVRMRX53N88VA4",
    "submittedAt": "2026-04-17T12:05:00.000Z"
  }
}

Echo the inner message exactly

body.message must be the full typedData.message from stage 1, byte for byte. The server re-builds the EIP-712 envelope and verifies the signature recovers to the portfolio owner. Common mistakes that break verification:
  • Re-serializing amountRaw as a number instead of a string.
  • Changing nonce casing or stripping the 0x prefix.
  • Re-ordering assets[] — array order is part of the hash.
  • Recomputing expiresAt as an ISO string instead of the original Unix seconds integer.
Both EOA (ECDSA) and ERC-1271 (smart-contract wallet) signatures are accepted.

Stage 3 — Poll for onchain confirmation

Stage 2 returns 202. The transfer is dispatched asynchronously. Poll:
curl https://api.glider.fi/v2/portfolios/pf_01.../operations/op_01... \
  -H 'x-api-key: gldr_sk_your_api_key'
Operations transition accepted → running → completed | failed | cancelled. Poll every 2–5 seconds until terminal.

Error Handling

CodeHTTPCauseRetry safe?
API_200404Portfolio not found or not owned by tenantNo
API_210400Insufficient balanceNo
API_211400Invalid recipient (chain-agnostic, or not EVM)No
API_212400Duplicate assetId in the requestNo
API_213400Recipient chain does not match asset chainNo
API_214400message.portfolioId does not match the path parameterNo
API_215400Portfolio has no vault on the recipient’s chainNo
API_216400Authorization expired (past message.expiresAt)No — re-run stage 1
API_217400Signature does not recover to the portfolio ownerNo
API_007409Same nonce, previous call still runningYes — back off and retry
API_008409Same nonce, different body — replay conflictNo
API_506503Signature verifier temporarily unavailableYes — retry with backoff
See Idempotency for the full 409 model.

Common Pitfalls

  • Omitting message from stage 2. The route requires the full typedData.message, not just { nonce, signature }.
  • Using different assets order in stage 2. The typed-data hash depends on array order. Do not sort or rewrite.
  • Running stage 1 more than 10 minutes before stage 2. The authorization has expired. Stage 2 returns API_216 — start over.
  • Treating expiresAt inside message as an ISO string. Inside the signed envelope it is always Unix seconds. The sibling data.expiresAt at the response-envelope level is the ISO-8601 version for human display.