Withdrawal (Payout)
A payout transfers funds out to a destination account. Before paying, check the balance via GET /v1/balance.
General flow
- (First time) Create a customer + customer account — see Manage Customers
- Create a payout with POST /v1/payouts
- Wait for the webhook callback when the payout is finalized; GET /v1/payouts/{id} is available if you want to check on demand
Create a payout
POST /v1/payouts
Content-Type: application/json
{
"request_amount": 500,
"currency_code": "THB",
"customer_id": "0198cbc6-fd02-7459-a671-78c306d0a1ad",
"customer_account_id": "0198cbc7-35ec-7c8c-8994-d3ac3e7f5255",
"merchant_ref_id": "REFUND-0081",
"sender_type": "BANK_TRANSFER",
"notes": "Refund for order 81"
}Things to watch out for
- Daily payout total may be capped by the limits your organization holds — fetch from GET /v1/me/limits first
- merchant_ref_id must be unique — a duplicate create is rejected (400 while the original is still pending, 409 otherwise), so a retry can never double-spend; when rejected, check the original with GET /v1/payouts?merchant_ref_id=... instead of paying again under a new reference
- Handle the full status set (see the payout status table below) — the wallet reserves amount + fee the moment the payout is created, deducts on success and releases on failed
Reading GET /v1/me/limits
- min/max_deposit and min/max_withdraw are the EFFECTIVE limits (org config + provider constraints + maintenance already folded in) — exactly what the API enforces at create time. min = max = 0 means the service for that currency is under maintenance (creating a payout then returns 503 PAYOUT_UNDER_MAINTENANCE).
- configured_min/max_* are the raw bounds configured for your organization (before folding) — validate on your side with the effective min/max_* values.
- maintenance_ranges are AMOUNT ranges (not time windows) temporarily unavailable — an amount inside a range is rejected with 423 AMOUNT_RANGE_MAINTENANCE; an amount outside min–max gets 423 INVALID_AMOUNT_RANGE.
- Check before creating on both sides: *_deposit fields for payins, *_withdraw for payouts (filterable by currency_code / method_type).
- Your fee rates for deposits/withdrawals are shown in the Backoffice under "My Organization" — actual per-transaction charges arrive as fee_amount in responses/callbacks, and settlements can be previewed before committing.
Refund / Dispute
If something looks wrong, open an issue ticket with POST /v1/issues referencing the transaction id. Track progress with GET /v1/issues (filterable list) or GET /v1/issues/{request_id} (latest issue for that request). If it has not been picked up yet, you can cancel with PATCH /v1/issues/{request_id}/cancel
Webhook callback
When a payout reaches a final outcome (success / partial_success / failed / refunded), G2Wallet POSTs the full transaction to your webhook URL — rejected and expired do not send callbacks (track them via GET). Large transfers may be split into execution_steps — each step reports its own status.
How to verify the callback X-Signature is covered in Authentication → Verifying webhook callbacks.
Request headers
Every callback is delivered with the headers below (example values). Use X-Signature and X-Timestamp to verify authenticity, X-Transaction-ID to match the transaction (same as id in the body), and X-Request-ID to de-duplicate retried deliveries. X-Transaction-Type is "payout" for this event.
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
Accept: application/json, text/plain
User-Agent: P2PPAY-Callback/1.0
X-Callback-Version: 1.0
X-Timestamp: 1737029770
X-Signature: a1d3e5f7092b4d6c8e0f2a4b6c8d0e1f3a5b7c9d1e3f5a7b9c1d3e5f7a9b0c2d
X-Request-ID: cb_1737029770987654321_987654
X-Transaction-ID: 019db52e-cbf7-77ed-ac30-ca1531780d9c
X-Organization-ID: 0198cbc5-8a3d-7e21-b4f6-9c2d1e0f3a5b
X-Transaction-Type: payoutRequest body
{
"id": "019db52e-cbf7-77ed-ac30-ca1531780d9c",
"transaction_type": "payout",
"status": "success",
"merchant_ref_id": "PAYOUT-2026-00042",
"currency_code": "THB",
"request_amount": 1000,
"transfer_amount": 1000,
"fee_amount": 0,
"net_amount": 1000,
"sender_type": "BANK_TRANSFER",
"sender_platform": "",
"sender_identifier": "",
"sender_holder_name": null,
"slip_qr_ref": null,
"slip_qr_string": "",
"execution_steps": [
{
"id": "019db52e-cbf7-77ed-ac30-ca1531780d9c",
"step": 1,
"amount": 500,
"transfer_amount": 500,
"status": "success",
"created_at": 1737029400,
"finalized_at": 1737029770
},
{
"id": "019db533-e776-72d1-acb9-9844b18fbcac",
"step": 2,
"amount": 500,
"transfer_amount": 500,
"status": "success",
"created_at": 1737029735,
"finalized_at": 1737029769
}
],
"customer_id": "0198cbc6-fd02-7459-a671-78c306d0a1ad",
"customer_account_id": "0198cbc7-35ec-7c8c-8994-d3ac3e7f5255",
"customer_name": "Jane Doe",
"customer_account_holder_name": "Jane Doe",
"created_at": 1737029400,
"updated_at": 1737029770,
"finalized_at": 1737029770,
"expired_at": null,
"notes": null
}Key fields to use
| Field | Description |
|---|---|
id | Payout id — use it to query /v1/payouts/{id} |
status | See the payout status table below — funds leave the wallet only on "success" / "partial_success", and callbacks fire only for success | partial_success | failed | refunded |
merchant_ref_id | Your reference id — match to your order/refund |
request_amount | Amount you asked to pay out |
transfer_amount | Amount actually delivered to the customer — in the callback this is already aggregated across every Smart Withdraw leg (for partial_success it is the total that was paid). In GET /v1/payouts/{id} the same field covers only the first leg; use chain_transfer_amount there instead |
fee_amount | Fee deducted (may be 0) |
net_amount | Net debited from your wallet |
execution_steps | Breakdown when the payout is split across multiple transfers; each step has its own id, amount and status |
sender_type | Channel used to send funds (e.g. BANK_TRANSFER) |
finalized_at | Unix seconds when the payout was settled |
Payout statuses
Every status a payout can show via GET /v1/payouts and callbacks. Funds actually leave the wallet only on success / partial_success — and callbacks are sent only for the four statuses marked below.
| Status | Meaning | Final? | Callback? |
|---|---|---|---|
pending | The payout is being created/validated. The amount + fee are reserved from your available balance immediately. | No | — |
processing | Handed to the provider for execution. | No | — |
success | Paid in full — the reserved amount + fee were deducted. | Almost — the review team can still revert it (rejected → refunded, money reversed; you get a refunded callback) | ✓ |
partial_success | Smart Withdraw delivered only part of the amount — the undelivered remainder (and the fee difference) is auto-refunded to your available balance. | Yes — never retried; create a new payout for the remainder if needed | ✓ |
failed | The payout failed — the reserved amount was released back to your balance. | Almost — staff can push it through review and retry it; the final outcome arrives as a new callback | ✓ |
rejected | Rejected by the review team; funds not yet settled back (an in-between step before refunded). | No — becomes refunded | ✕ — visible via GET only |
refunded | Fully reversed/refunded (including reverting a payout that had already succeeded). | Yes | ✓ |
expired | Timed out before confirmation. | Almost — a late provider confirmation can still change the status to success (you then get a callback) | ✕ — the final-outcome callback is deferred until resolution |
pending_review | Waiting for the review team (also shown while the system holds a payout for correction). | No | — |
in_review | Being reviewed by staff — may proceed to a retry, success, failed, or rejected. | No | — |