wallets:withdraw scope when creating a dedicated key, store its signing secret once, and keep it in a server-side secret store.
Production withdrawal submission requires approved KYB for every supported NGN
and USD settlement rail. Withdrawal quotes remain available before approval,
but submit returns 403 while KYB is non-approved. Development and staging skip
this KYB gate.
The signed Partner API request authorizes the withdrawal directly. There is no
emailed verification step and no client-supplied withdrawal token after submit.
See Withdrawal examples for complete Node.js and
Python implementations that quote, sign, submit, and hand completion off to
webhooks.
Routes
This page covers a withdrawal to one beneficiary and the signing, retry, and error rules that apply to every withdrawal. An NGN request can also carry up to 50 beneficiaries; see Bulk withdrawals for the multi-beneficiary specifics.
Quote and submit require
X-Api-Key, X-Stabyl-Timestamp, X-Stabyl-Nonce, and X-Stabyl-Signature. Transaction reads require X-Api-Key and wallets:view.
Request Signing
Use the exact bytes sent in the HTTP body. Do not parse and reserialize JSON between hashing and sending the request./v1/partner/wallets/withdrawals, not
/partner/wallets/withdrawals. Include the query string exactly when one is
present.
The server accepts a timestamp up to 30 seconds behind or ahead. A verified nonce is single-use for five minutes. Synchronize server clocks with NTP.
Test Vector
These values are synthetic and must never be used outside tests.Quote NGN
Quote bodies always useitems and omit idempotency_key:
100000000.00 NGN, so a quote for 200000000.00 NGN reports two transfers. The parent amount, fee, and debit remain aggregate values.
Quote Response
Quotes return one entry per settlement route underquotes:
bank_transfer entry with
status: "available". A USD quote contains one entry for the requested
settlement_rail; it adds expires_at and settlement_estimate, omits
max_transfer_amount, and can carry a status other than available with a
reason. Submit a USD withdrawal before its quote expires.
Quoting never reserves funds, but it does run the same account checks as
submit: it returns 422 ACCOUNT_WITHDRAWAL_LIMIT_EXCEEDED or
402 INSUFFICIENT_BALANCE when the request could not be submitted, and it has
its own rate limit, so quote as part of a user action rather than in a loop.
Submit NGN
Submit uses the same list envelope and adds one parent idempotency key:client_item_id is optional but recommended for correlating your records with response items.
Paying many bank accounts at once? Bulk withdrawals
covers the 50-beneficiary limit, per-transfer fees,
partial outcomes, and
how to reconcile each transfer.Submit USD
USD stablecoin settlement uses the same envelope, with a parentsettlement_rail and exactly one item in the current version:
USDT and USDC on an available chain. A USD quote validates the supplied chain, slug, and destination against the current environment and enabled route before it can return available. Cash USD settlement is not available through this API. An unsupported rail, a destination that does not match the rail, or an unsupported chain returns UNSUPPORTED_WITHDRAWAL_ROUTE; more than one USD item or a missing settlement_rail returns VALIDATION_FAILED.
The USD submit response differs from the NGN one in a few ways: rail is
crypto, settlement_rail is present, amount equals debit_amount with the
fee taken out of it (so estimated_receive is what the destination receives),
transfer_count is 1, max_transfer_amount is absent, and reference
equals id.
Response Shape
The submit response represents one parent transaction.items is always a list, including one-item NGN and USD withdrawals. NGN items can contain multiple transfers. The NGN fee is charged for every generated bank transfer, so fee is the ₦100 flat transfer fee multiplied by transfer_count.
pending and its transfers as processing. reference
is WD- followed by a full UUID; it is generated by Stabyl and is not derived
from id, so store it rather than reconstructing it.
Do not infer completion from the initial response. Persist data.id and wait for
the corresponding withdrawal.success, withdrawal.partial, or
withdrawal.failed webhook. A parent moves from pending to processing
and can remain there while item transfers settle independently; the terminal
statuses are success, partial, failed, and cancelled. withdrawal.partial applies only to NGN parents
with more than one bank transfer, including a single beneficiary whose amount
was split above the per-transfer limit. Use
GET /partner/wallets/transactions/{id} for on-demand confirmation, webhook-gap
recovery, and periodic reconciliation rather than continuous polling.
Safe Retries
For an uncertain HTTP response:- Keep the same body and
idempotency_key. - Generate a new timestamp and nonce.
- Sign the new canonical payload.
- Retry submit, then read the returned parent transaction.
IDEMPOTENCY_CONFLICT. Amounts are compared as
decimals, so 200000000.00 and 200000000 are the same intent.
Errors
Never automatically retry a
400, 401, 402, 403, 409, or 422; correct the request, funding, or key configuration first. Retry 429 and 503 with bounded backoff and a new nonce. Requests need Content-Type: application/json. A body with an unknown field, or one that does not match the request schema, is rejected before the request is processed with a plain-text 422 and no error envelope, so branch on the status code for that case.