Withdrawals
Return unused prefund to your own registered payout destination — quoted up front, safe to retry, held for a business day, cancellable throughout the hold.
A withdrawal returns your workspace's unused prefund to your own registered payout destination. The API can start and cancel a withdrawal, but it can never choose a destination — which is what makes the money-moving scope safe to automate.
Two scopes split reads from writes:
wallet.read— quote a hypothetical withdrawal, read the workspace's withdrawals, and list its payout destinations.wallet.withdraw— request or cancel one. Deliberately opt-in and separate, so a leaked read-only or ordering key can never trigger a payout.
The safety model, in one paragraph
Requesting a withdrawal never pays instantly: the amount leaves availableBalance immediately, but payout happens only after a business-day hold (payableAfterUtc). Throughout the hold you can cancel and the funds return to your wallet. When the hold ends the withdrawal becomes payable and may already be on its way, so from payableAfterUtc on it can no longer be cancelled. Destination, fee schedule, and hold length are workspace configuration — not request parameters — and the workspace's owners and admins are told by message the moment a withdrawal is requested or cancelled through the API, so the worst a compromised wallet.withdraw key can do is move your money to you, visibly, with a cancellation window.
Where a withdrawal is paid
A workspace owner registers the places its withdrawals may be paid to under Payout details in the dashboard: a bank account, or an address on a network Xegora lists there. The full account or address is stored encrypted and is never shown again; every list shows a masked summary only (Commerzbank · DE · ••••3000).
A new destination cannot receive money straight away. This is deliberate, so that taking over one login or one key cannot quietly redirect your money:
- Only an owner can add, rename, make the default or remove a destination, and adding one asks for a fresh two-factor code.
- Every owner of the workspace is told by message when a destination is added, changed or removed, so a second owner can object.
- A person at Xegora, other than anyone at your workspace, approves the destination after looking at it; a destination nobody approves in time expires.
- It cannot receive money before its cooling-off period ends — 48 hours by default, counted from when it was added (the dashboard and
availableAtUtcshow when) — however early it is approved.
An API withdrawal is paid to your default destination for the currency: the one an owner marked as the default, once Xegora approved it and its cooling-off has passed. The request never carries a destination, so the worst a leaked wallet.withdraw key can do is move your money to an account you already approved. Each withdrawal comes back with a payoutDestination summary that says where it will be paid, and keeps it for the withdrawal's life; changing the default later does not move a withdrawal already requested. If Xegora takes an approval back before a withdrawal is paid, the payout stops and the withdrawal is declined with the amount returned to availableBalance.
A request is refused, and nothing is held, when there is nowhere it may be paid: 409 payout_destination_required when the currency has no approved default (an owner has not set one, or the default still waits for Xegora's approval), or 409 payout_destination_cooling_off with availableAtUtc, the instant the default destination can first receive money.
List the payout destinations (wallet.read)
wallet.read)GET /api/v1/payout-destinations
GET /api/v1/payout-destinations?includeClosed=true
The destinations your owners registered, read only — a key never registers, changes or removes one. Approved destinations come first, then those awaiting approval, newest first within each; includeClosed=true adds the rejected, revoked and removed ones (at most 200 in all).
{
"items": [
{
"id": "0198e0aa-1111-7abc-9def-222233334444",
"publicReference": "pdest_0198e0aa11117abc9def222233334444",
"kind": "BankTransfer",
"label": "Treasury account",
"currency": "USD",
"network": null,
"networkName": null,
"tokenStandard": null,
"token": null,
"maskedSummary": "Commerzbank · DE · ••••3000",
"isDefault": true,
"status": "Approved",
"state": "ready",
"requestedAtUtc": "2026-08-20T09:12:44+00:00",
"availableAtUtc": "2026-08-22T09:12:44+00:00",
"approvedAtUtc": "2026-08-20T10:03:10+00:00",
"closedAtUtc": null,
"canReceiveMoney": true
}
]
}| Field | Meaning |
|---|---|
publicReference | Xegora's reference: pdest_ and 32 hex digits. |
kind | BankTransfer or OnChain. An on-chain destination also names its network (a code, as the deposit options name networks), networkName and tokenStandard to show, and the stablecoin it is paid in (token); all four are null for a bank account. |
isDefault | The destination API withdrawals in its currency are paid to. |
status | Pending, Approved, Rejected, Revoked or Removed. |
state | What the status means now: awaiting_approval, cooling_off (approved, not usable before availableAtUtc), ready, expired (nobody approved it in time), rejected, revoked or removed. |
availableAtUtc | The first instant it can receive money once approved: the end of its cooling-off. |
closedAtUtc | When it was rejected, revoked or removed; null otherwise. |
canReceiveMoney | Approved and past its cooling-off: a withdrawal can be paid to it now. |
Quote a withdrawal (wallet.read)
wallet.read)POST /api/v1/withdrawals/quotes
{ "amount": 100.00, "currency": "USD" }{
"amount": 100.00,
"fee": 1.00,
"net": 99.00,
"currency": "USD",
"payableAfterUtc": "2026-09-08T12:00:00+00:00",
"availableBalance": 425.00
}Quoting has no side effects — use it to show fees and the earliest payout instant before committing. availableBalance is your current headroom. The fee rounds up to the currency's minor unit and is always less than the amount, so net is positive. A quote is refused as a request would be for a malformed amount or currency (400 invalid_request), an amount below the minimum or the fee (422 withdrawal_amount_below_minimum, 422 withdrawal_amount_below_fee), or a currency without an active wallet (409 withdrawal_unavailable); it does not check the balance, the payout destination or the limits. Quoting stays available while the workspace's subscription is overdue or its entitlement has ended.
Request a withdrawal (wallet.withdraw)
wallet.withdraw)POST /api/v1/withdrawals
Idempotency-Key: <optional, 16–128 characters>
{ "amount": 100.00, "currency": "USD", "fee": 1.00, "net": 99.00 }| Field | Rules |
|---|---|
amount | Required. Positive, in at most the currency's minor-unit digits (two for USD). |
currency | Required. A currency the workspace holds an active wallet in. |
fee, net | Optional, both or neither: the terms a quote showed you. With them, a request under terms that changed since the quote is refused 409 withdrawal_terms_changed instead of being held under terms nobody agreed to. One without the other is 400. |
Idempotency — safe retries
Send an Idempotency-Key header (16–128 characters, no control characters; surrounding spaces are trimmed) and a withdrawal request becomes safe to retry:
- The first request creates the withdrawal:
201 Createdwith aLocationheader (/api/v1/withdrawals/{withdrawalId}). - A retry under the same key creates nothing: it is answered
200 OKwith the withdrawal the first request made, as it stands now — and no second message goes to the owners. - The same key with a different
amountorcurrencyis refused409 idempotency_conflict; nothing is held. - A key is spent for good once it has made a withdrawal; a refused request holds nothing and does not spend it. Keys belong to the API key that sends them: two of your API keys never share one.
- Without the header every request is a new withdrawal. On a timeout, read before retrying —
GET /withdrawals/page?status=requested— and resend only if your request is genuinely absent.
Derive the key from your own payout identity (e.g. payout-{yourPayoutId}) and persist it with your record.
A new request returns:
{
"id": "0198e300-4444-7abc-9def-555566667777",
"publicReference": "wdr_0198e30044447abc9def555566667777",
"status": "Requested",
"amount": 100.00,
"fee": 1.00,
"net": 99.00,
"currency": "USD",
"requestedAtUtc": "2026-09-05T12:00:00+00:00",
"payableAfterUtc": "2026-09-08T12:00:00+00:00",
"paidAtUtc": null,
"cancelledAtUtc": null,
"cancellationReason": null,
"heldForReview": false,
"reviewHours": null,
"payoutDestination": {
"id": "0198e0aa-1111-7abc-9def-222233334444",
"publicReference": "pdest_0198e0aa11117abc9def222233334444",
"kind": "BankTransfer",
"label": "Treasury account",
"currency": "USD",
"maskedSummary": "Commerzbank · DE · ••••3000"
},
"payoutReference": null
}publicReference is wdr_ and 32 hex digits; quote it when talking to support or matching bank statements. payoutDestination.kind is BankTransfer or OnChain; for a withdrawal requested before destinations were registered, payoutDestination is null. payoutReference is the reference of the transfer that paid the withdrawal — a wire's reference, or a transaction hash — as Xegora recorded it when it was paid; null until then.
The workspace's owners and admins are told by message as soon as a withdrawal is requested, as they are of one requested in the dashboard.
Why a request is refused
Nothing is held by a refused request.
| Status | code | Why |
|---|---|---|
| 400 | invalid_request | A malformed Idempotency-Key; fee without net or the reverse; an amount that is not positive or has too many decimals; a malformed or unsupported currency; a body member the request does not have. |
| 403 | api_access_inactive | The workspace may not withdraw right now (reason says why). |
| 409 | insufficient_balance | The amount is above your available balance. |
| 409 | payout_destination_required | The currency has no approved default payout destination. |
| 409 | payout_destination_cooling_off | The default destination is still cooling off; availableAtUtc says until when. |
| 409 | withdrawal_terms_changed | The fee and net you sent are not today's terms. Quote again and show the new terms. |
| 409 | idempotency_conflict | The Idempotency-Key already made a different withdrawal. |
| 409 | withdrawal_unavailable | The workspace holds no active wallet in that currency (none, or one that is frozen or closed). |
| 409 | withdrawal_ineligible | The workspace cannot withdraw. |
| 422 | withdrawal_amount_below_minimum | The amount is below the minimum withdrawal. |
| 422 | withdrawal_amount_below_fee | The amount does not cover the withdrawal fee. |
| 422 | withdrawal_limit_exceeded | One withdrawal may take at most the limit Xegora set for the workspace; the body carries limit and currency. |
| 422 | withdrawal_period_limit_exceeded | The last 30 days of withdrawals, this one included, would pass limit; used is what those days already count. |
| 422 | currency_limits_not_configured | Limits are set for the workspace in other currencies but not in this one, so withdrawals in it are refused until Xegora sets them. |
A limit is changed by contacting Xegora operations; GET /api/v1/workspace shows the limits and what the last 30 days already count (limits[].rolling30DayWithdrawalsUsed) to a key holding wallet.read — to any other key the whole limits block is null.
The amount above which a withdrawal is held for review is Xegora's own control and is never reported: no field says it, because a threshold made known could be stayed under. Read heldForReview on the withdrawal instead (below).
A withdrawal held for review
A large withdrawal is looked at by a person before it can be paid. Above the review amount Xegora operations set for your workspace, the request is still created (201 Created, status: "Requested") and comes back with heldForReview: true and reviewHours, how many hours a review usually takes. Nothing else changes for you: the amount is held on the wallet exactly as for any request, you can still cancel it (until payableAfterUtc, and after it for as long as the review lasts, since nothing is paid while it is held), and your workspace owners and admins are told by message that it waits for review and roughly how long that takes (usually within a day).
- Released — the payout proceeds as usual;
heldForReviewbecomesfalseand the owners are told. - Declined — the withdrawal is cancelled with the reason in
cancellationReasonand the amount returns toavailableBalance.
A withdrawal is not paid at payableAfterUtc while heldForReview is true. Treat heldForReview: true after that instant as "waiting for Xegora", not as a fault.
Cancel before payout (wallet.withdraw)
wallet.withdraw)POST /api/v1/withdrawals/{withdrawalId}/cancellation
{ "reason": "requested in error" }The body is optional; a reason is kept as your note (trimmed, at most 200 characters). Cancels a withdrawal that is still in its hold and returns the funds to availableBalance (200 OK with the updated withdrawal, status: "Cancelled"). The workspace's owners and admins are told by message.
| Status | code | Why |
|---|---|---|
| 404 | not_found | No such withdrawal in your workspace. |
| 409 | withdrawal_cancel_window_closed | payableAfterUtc has passed and the payout may already be under way (unless the withdrawal is still heldForReview, when nothing can be under way and it can still be cancelled). |
| 409 | withdrawal_not_pending | The withdrawal is no longer Requested: it was paid or cancelled. |
| 409 | withdrawal_reconciliation_required | The amount the withdrawal held is no longer held on the wallet; Xegora reconciles it before anything moves. Contact support. |
| 403 | api_access_inactive | The workspace may not change anything right now (reason says why). |
Read withdrawals (wallet.read)
wallet.read)GET /api/v1/withdrawals/{withdrawalId}
GET /api/v1/withdrawals/page?status=Requested&offset=0&limit=50
GET /api/v1/withdrawals?status=Requested
- One withdrawal by id; another workspace's, or one that does not exist, is
404 not_found. - A page of the workspace's withdrawals, newest first:
{ items, offset, limit, totalCount, hasMore }, withoffset0–100000 (default 0) andlimit1–100 (default 50). This is the way to walk the whole history. - The list returns the newest 200 withdrawals as a plain array — kept for integrations that read it at once; it never returns more than 200.
status is optional on both lists and takes exactly one status name from the table below — in any letter case, surrounding spaces ignored (Requested, requested, PAID ). Anything else is 400 invalid_request: a number (0, +2), a comma-separated list (Requested,Paid), two names separated by a space, or a name that is not a withdrawal status. This is the same rule the order list applies to its status; narrow by status one call at a time, or read every status and group them yourself.
| Status | Meaning | Terminal? |
|---|---|---|
Requested | Held until payableAfterUtc and cancellable until then; payable after it — unless heldForReview is true, when it waits for Xegora operations to release it. | no |
Paid | Paid out to your registered destination (paidAtUtc set; payoutReference names the transfer). | yes |
Cancelled | Cancelled before payout — by you, or declined by Xegora operations; funds returned (cancelledAtUtc set, and cancellationReason says why). | yes |
Withdrawal statuses are PascalCase on the wire, unlike order statuses.
Operational guidance
- Keep the
wallet.withdrawkey in your treasury tooling only, ideally CIDR-pinned; nothing customer-facing ever needs it. - Send an
Idempotency-Keywith every withdrawal request, and the quote'sfeeandnet. - Reconcile
Paidwithdrawals against your bank/settlement records bypublicReference,netandpayoutReference; the wallet statement shows each one'swithdrawal_holdandwithdrawal_payout(orwithdrawal_release) lines. - A
Requestedwithdrawal past itspayableAfterUtcby more than a business day, and notheldForReview, deserves a support ticket with the withdrawalid.
Updated 18 days ago