Withdrawals
AbstraPay supports two approval modes for withdrawals. Your operator is configured for one of them by the AbstraPay team.
| Mode | Who approves | How |
|---|---|---|
| Synchronous (default) | You, in real time + an admin in the panel | You approve/deny inline on the WITHDRAW_REQUEST webhook, then finalize in the management panel |
| External approval (async) | Your backend, via a callback | We notify you asynchronously; your backend calls us back APPROVE/REJECT and we settle automatically |
Both modes end the same way: on approval AbstraPay pays out on-chain and sends a WITHDRAW_COMPLETE webhook so you debit the user.
Mode A — Synchronous (default)
A single withdrawal passes through your system twice:
- Auto-validation — the moment a user submits a withdrawal, AbstraPay calls your
WITHDRAW_REQUESTwebhook synchronously (before anything is created) so your backend can approve/deny based on the user's balance. A denial stops it immediately. - Manual approval — if your backend approved it, the request lands in your management panel at
PENDINGfor an admin to finalize.
Flow
user requests withdrawal
→ WITHDRAW_REQUEST webhook (you validate balance/limits — approve or deny in real time)
→ status PENDING, shown in your management panel
→ you start processing (PATCH status: PENDING → PROCESSING)
→ you finalize (PATCH status: PROCESSING → COMPLETED / REJECTED)
→ on COMPLETED: on-chain payout, then WITHDRAW_COMPLETE webhook → you debit the user1. Request — initiated from the widget/session (POST /withdraw/direct-transfer with sid, destination address, cryptoCurrencyCode, fiatCurrencyCode, chainId, and an amount as either cryptoAmount or fiatAmount).
2. WITHDRAW_REQUEST webhook (synchronous gate) — AbstraPay calls you and waits. Validate the user's balance/limits and respond:
- Approve →
{ "error": 0, "description": "ok" } - Deny →
{ "error": <code>, "description": "..." }(see error codes below)
A denial stops the withdrawal immediately.
3. Manual approval (management panel) — approved requests sit at PENDING. Your admin reviews and finalizes them in the AbstraPay management panel (see Management Panel) — no integration work needed on your side. The finalize is a two-step state machine: a request must first move PENDING → PROCESSING, then PROCESSING → COMPLETED (or REJECTED). You cannot jump straight from PENDING to COMPLETED. Under the hood the panel makes two calls:
# Step 1 — claim the request for processing
curl -X PATCH https://api.ensopay.io/admin/withdrawals/<id>/status \
-H "Authorization: Bearer <ADMIN_TOKEN>" -H "Content-Type: application/json" \
-d '{ "status": "PROCESSING", "adminNote": "reviewing" }'
# Step 2 — finalize (COMPLETED or REJECTED)
curl -X PATCH https://api.ensopay.io/admin/withdrawals/<id>/status \
-H "Authorization: Bearer <ADMIN_TOKEN>" -H "Content-Type: application/json" \
-d '{ "status": "COMPLETED", "adminNote": "approved" }'status ∈ PROCESSING · COMPLETED · REJECTED. Note: <ADMIN_TOKEN> is a panel admin login token, not your OPERATOR_SECRET_KEY — these admin endpoints use a separate dashboard login.
4. Payout — on COMPLETED, AbstraPay transfers the funds on-chain to the recipient, then sends a WITHDRAW_COMPLETE webhook → debit the user.
Mode B — External approval (async callback)
In this mode you approve withdrawals from your own backend, on your own schedule, and AbstraPay settles automatically on your APPROVE — no admin panel step required.
External approval is enabled per operator by the AbstraPay team. You cannot self-enable it. It requires your webhook URL + secret to be configured (that's where the notification is delivered), and AbstraPay sets your approval caps (see below). Ask us to turn it on for your operator.
What each side does
| You give us | A webhook endpoint (already configured) to receive the notification |
| You send us | An APPROVE / REJECT decision, by calling POST /withdraw/{id}/decision |
| You get from us | The async WITHDRAW_REQUEST notification, then — on APPROVE — the on-chain payout + a WITHDRAW_COMPLETE webhook |
Flow
user requests withdrawal
→ status AWAITING_APPROVAL, shown in your management panel
→ WITHDRAW_REQUEST webhook (ASYNC + retried — just acknowledge with {error:0};
the real decision comes from your callback, not this response)
→ your backend decides, whenever ready:
POST /withdraw/{withdrawRequestId}/decision { "decision": "APPROVE" | "REJECT" }
→ APPROVE → on-chain payout → WITHDRAW_COMPLETE webhook → you debit the user
→ REJECT → status REJECTED (no payout, no webhook)1. You receive the WITHDRAW_REQUEST notification (async)
Same event and payload as Mode A, with two differences:
- It is asynchronous and retried (delivered via our reliable outbox), so your endpoint just needs to acknowledge it with
{ "error": 0, "description": "received" }. Your response is not the decision. withdrawRequestIdis the real withdrawal id (a UUID) — use it as the handle for your callback. (In Mode A this field is empty.)
2. You send your decision
curl -X POST https://api.ensopay.io/withdraw/{withdrawRequestId}/decision \
-H "Authorization: Bearer <YOUR_OPERATOR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{ "decision": "APPROVE" }'
# reject with an optional note:
# -d '{ "decision": "REJECT", "reason": "failed internal review" }'Auth here is your operator API key (the same Bearer key you use for POST /session/create) — not the dashboard admin token used by the /admin/* endpoints. You may only decide on withdrawals that belong to your own operator.
Response:
{
"status": "success",
"data": {
"withdrawRequestId": "3f2b…",
"withdrawStatus": "COMPLETED",
"settlementTxHash": "0x…",
"held": false,
"heldReason": null
}
}withdrawStatus tells you the outcome:
COMPLETED— settled on-chain (settlementTxHashis set); aWITHDRAW_COMPLETEwebhook follows.REJECTED— rejected; no payout, no webhook.AWAITING_APPROVAL— held, not settled (see caps below). Retry later once conditions clear, or an AbstraPay admin resolves it.
When an APPROVE couldn't auto-settle, held is true and heldReason gives a stable code so you don't have to guess:
heldReason | Meaning |
|---|---|
CAP_EXCEEDED | Above your per-transaction or rolling-24h daily cap. |
APPROVAL_NOT_ENABLED | Your operator isn't enabled to release withdrawals. |
OPERATOR_INACTIVE | Your operator account is inactive. |
EXPIRED | The request passed its TTL before you decided. |
TEMPORARILY_UNAVAILABLE | Can't settle right now (e.g. transient liquidity) — safe to retry. |
Approval caps
AbstraPay bounds how much you can release, in your platform fiat:
- Per-transaction cap — a single withdrawal above this is not auto-settled.
- Daily cap — a rolling 24-hour cumulative limit across your settled withdrawals.
If an APPROVE would exceed either cap (or the bankroll is momentarily short), the request is held at AWAITING_APPROVAL rather than paid out — your callback response returns withdrawStatus: "AWAITING_APPROVAL". It then waits for an AbstraPay admin, or for you to retry once you're back within the caps.
Idempotency & overrides
- Idempotent — safe to retry the callback. A decision on an already-decided withdrawal simply returns its current status; a payout is never sent twice.
- Panel visibility — these withdrawals also appear in your management panel, where an AbstraPay admin can override (approve/reject) if needed.
Statuses
- Mode A:
PENDING→PROCESSING→COMPLETED· orREJECTED. - Mode B:
AWAITING_APPROVAL→COMPLETED· orREJECTED. - Both:
CANCELLED(superseded by a newer request / session cancelled) andEXPIRED(left unactioned past its 24h TTL).
WITHDRAW_REQUEST response codes
Used in Mode A to approve/deny inline. In Mode B, respond 0 to acknowledge (the decision is your callback).
| code | meaning |
|---|---|
| 0 | approve |
| 1 | declined by operator |
| 2 | insufficient balance |
| 3 | temporarily unavailable |
| 4 | user not eligible |
| 5 | daily limit exceeded |
| 6 | amount too low |
| 7 | amount too high |
| 8 | verification required |
| 9 | account suspended |