> For the complete documentation index, see [llms.txt](https://docs.nexus.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nexus.xyz/api-reference/account/withdraw.md).

# POST /withdrawals

Submit a withdrawal

Submit a withdrawal.

`operationId`: `withdraw` · CCXT method: `withdraw`

> **Declared server:** `https://api.testnet.nexus.xyz/v1`. See [Networks](/api-reference/guides/networks.md).

**Authentication:** a wallet signature carried in the request body. API-key headers and session tokens are ignored. The description below says what to sign. An unsigned call gets `401`.

## Submit a withdrawal

> Withdraws USDX collateral from the venue back to the signing wallet.\
> \
> \*\*The signature is the authentication, not an API key.\*\* This operation takes no \`X-API-Key\` and no session token: the caller signs an EIP-712 \`WithdrawIntent\` with the wallet's own key and the server recovers the signer from \`signature\`. The recovered address must equal \`wallet\`, or the request is refused with \`401 SIGNER\_MISMATCH\`. \`security\` is omitted rather than \`\[]\` for that reason — the operation is not public, it is authorized by something this document has no scheme for, the same way \`POST /agents/register\` expresses it.\
> \
> EIP-712 domain: \`{ name: 'Nexus Exchange', version: '1', chainId: 20056 }\`. Unlike \`POST /agents/register\`, the chain id is \*\*not\*\* a request field here: the server always verifies against \`20056\`, so a signature produced under any other domain simply does not recover \`wallet\` and is refused with \`401 SIGNATURE\_INVALID\`. Sign with that value; do not substitute a network chain id.\
> Typed data type: \`WithdrawIntent { uint256 amount, address asset, uint64 nonce }\`, where \`asset\` is the USDX sentinel address \`0xcccccccccccccccccccccccccccccccccccccccc\`. The venue is single-collateral and ignores the asset; the address exists only so the typed data has a stable binding.\
> \
> \*\*The destination is implicit.\*\* Funds are credited to the signing wallet; the typed data carries no destination, so there is nothing to redirect. \`destination\` may be sent for clarity but must equal \`wallet\` — any other value is refused with \`400 destination\_locked\`.\
> \
> \*\*\`nonce\` is single-use per wallet.\*\* Re-submitting an already-accepted \`(wallet, nonce)\` pair is refused with \`401 NONCE\_REPLAY\` and creates no second withdrawal, so a client that retries on an ambiguous network error cannot double-withdraw. This operation is not idempotent: a replay is rejected rather than answered with the original result.\
> \
> \*\*\`amount\` is a whole smallest-unit count.\*\* It is a decimal string on the wire, but \`WithdrawIntent\` commits to a \`uint256\`, so a fractional value cannot be represented and is refused with \`400 fractional\_amount\` rather than rounded.\
> \
> \*\*Exposure must be closed first.\*\* A withdrawal that would leave an open position under-collateralized is refused with \`400 WITHDRAW\_BLOCKED\_BY\_OPEN\_POSITION\`; close positions and cancel resting orders before withdrawing.\
> \
> \*\*Acceptance is not settlement.\*\* A \`200\` means the collateral was debited and the withdrawal was recorded with status \`pending\`; it does not mean funds have moved on-chain. Poll \`GET /withdrawals\` for the lifecycle \`status\` and for \`tx\_hash\`, which stays \`null\` until the withdrawal is submitted on-chain.

```json
{"openapi":"3.1.0","info":{"title":"Nexus Exchange API","version":"0.9.101"},"tags":[{"name":"Account","description":"Balance and order management"}],"servers":[{"url":"https://api.testnet.nexus.xyz/v1","description":"Public testnet (play funds) — the default server and /v1 transport base for unversioned paths."},{"url":"https://api.nexus.xyz/v1","description":"Mainnet — **REAL FUNDS**. Collateral is USDX bridged from Ethereum Mainnet; orders placed here move real money, and there is no faucet. The durable per-network base. Listed after testnet on purpose, so nothing that reaches for \"the first https server\" lands on the real-funds target. Not resolvable yet: DNS/TLS is a separate infra change."},{"url":"http://localhost:9090","description":"Local development — the indexer served directly. Not a public network; whatever your local instance holds."}],"paths":{"/withdrawals":{"post":{"operationId":"withdraw","tags":["Account"],"summary":"Submit a withdrawal","description":"Withdraws USDX collateral from the venue back to the signing wallet.\n\n**The signature is the authentication, not an API key.** This operation takes no `X-API-Key` and no session token: the caller signs an EIP-712 `WithdrawIntent` with the wallet's own key and the server recovers the signer from `signature`. The recovered address must equal `wallet`, or the request is refused with `401 SIGNER_MISMATCH`. `security` is omitted rather than `[]` for that reason — the operation is not public, it is authorized by something this document has no scheme for, the same way `POST /agents/register` expresses it.\n\nEIP-712 domain: `{ name: 'Nexus Exchange', version: '1', chainId: 20056 }`. Unlike `POST /agents/register`, the chain id is **not** a request field here: the server always verifies against `20056`, so a signature produced under any other domain simply does not recover `wallet` and is refused with `401 SIGNATURE_INVALID`. Sign with that value; do not substitute a network chain id.\nTyped data type: `WithdrawIntent { uint256 amount, address asset, uint64 nonce }`, where `asset` is the USDX sentinel address `0xcccccccccccccccccccccccccccccccccccccccc`. The venue is single-collateral and ignores the asset; the address exists only so the typed data has a stable binding.\n\n**The destination is implicit.** Funds are credited to the signing wallet; the typed data carries no destination, so there is nothing to redirect. `destination` may be sent for clarity but must equal `wallet` — any other value is refused with `400 destination_locked`.\n\n**`nonce` is single-use per wallet.** Re-submitting an already-accepted `(wallet, nonce)` pair is refused with `401 NONCE_REPLAY` and creates no second withdrawal, so a client that retries on an ambiguous network error cannot double-withdraw. This operation is not idempotent: a replay is rejected rather than answered with the original result.\n\n**`amount` is a whole smallest-unit count.** It is a decimal string on the wire, but `WithdrawIntent` commits to a `uint256`, so a fractional value cannot be represented and is refused with `400 fractional_amount` rather than rounded.\n\n**Exposure must be closed first.** A withdrawal that would leave an open position under-collateralized is refused with `400 WITHDRAW_BLOCKED_BY_OPEN_POSITION`; close positions and cancel resting orders before withdrawing.\n\n**Acceptance is not settlement.** A `200` means the collateral was debited and the withdrawal was recorded with status `pending`; it does not mean funds have moved on-chain. Poll `GET /withdrawals` for the lifecycle `status` and for `tx_hash`, which stays `null` until the withdrawal is submitted on-chain.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalRequest"}}}},"responses":{"200":{"description":"Withdrawal accepted and collateral debited. The withdrawal is recorded with status `pending`; this is an acceptance, not an on-chain settlement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalResponse"}}}},"400":{"description":"The withdrawal was refused before or by the engine. Branch on `code`:\n- `WITHDRAW_BLOCKED_BY_OPEN_POSITION` — the account has open exposure; close it first.\n- `INSUFFICIENT_BALANCE` — the requested amount exceeds withdrawable collateral.\n- `INVALID_AMOUNT` — `amount` is absent, non-numeric, or not greater than zero.\n- `fractional_amount` — `amount` has a fractional part; `WithdrawIntent` carries a `uint256` smallest-unit count.\n- `destination_locked` — `destination` was supplied and does not equal `wallet`.\n- `BAD_WALLET` — `wallet` is not a 0x-prefixed 20-byte address.\n- `INVALID_JSON` / `BAD_REQUEST` — the body could not be read or parsed.\n\nAn engine rejection with no dedicated classification is forwarded unchanged; it carries the same value under both `error` and `code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalError"}}}},"401":{"description":"The signature did not authorize this withdrawal. Branch on `code`:\n- `NONCE_REPLAY` — this `(wallet, nonce)` pair was already accepted. No second withdrawal was created; sign a fresh nonce.\n- `SIGNER_MISMATCH` — the address recovered from `signature` is not `wallet`. The response echoes the `claimed` and `recovered` addresses.\n- `SIGNATURE_INVALID` — `signature` is malformed, not 65 bytes, non-canonical (high-S), or recovered no key.\n- `STORE_ERROR` — the nonce could not be recorded, so the withdrawal was not attempted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalError"}}}},"403":{"description":"Withdrawals are refused for this wallet or this deployment. All three cases are decided before any signature work, so no nonce is consumed and a retry with the same signed intent is safe once the condition clears. Branch on `code`, or on `error` for the freeze envelope:\n- `WITHDRAWALS_DISABLED` (`code`) — the deployment-wide withdrawal kill switch is off.\n- `withdrawals_frozen` (`error`) — the exchange is frozen; balances remain tradeable but new outflows are suspended.\n- `EARLY_ACCESS_REQUIRED` (`code`) — the wallet is not on the early-access allowlist on a deployment that enforces one.\n\nJurisdiction controls do **not** appear here: withdrawal paths are exempt from them by design, because a restriction that traps funds is worse than no restriction.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalError"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"schemas":{"WithdrawalRequest":{"type":"object","description":"A signed `WithdrawIntent`. The server recovers the signer from `signature` and credits that wallet; nothing here selects a destination.","required":["wallet","amount","nonce","signature"],"properties":{"wallet":{"type":"string","description":"Withdrawing wallet address (0x-prefixed, 20 bytes). Must equal the address recovered from `signature`."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Amount to withdraw, as a positive whole number of the asset's smallest unit. `WithdrawIntent` carries a `uint256`, so a fractional value cannot be represented and is refused with `400 fractional_amount`. Encode the same integer you signed."},"asset":{"type":"string","default":"USDX","description":"Asset symbol; defaults to USDX. Recorded on the withdrawal record and not forwarded to the engine. The venue is single-collateral, so the signed `asset` address is always the USDX sentinel."},"nonce":{"type":"integer","format":"int64","description":"Single-use per-wallet nonce, included in the signed typed data. The current Unix timestamp in milliseconds is a safe value. Reusing an accepted nonce returns `401 NONCE_REPLAY` and creates no second withdrawal."},"signature":{"type":"string","description":"EIP-712 signature over `WithdrawIntent{amount, asset, nonce}` from the wallet private key (0x-prefixed, 65 bytes, canonical low-S)."},"destination":{"type":"string","description":"Optional, and locked to `wallet`. The signed intent carries no destination, so any other value is refused with `400 destination_locked`. Omit it unless you are asserting the destination explicitly."}}},"Decimal":{"type":"string","description":"Arbitrary-precision decimal serialized as a string (lossless). Parse with a decimal type, never a float."},"WithdrawalResponse":{"type":"object","description":"Withdrawal acceptance. Carries the authoritative post-withdrawal collateral balance from the engine, plus the identity of the ledger entry the acceptance created. The withdrawal is recorded with status `pending` and is also readable through `GET /withdrawals`; this response is not proof of on-chain settlement.\n\nENG-13528 added `id`, `amount`, `status` and `timestamp`. Before that this object was `balance` alone, so a caller could not follow its own withdrawal — it had to re-read `GET /withdrawals` and guess which row was its own. `txid` is deliberately absent: no on-chain transaction exists yet at acceptance, and it appears on the `GET /withdrawals` entry once the transfer is submitted.","required":["balance","id","amount","status","timestamp"],"properties":{"balance":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Authoritative post-withdrawal collateral balance, as the engine reported it. `null` when the engine accepted the withdrawal but its body could not be read — the acceptance still stands, and the identity fields below are still served, so an unreadable balance is never reported as a fabricated number. Nullable in the schema and not only in this sentence: a generated client typed on a bare string would reject a legal response."},"id":{"type":"integer","format":"int64","description":"Identifier of the `FundsEntry` this acceptance recorded. The same `id` the entry carries on `GET /withdrawals`."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"The accepted withdrawal amount, echoed from the signed intent."},"status":{"type":"string","enum":["pending","submitted","confirmed","failed"],"description":"Status the entry was recorded with. Always `pending` at acceptance; the other values appear only on `GET /withdrawals` after an operator transition."},"timestamp":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"When the entry was recorded, in epoch milliseconds. The same instant the entry carries on `GET /withdrawals`."}},"additionalProperties":true},"TimestampMs":{"type":"integer","format":"int64","description":"Unix epoch timestamp in milliseconds."},"WithdrawalError":{"type":"object","description":"Machine-readable withdrawal rejection. The classification is in `code` for every rejection the exchange raises itself; the exchange-freeze refusal carries it in `error` instead, and a forwarded engine rejection carries the same value under both. Branch on whichever is present — one always is — and treat `message` as human-readable detail only.","anyOf":[{"required":["code"]},{"required":["error"]}],"properties":{"code":{"type":"string","description":"Stable error classification. See the per-status descriptions for the documented values."},"error":{"type":"string","description":"Error classification for the freeze refusal (`withdrawals_frozen`), and a duplicate of `code` on forwarded engine rejections."},"message":{"type":"string","description":"Human-readable detail. Do not branch on it."},"claimed":{"type":"string","description":"On `SIGNER_MISMATCH`: the `wallet` the request claimed."},"recovered":{"type":"string","description":"On `SIGNER_MISMATCH`: the address actually recovered from `signature`."}}},"RateLimitError":{"type":"object","description":"The body of a 429. Branch on `code` (always `RATE_LIMIT_EXCEEDED`) and on `bucket` to learn WHICH budget bottlenecked. Schematised rather than left to `examples` because `bucket` is meant to be read by machines: an example gives a generated client no field and a validator nothing to check `\"bucket\": \"ownr\"` against. `bucket` here and the `X-RateLimit-Bucket` header carry the same value and the same enum.","required":["code","message","bucket"],"properties":{"code":{"type":"string","enum":["RATE_LIMIT_EXCEEDED"],"description":"Always `RATE_LIMIT_EXCEEDED` on this response. The other 429s this API can return carry different codes (`too_many_agents`, `rate_limited`, `cap_exceeded`, `daily_limit_exceeded`) and are not this shape."},"message":{"type":"string","description":"A diagnostic whose wording is NOT stable. Do not parse it; branch on `bucket`."},"bucket":{"type":"string","enum":["key","owner","order","cancel","ip","login"],"description":"Which budget refused this request. `key` — the per-API-key bucket; `owner` — the per-account request bucket; `order` — the trading-action bucket, order submission and amend only; `cancel` — the separate cancellation bucket, `DELETE` on the order surface only; `ip` — the public per-IP bucket for unauthenticated and gateway traffic; `login` — the dedicated per-IP bucket for `POST /auth/login`, tighter than `ip` and drawn from its own pool, so a login refusal leaves the read budget untouched and vice versa. The buckets are independent, so a caller at its read ceiling can still submit orders, and a caller at its submission ceiling can still cancel — do not back off the whole client on any one of them."},"tier":{"type":"string","description":"The rate-limit tier the refused request was charged against — `public` for the per-IP bucket, otherwise the authenticated key's tier. Absent on responses from paths that refuse before a tier is resolved."}}}},"responses":{"RateLimited":{"description":"Rate limit exceeded. **Retryable** — honour `retry-after` and back off; this is what separates a `429` from the permanent `403` jurisdiction refusals. Branch on the body's `code`, which is always `RATE_LIMIT_EXCEEDED`, and on `bucket` to learn WHICH budget bottlenecked — the buckets are independent, so a caller at its read ceiling can still submit orders and should not back off the whole client. Do not parse `message`: it is a diagnostic whose wording is not stable. Requests are charged by weight, so exhausting a budget takes fewer heavy calls than `x-ratelimit-limit` suggests. See “Rate limits” in the API description.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Budget of request weight per second for your tier — not a count of requests. Sent on successful responses too."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Tokens left, in unit-cost requests: 10 means ten weight-1 requests or two weighing 5. Sent on successful responses too. Always `0` on this response."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix timestamp (seconds) when the budget is whole again. Sent only on a `429`, never on a success."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying, never below 1. Derived from the **weighted** cost of the refused request, so it is not in the same unit as `X-RateLimit-Remaining`. Sent only on a `429`."},"X-RateLimit-Bucket":{"schema":{"type":"string","enum":["key","owner","order","cancel","ip","login"]},"description":"Which budget refused this request, mirroring the body's `bucket`. `key` — the per-API-key bucket; `owner` — the per-account request bucket; `order` — the trading-action bucket, order submission and amend only; `cancel` — the separate cancellation bucket, `DELETE` on the order surface only; `ip` — the public per-IP bucket for unauthenticated and gateway traffic; `login` — the dedicated per-IP bucket for `POST /auth/login`, tighter than `ip` and drawn from its own pool, so a login refusal leaves the read budget untouched and vice versa. The read, trading and cancellation budgets are counted separately, so a `429` on one does not mean the others are exhausted — in particular a `429` naming `order` never means your cancels are refused. Sent only on a `429`."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}}}}}
```

***

<sub>API version</sub> <sub></sub><sub>`0.9.131`</sub>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.nexus.xyz/api-reference/account/withdraw.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
