· API Docs

Withdrawal (Payout)

A payout transfers funds out to a destination account. Before paying, check the balance via GET /v1/balance.

General flow

  1. (First time) Create a customer + customer account — see Manage Customers
  2. Create a payout with POST /v1/payouts
  3. Wait for the webhook callback when the payout is finalized; GET /v1/payouts/{id} is available if you want to check on demand
MerchantGET /v1/me/balancesBalance OK?NoStop · notify userYesPOST /v1/payoutsResponse201 Createdstatus: pendingWait forwebhook callbackGET /v1/payouts/{id}check on demandstatusfailedInspecterror.codeRetry?NoPOST /v1/issuesGET /v1/issues/{request_id}trackYessuccess✓ SentrefundedReturnedcheck balance
Payout flow: check balance → create payout → track status

Create a payout

HTTP
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.

HTTP
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: payout

Request body

JSON
{
  "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

FieldDescription
idPayout id — use it to query /v1/payouts/{id}
statusSee 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_idYour reference id — match to your order/refund
request_amountAmount you asked to pay out
transfer_amountAmount 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_amountFee deducted (may be 0)
net_amountNet debited from your wallet
execution_stepsBreakdown when the payout is split across multiple transfers; each step has its own id, amount and status
sender_typeChannel used to send funds (e.g. BANK_TRANSFER)
finalized_atUnix 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.

StatusMeaningFinal?Callback?
pendingThe payout is being created/validated. The amount + fee are reserved from your available balance immediately.No—
processingHanded to the provider for execution.No—
successPaid 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_successSmart 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✓
failedThe 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✓
rejectedRejected by the review team; funds not yet settled back (an in-between step before refunded).No — becomes refunded✕ — visible via GET only
refundedFully reversed/refunded (including reverting a payout that had already succeeded).Yes✓
expiredTimed 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_reviewWaiting for the review team (also shown while the system holds a payout for correction).No—
in_reviewBeing reviewed by staff — may proceed to a retry, success, failed, or rejected.No—