Client API Specification — Withdrawal Requests
Withdrawal Requests API
A withdrawal request does not move funds. It enters an approval queue; funds are moved only once Zodia Markets Operations has approved and processed the request. Until then you can track the request's state, and while it is still awaiting for approval you can cancel it.
Contents
- Before you start
- Authentication
- Account groups
- Request states
- Endpoints
- Errors
- Conventions
- Worked example
Before you start
You need:
| Base URL | {{API_BASE_URL}} — all paths below are relative to this |
| API key + secret | Refer to Authentication section |
MOVE FUNDS permission on the key | Required by every endpoint on this page, including the read-only list endpoints. A key without it receives 403 MOVE FUNDS permission required |
The OPERATIONS role on the account group you are transacting for | See Account groups |
| A registered settlement instruction | A verified, enabled beneficiary bank account (fiat) or a whitelisted, enabled wallet (crypto). Register these before your first withdrawal request — they cannot be created as part of one |
Authentication
Every endpoint is a POST carrying a signed JSON body, authenticated with two headers:
| Header | Value |
|---|---|
Rest-Key | Your API key |
Rest-Sign | Signature over the request, computed with your API secret |
The JSON body always includes a nonce alongside the operation's own fields:
{ "nonce": "1720950000000", "…": "…" }nonce must be strictly increasing for a given API key. A reused or decreasing value is rejected.
Using the current time in milliseconds is the usual approach.
Signature computation is identical to the rest of the v3 API — see {{LINK_TO_EXISTING_V3_AUTH_DOCS}}. Two points matter especially here:
- The signature covers the request body and the endpoint path only. Anything you put in the
query string is not signed, and endpoints on this page will not read parameters from it.- Sign the exact byte sequence you transmit. Re-serialising the body after signing (pretty-printing,
reordering keys) invalidates the signature.
Authentication failures return 401 Unauthorized.
Account groups
Every operation is scoped to an account group — the entity whose balances and settlement
instructions you are acting on. You identify it with accountGroupUuid.
- To submit or cancel, your key's user must hold the
OPERATIONSrole on that account group.
Otherwise the call returns403 You do not have permission to perform this action. - The list endpoints take no account-group parameter. They return requests across every account
group your key can access, and silently omit any group where you do not holdOPERATIONS.
Lists include requests submitted by other members of the same account group, not just your own.
Request states
Each request exposes a state:
| State | Meaning | Cancellable | Fiat | Crypto |
|---|---|---|---|---|
PENDING_APPROVAL | Submitted, awaiting review by Zodia Operations | Yes | ✔ | ✔ |
PENDING | Accepted by Operations; withdrawal in progress | No | ✔ | ✔ |
PROCESSED | Funds sent | No | ✔ | ✔ |
FAILED | Processing failed — contact your relationship manager | No | — | ✔ |
CANCELLED | Cancelled by you, or declined by Operations | No | ✔ | ✔ |
PENDING_APPROVAL is the only state a request can be cancelled from. Note that CANCELLED does not
distinguish between a client cancellation and a decision by Operations.
Requests created before this API was introduced may omit the
statefield entirely. Treat a missing
stateas unknown rather than as a value.
Endpoints
| Operation | Method + path |
|---|---|
| Submit fiat request | POST /api/3/withdrawal/fiat |
| Submit crypto request | POST /api/3/withdrawal/crypto |
| List fiat requests | POST /api/3/withdrawal/fiat/list |
| List crypto requests | POST /api/3/withdrawal/crypto/list |
| Cancel fiat request | POST /api/3/withdrawal/fiat/{uuid}/cancel |
| Cancel crypto request | POST /api/3/withdrawal/crypto/{uuid}/cancel |
All six are POST and take a signed JSON body — including the read-only lists.
Submit a fiat withdrawal request
POST /api/3/withdrawal/fiat
| Field | Type | Required | Rules |
|---|---|---|---|
nonce | string | yes | Strictly increasing per key |
accountGroupUuid | string (UUID) | yes | You must hold OPERATIONS on this group |
ccy | string | yes | A supported currency code, e.g. USD |
amount | decimal | yes | Positive; at most 2 decimal places |
withdrawalMethod | string | yes | BANK_WIRE — the only supported value |
bankAccountUuid | string (UUID) | yes | A verified, enabled beneficiary bank account on this account group, supporting ccy |
clientComment | string | no | Up to 1024 characters; echoed back on the list endpoints |
Returns 200 with the new request's identifier. The request starts in PENDING_APPROVAL:
{ "success": true, "uuid": "e1b2c3d4-…" }Business rejections return 422 — see Errors.
Submit a crypto withdrawal request
POST /api/3/withdrawal/crypto
| Field | Type | Required | Rules |
|---|---|---|---|
nonce | string | yes | Strictly increasing per key |
accountGroupUuid | string (UUID) | yes | You must hold OPERATIONS on this group |
ccy | string | yes | A supported asset code |
amount | decimal | yes | Positive; no decimal-place limit |
walletParticipantUuid | string (UUID) | yes | A whitelisted, enabled wallet on this account group, supporting ccy |
clientComment | string | no | Up to 1024 characters |
Response is the same shape as fiat.
List withdrawal requests
POST /api/3/withdrawal/fiat/list · POST /api/3/withdrawal/crypto/list
Body carries only the nonce:
{ "nonce": "1720950000002" }Returns a 200 JSON array, newest first by creation date. There is no pagination and no
filtering — every request you can see is returned, so filter and page client-side.
Common fields on both:
| Field | Type | Notes |
|---|---|---|
uuid | string | Pass this to the cancel endpoint |
amount | string | Exact decimal, as a string |
ccy | string | Currency or asset code |
state | string | See Request states |
accountGroupName | string | Prefer this over the deprecated accountGroup, which carries the same value |
accountGroupUuid | string | |
accountGroupOwner / accountGroupOwnerUuid | string | Account group owner |
clientName | string | The client the request belongs to |
requestor | string | The user who actually submitted it |
beneficiary | object | { uuid, name } |
clientComment | string | As submitted |
dateCreated | string | ISO-8601 |
lastUpdated | string | ISO-8601; changes on every state transition |
Fiat rows add:
| Field | Type | Notes |
|---|---|---|
withdrawalMethod | string | BANK_WIRE |
bankAccount | object | { uuid, name, alias } |
Crypto rows add:
| Field | Type | Notes |
|---|---|---|
coinAddress | string | Destination address |
wallet | object | { uuid, address, alias } |
Crypto rows carry no withdrawalMethod.
Cancel a withdrawal request
POST /api/3/withdrawal/fiat/{uuid}/cancel · POST /api/3/withdrawal/crypto/{uuid}/cancel
{uuid}— the request identifier, in the path.accountGroupUuid— required, in the signed JSON body.
{ "nonce": "1720950000003", "accountGroupUuid": "0b0e6f0e-…" }
accountGroupUuidgoes in the body, not the query string. Because the signature does not cover
the query string, a value passed on the URL would be unsigned and is not read. Sending
?accountGroupUuid=…and omitting it from the body returns403 Invalid signed params.
Cancellation is only possible while the request is in PENDING_APPROVAL. Once Operations has picked
the request up, cancel returns 409 and the state is unchanged — for crypto this window closes as soon
as Operations accepts the request. After a successful cancel the request reports state: CANCELLED.
| Status | Meaning |
|---|---|
200 | Cancelled — { "success": true, "uuid": "…" } |
403 | accountGroupUuid missing or blank in the body, or you lack OPERATIONS on the group |
404 | No such request in that account group |
409 | The request is no longer in PENDING_APPROVAL |
Errors
Failures return the relevant HTTP status with:
{ "success": false, "message": "<reason>" }| Status | Meaning | message examples |
|---|---|---|
401 | Authentication failed — bad key, signature or nonce | Unauthorized |
403 | Invalid request body, missing MOVE FUNDS, or no OPERATIONS role on the account group | Invalid signed params, MOVE FUNDS permission required, You do not have permission to perform this action |
404 | Cancel target not found in that account group | Request not found |
409 | Cancel not allowed in the current state | Request can not be cancelled in current state |
422 | Business validation failed | See below |
500 | Unexpected error — the outcome is undefined, see the retry guidance in Conventions | Error while processing request |
Validate your fields before sending
Field-level validation failures return 403 Invalid signed params, the same status as a malformed
or unsigned body — and the response does not indicate which field was at fault. Validate
client-side against the field tables above (required fields present, amount positive and within the
decimal-place limit, withdrawalMethod set to BANK_WIRE, clientComment within 1024 characters)
rather than relying on the API to tell you what was wrong.
422 business rejections
422 business rejectionsFiat:
message | Cause |
|---|---|
Bank account not found in account group | bankAccountUuid is not a beneficiary account on this account group |
Bank account is disabled or is not verified | The account exists but is not usable |
Bank account does not support the requested currency | Currency mismatch against the beneficiary account |
Insufficient funds | See below |
Crypto:
message | Cause |
|---|---|
Wallet not found for the caller's accountGroup | walletParticipantUuid is not a wallet on this account group |
Wallet is disabled or is not whitelisted | The wallet exists but is not usable |
Wallet does not support the requested currency | Asset mismatch against the wallet |
Insufficient funds | See below |
Insufficient funds means the amount exceeds what is available to withdraw on that currency's
account — your total balance less the withdrawal requests you already have in flight. Because in-flight
requests are counted, several individually-affordable requests can collectively exceed your balance and
the last one will be rejected. If you receive this unexpectedly, call the matching list endpoint to see
what is still outstanding.
Conventions
Content-Type: application/jsonon every request and response.- Fields with no value are omitted from responses rather than returned as
null. - Monetary amounts are returned as strings to preserve exact decimals — parse them as decimals, not
floats. - Timestamps are ISO-8601 with milliseconds and a zone offset, e.g.
2026-08-04T10:15:30.451+01:00. - Successful submit and cancel calls return
{ "success": true, "uuid": "…" }. - Requests are not idempotent and there is no idempotency key. Because each call needs a fresh
nonce, you cannot safely retry a submit by resending it verbatim: replaying the original body is
rejected by the nonce check, and resending with a newnoncecreates a second withdrawal request.
So if a submit times out or returns500, do not retry blind — call the matching list endpoint first
to establish whether the request was created, and only resubmit if it was not.
Worked example
Submit a USD withdrawal, then cancel it while it is still awaiting approval.
1 — Submit
POST /api/3/withdrawal/fiat
Rest-Key: <api key>
Rest-Sign: <signature over body>
Content-Type: application/json
{
"nonce": "1720950000000",
"accountGroupUuid": "0b0e6f0e-…",
"ccy": "USD",
"amount": 2500.00,
"withdrawalMethod": "BANK_WIRE",
"bankAccountUuid": "7f3a9c21-…",
"clientComment": "Monthly settlement"
}{ "success": true, "uuid": "e1b2c3d4-…" }2 — Confirm it is awaiting approval
POST /api/3/withdrawal/fiat/list
Rest-Key: <api key>
Rest-Sign: <signature over body>
Content-Type: application/json
{ "nonce": "1720950000001" }[
{
"uuid": "e1b2c3d4-…",
"amount": "2500.00",
"ccy": "USD",
"state": "PENDING_APPROVAL",
"withdrawalMethod": "BANK_WIRE",
"bankAccount": { "uuid": "7f3a9c21-…", "name": "…", "alias": "…" },
"clientComment": "Monthly settlement",
"dateCreated": "2026-08-04T10:15:30.451+01:00",
"lastUpdated": "2026-08-04T10:15:30.451+01:00"
}
]3 — Cancel it — note accountGroupUuid in the body, and the request uuid in the path:
POST /api/3/withdrawal/fiat/e1b2c3d4-…/cancel
Rest-Key: <api key>
Rest-Sign: <signature over body>
Content-Type: application/json
{ "nonce": "1720950000002", "accountGroupUuid": "0b0e6f0e-…" }{ "success": true, "uuid": "e1b2c3d4-…" }The request now reports state: CANCELLED.