Post Withdrawal Requests

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

You need:

Base URL{{API_BASE_URL}} — all paths below are relative to this
API key + secretRefer to Authentication section
MOVE FUNDS permission on the keyRequired 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 forSee Account groups
A registered settlement instructionA 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:

HeaderValue
Rest-KeyYour API key
Rest-SignSignature 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 OPERATIONS role on that account group.
    Otherwise the call returns 403 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 hold OPERATIONS.

Lists include requests submitted by other members of the same account group, not just your own.


Request states

Each request exposes a state:

StateMeaningCancellableFiatCrypto
PENDING_APPROVALSubmitted, awaiting review by Zodia OperationsYes
PENDINGAccepted by Operations; withdrawal in progressNo
PROCESSEDFunds sentNo
FAILEDProcessing failed — contact your relationship managerNo
CANCELLEDCancelled by you, or declined by OperationsNo

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 state field entirely. Treat a missing
state as unknown rather than as a value.


Endpoints

OperationMethod + path
Submit fiat requestPOST /api/3/withdrawal/fiat
Submit crypto requestPOST /api/3/withdrawal/crypto
List fiat requestsPOST /api/3/withdrawal/fiat/list
List crypto requestsPOST /api/3/withdrawal/crypto/list
Cancel fiat requestPOST /api/3/withdrawal/fiat/{uuid}/cancel
Cancel crypto requestPOST /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

FieldTypeRequiredRules
noncestringyesStrictly increasing per key
accountGroupUuidstring (UUID)yesYou must hold OPERATIONS on this group
ccystringyesA supported currency code, e.g. USD
amountdecimalyesPositive; at most 2 decimal places
withdrawalMethodstringyesBANK_WIRE — the only supported value
bankAccountUuidstring (UUID)yesA verified, enabled beneficiary bank account on this account group, supporting ccy
clientCommentstringnoUp 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

FieldTypeRequiredRules
noncestringyesStrictly increasing per key
accountGroupUuidstring (UUID)yesYou must hold OPERATIONS on this group
ccystringyesA supported asset code
amountdecimalyesPositive; no decimal-place limit
walletParticipantUuidstring (UUID)yesA whitelisted, enabled wallet on this account group, supporting ccy
clientCommentstringnoUp 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:

FieldTypeNotes
uuidstringPass this to the cancel endpoint
amountstringExact decimal, as a string
ccystringCurrency or asset code
statestringSee Request states
accountGroupNamestringPrefer this over the deprecated accountGroup, which carries the same value
accountGroupUuidstring
accountGroupOwner / accountGroupOwnerUuidstringAccount group owner
clientNamestringThe client the request belongs to
requestorstringThe user who actually submitted it
beneficiaryobject{ uuid, name }
clientCommentstringAs submitted
dateCreatedstringISO-8601
lastUpdatedstringISO-8601; changes on every state transition

Fiat rows add:

FieldTypeNotes
withdrawalMethodstringBANK_WIRE
bankAccountobject{ uuid, name, alias }

Crypto rows add:

FieldTypeNotes
coinAddressstringDestination address
walletobject{ 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.
  • accountGroupUuidrequired, in the signed JSON body.
{ "nonce": "1720950000003", "accountGroupUuid": "0b0e6f0e-…" }

accountGroupUuid goes 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 returns 403 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.

StatusMeaning
200Cancelled — { "success": true, "uuid": "…" }
403accountGroupUuid missing or blank in the body, or you lack OPERATIONS on the group
404No such request in that account group
409The request is no longer in PENDING_APPROVAL

Errors

Failures return the relevant HTTP status with:

{ "success": false, "message": "<reason>" }
StatusMeaningmessage examples
401Authentication failed — bad key, signature or nonceUnauthorized
403Invalid request body, missing MOVE FUNDS, or no OPERATIONS role on the account groupInvalid signed params, MOVE FUNDS permission required, You do not have permission to perform this action
404Cancel target not found in that account groupRequest not found
409Cancel not allowed in the current stateRequest can not be cancelled in current state
422Business validation failedSee below
500Unexpected error — the outcome is undefined, see the retry guidance in ConventionsError 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

Fiat:

messageCause
Bank account not found in account groupbankAccountUuid is not a beneficiary account on this account group
Bank account is disabled or is not verifiedThe account exists but is not usable
Bank account does not support the requested currencyCurrency mismatch against the beneficiary account
Insufficient fundsSee below

Crypto:

messageCause
Wallet not found for the caller's accountGroupwalletParticipantUuid is not a wallet on this account group
Wallet is disabled or is not whitelistedThe wallet exists but is not usable
Wallet does not support the requested currencyAsset mismatch against the wallet
Insufficient fundsSee 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/json on 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 new nonce creates a second withdrawal request.
    So if a submit times out or returns 500, 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.