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:

  1. Only an owner can add, rename, make the default or remove a destination, and adding one asks for a fresh two-factor code.
  2. Every owner of the workspace is told by message when a destination is added, changed or removed, so a second owner can object.
  3. A person at Xegora, other than anyone at your workspace, approves the destination after looking at it; a destination nobody approves in time expires.
  4. It cannot receive money before its cooling-off period ends — 48 hours by default, counted from when it was added (the dashboard and availableAtUtc show 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)

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
    }
  ]
}
FieldMeaning
publicReferenceXegora's reference: pdest_ and 32 hex digits.
kindBankTransfer 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.
isDefaultThe destination API withdrawals in its currency are paid to.
statusPending, Approved, Rejected, Revoked or Removed.
stateWhat the status means now: awaiting_approval, cooling_off (approved, not usable before availableAtUtc), ready, expired (nobody approved it in time), rejected, revoked or removed.
availableAtUtcThe first instant it can receive money once approved: the end of its cooling-off.
closedAtUtcWhen it was rejected, revoked or removed; null otherwise.
canReceiveMoneyApproved and past its cooling-off: a withdrawal can be paid to it now.

Quote a withdrawal (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)

POST /api/v1/withdrawals
Idempotency-Key: <optional, 16–128 characters>
{ "amount": 100.00, "currency": "USD", "fee": 1.00, "net": 99.00 }
FieldRules
amountRequired. Positive, in at most the currency's minor-unit digits (two for USD).
currencyRequired. A currency the workspace holds an active wallet in.
fee, netOptional, 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 Created with a Location header (/api/v1/withdrawals/{withdrawalId}).
  • A retry under the same key creates nothing: it is answered 200 OK with the withdrawal the first request made, as it stands now — and no second message goes to the owners.
  • The same key with a different amount or currency is refused 409 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.

StatuscodeWhy
400invalid_requestA 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.
403api_access_inactiveThe workspace may not withdraw right now (reason says why).
409insufficient_balanceThe amount is above your available balance.
409payout_destination_requiredThe currency has no approved default payout destination.
409payout_destination_cooling_offThe default destination is still cooling off; availableAtUtc says until when.
409withdrawal_terms_changedThe fee and net you sent are not today's terms. Quote again and show the new terms.
409idempotency_conflictThe Idempotency-Key already made a different withdrawal.
409withdrawal_unavailableThe workspace holds no active wallet in that currency (none, or one that is frozen or closed).
409withdrawal_ineligibleThe workspace cannot withdraw.
422withdrawal_amount_below_minimumThe amount is below the minimum withdrawal.
422withdrawal_amount_below_feeThe amount does not cover the withdrawal fee.
422withdrawal_limit_exceededOne withdrawal may take at most the limit Xegora set for the workspace; the body carries limit and currency.
422withdrawal_period_limit_exceededThe last 30 days of withdrawals, this one included, would pass limit; used is what those days already count.
422currency_limits_not_configuredLimits 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; heldForReview becomes false and the owners are told.
  • Declined — the withdrawal is cancelled with the reason in cancellationReason and the amount returns to availableBalance.

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)

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.

StatuscodeWhy
404not_foundNo such withdrawal in your workspace.
409withdrawal_cancel_window_closedpayableAfterUtc 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).
409withdrawal_not_pendingThe withdrawal is no longer Requested: it was paid or cancelled.
409withdrawal_reconciliation_requiredThe amount the withdrawal held is no longer held on the wallet; Xegora reconciles it before anything moves. Contact support.
403api_access_inactiveThe workspace may not change anything right now (reason says why).

Read withdrawals (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 }, with offset 0–100000 (default 0) and limit 1–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.

StatusMeaningTerminal?
RequestedHeld until payableAfterUtc and cancellable until then; payable after it — unless heldForReview is true, when it waits for Xegora operations to release it.no
PaidPaid out to your registered destination (paidAtUtc set; payoutReference names the transfer).yes
CancelledCancelled 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.withdraw key in your treasury tooling only, ideally CIDR-pinned; nothing customer-facing ever needs it.
  • Send an Idempotency-Key with every withdrawal request, and the quote's fee and net.
  • Reconcile Paid withdrawals against your bank/settlement records by publicReference, net and payoutReference; the wallet statement shows each one's withdrawal_hold and withdrawal_payout (or withdrawal_release) lines.
  • A Requested withdrawal past its payableAfterUtc by more than a business day, and not heldForReview, deserves a support ticket with the withdrawal id.

Did this page help you?