Overview
Returns one ready-to-sign selfBatchWithdraw transaction per chain,
source chain first then ascending chain id. Sign and submit in that
order, data verbatim. Paused and zero-share positions are excluded.
Each transaction has its own intentId (kind close), written
server-side. No auth; no status gate. Optional chainIds in the JSON
body restricts the plan to those chains.
If nothing is withdrawable, transactions is [] and fallback is
"delete": close via DELETE /v1/strategies/{id} instead.
expiresAt and intent TTL semantics match the deposit endpoint (the
pre-deposit purge does not apply).
Endpoint
POST https://earn-api.quicknode.dev/functions/v1/api/v1/strategies/{id}/calldata/withdraw
Authorization
Include the published apikey header. This endpoint is public and does not require a SIWE signature.
Rate limits
This operation belongs to the calldata rate-limit bucket. It allows 10 requests per 60 seconds, keyed by strategy+ip. A coarser limit of approximately 30 requests per 60 seconds also applies per IP across deposit, withdrawal, and claim calldata requests.
Parameters
| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Strategy UUID. |
Request body
Withdraw/close calldata request body. Public (no SIWE).
| Field | Type | Required | Description |
|---|---|---|---|
| chainIds | array<integer> | No | Optional subset of chain ids to withdraw from. Omit to close all chains. |
Example request
curl --request POST \
--url 'https://earn-api.quicknode.dev/functions/v1/api/v1/strategies/b7c1e2d3-0000-4000-8000-000000000001/calldata/withdraw' \
--header 'Accept: application/json' \
--header 'apikey: <EARN_PUBLIC_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{}'
Response
| Field | Type | Required | Description |
|---|---|---|---|
| kind | string (withdraw) | Yes | Identifies the response as a withdraw calldata plan. |
| transactions | array<object> | Yes | One per chain, source-chain-first. Empty when there is nothing to withdraw. |
| approvalsNeeded | array<object> | No | Present on a non-empty plan; omitted on the empty (delete-fallback) plan. |
| expiresAt | string | Yes | ~1h ADVISORY plan-freshness bound. NOT the intent attribution window (the intent carries a 30-day TTL). Regenerate after this passes; regeneration within the intent TTL returns the same `intentId`. |
| plan | object | Yes | Per-chain vault, share, fee, and bridge-burn details for the withdrawal. |
| fallback | string (delete) | No | Present with value `delete` when `transactions` is empty — call `DELETE /v1/strategies/{id}` instead. |
| transactions[].chainId | integer | Yes | Chain on which to submit the transaction. |
| transactions[].to | string | Yes | Quicknode Earn proxy address on `chainId`. |
| transactions[].data | string | Yes | Transaction calldata. Submit it verbatim. |
| transactions[].value | string | Yes | Native-token value. Always `"0"`. |
| transactions[].intentId | string | Yes | Intent UUID used to track this chain close. |
| transactions[].kind | string (close) | Yes | Transaction intent kind. |
| transactions[].description | string | Yes | Human-readable transaction summary. |
| transactions[].gasHint | string | null | Yes | Recommended gas limit, or `null` when it cannot be estimated. |
| transactions[].gasHintReason | string | No | `approvals_required` or `estimation_failed` when `gasHint` is `null`. |
| approvalsNeeded[].chainId | integer | Yes when present | Chain where approval is required. |
| approvalsNeeded[].token | string | Yes when present | Vault share-token contract requiring approval. |
| approvalsNeeded[].spender | string | Yes when present | Quicknode Earn proxy address. |
| approvalsNeeded[].currentAllowance | string | Yes when present | Current allowance in token base units. |
| approvalsNeeded[].requiredAllowance | string | Yes when present | Required allowance in token base units. |
| approvalsNeeded[].tx | object | Yes when present | Ready-to-sign token approval transaction. |
| approvalsNeeded[].tx.to | string | Yes when present | Token contract receiving the approval call. |
| approvalsNeeded[].tx.data | string | Yes when present | ABI-encoded approval calldata. |
| approvalsNeeded[].tx.value | string | Yes when present | Native-token value, normally `"0"`. |
| plan.chains | object | Yes | Withdrawal plan keyed by chain ID. |
| plan.chains.<chainId>.vaults | array<string> | Yes | Vault addresses to withdraw from. |
| plan.chains.<chainId>.shares | array<string> | Yes | Raw share amounts aligned with the vault list. |
| plan.chains.<chainId>.feeAmounts | array<string> | Yes | Fee amounts. User withdrawals return `"0"` entries. |
| plan.chains.<chainId>.totalFeeUsdc | number | Yes | Total fee in decimal US dollars. |
| plan.chains.<chainId>.burns | array<object> | Yes | CCTP burn legs needed to return remote-chain USDC. |
Example response
The data values below are shortened for readability. Submit the complete value returned by the API verbatim.
{
"kind": "withdraw",
"transactions": [
{
"chainId": 8453,
"to": "0x48b415841165304f7EfaA7D5dD5FC65cc7B4bd8e",
"data": "0x458c2be6…",
"value": "0",
"intentId": "7f3a1c2b-0000-4000-8000-000000000abc",
"kind": "close",
"description": "Close 1 position(s) on chain 8453",
"gasHint": "310000"
},
{
"chainId": 42161,
"to": "0x48b415841165304f7EfaA7D5dD5FC65cc7B4bd8e",
"data": "0x458c2be6…",
"value": "0",
"intentId": "8e4b2d3c-0000-4000-8000-000000000abd",
"kind": "close",
"description": "Close 1 position(s) on chain 42161, bridging USDC to the source chain",
"gasHint": "410000"
}
],
"approvalsNeeded": [],
"plan": {
"chains": {
"8453": {
"vaults": ["0x616a4e1db48e22028f6bbf20444cd3b8e3273738"],
"shares": ["599430000000000000000"],
"feeAmounts": ["0"],
"totalFeeUsdc": 0,
"burns": [
{
"destDomain": 0,
"mintRecipient": "0x0000000000000000000000001234567890abcdef1234567890abcdef12345678",
"destinationCaller": "0x0000000000000000000000000000000000000000000000000000000000000000",
"amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
"maxFee": "0",
"minFinalityThreshold": 2000
}
]
},
"42161": {
"vaults": ["0x1a996cb54bb95462040408c06122d45d6cdb6096"],
"shares": ["399120000000000000000"],
"feeAmounts": ["0"],
"totalFeeUsdc": 0,
"burns": [
{
"destDomain": 0,
"mintRecipient": "0x0000000000000000000000001234567890abcdef1234567890abcdef12345678",
"destinationCaller": "0x0000000000000000000000000000000000000000000000000000000000000000",
"amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
"maxFee": "0",
"minFinalityThreshold": 2000
}
]
}
}
},
"expiresAt": "2026-07-07T19:00:00.000Z"
}
HTTP responses
| Status | Description |
|---|---|
| 200 | Per-chain withdraw transactions (source-first), approvals needed, plan transparency, and an advisory expiry. May be an empty plan with a `fallback` hint. |
| 400 | A malformed JSON body or a bad `chainIds` value (`invalid_request`), or a `chainIds` subset that matches no withdrawable position (`no_positions_for_chains`). |
| 404 | Resource not found. |
| 429 | Rate limit exceeded. `Retry-After` (seconds) tells you when to retry. Reads, feedback, and push writes are keyed per IP; create/update/delete are keyed per wallet+IP; deposit/withdraw/claim calldata is keyed per strategy+IP. |
| 500 | Internal server error. |
This page is based on operation withdrawCalldata in the Earn OpenAPI 3.1 specification.