Live P2P API — implemented profile
Status: implementation 0092 — validation and deployment pending
This page describes implemented code. It does not confirm availability on your production deployment. Have your provider confirm the deployed version and Merchant configuration before sending real payments. Sections marked Target contract describe a broader scope that this profile does not fully enable.
Use https://api.<White Label domain> with a Live key and the existing HMAC authentication (PG-Version: 2026-08-01). The hostname and credential determine the environment; request fields or headers cannot switch Live/Test mode.
Operations in this profile
| Method and path | Key scope | Result |
|---|---|---|
POST /v1/pay/withdrawals | withdrawals:write | Create a P2P payout and reserve funds, pending two approvals |
GET /v1/pay/withdrawals/{id} | withdrawals:read | Full current withdrawal state and amounts |
GET /v1/pay/withdrawals | withdrawals:read | This Merchant's withdrawal list |
GET /v1/pay/deposits | deposits:read | Deposit summaries without destination accounts, QR or payment instructions |
GET /v1/pay/balance | wallet:read | Available, pending clearing and reserved amounts |
GET /v1/pay/events | wallet:read | Continue reading business events by sequence |
Existing deposit creation and individual reads retain their established flow. This profile does not add a Customer API, beneficiary registration API, inline beneficiary, Merchant quote, or withdrawal cancellation command.
Prepare the key and beneficiary
New Live keys may explicitly select deposits:read, deposits:write, withdrawals:read, withdrawals:write and wallet:read as needed. Existing keys are not expanded automatically. Rotation preserves the old key's actual approved scopes. sandbox:write is Test-only; see the supported sandbox API.
Have your provider prepare an existing beneficiary owned by the same Merchant in Live mode. It must be verified, accept new orders, and satisfy current account and receiving-bank eligibility checks. Its public ID is ben_ followed by the UUID with hyphens removed: exactly 32 lowercase hexadecimal characters, for example ben_92000000000040008000000000000423. Do not send a raw UUID, bank account number, or inline beneficiary object. See destination accounts and create a withdrawal.
Create a withdrawal
Example body for a signed request:
{
"amount_minor": 100000,
"currency": "THB",
"merchant_reference": "WD-20261017-0001",
"end_user_ref": "customer-001",
"beneficiary_id": "ben_92000000000040008000000000000423",
"metadata": { "order": "WD-20261017-0001" }
}amount_minoris a positive safe integer in satang, at most9007199254740991, and must meet the current P2P minimum. The example amount must still satisfy that policy.merchant_referenceandend_user_refare 1–64 characters without control characters. The reference is unique per Merchant.end_user_refis your reference, not a Customer creation request.metadataaccepts at most 20 keys, key lengths 1–40, and string values up to 500 characters. Optionalclientaccepts only validateddevice_fingerprint,ipanduser_agenthints; those hints are not stored in payment metadata or returned in receipts.- Retry with the same 8–200 character
Idempotency-Keyand exact body. Generate a fresh nonce and signature on every attempt. An exact replay returns the original receipt withIdempotent-Replay: true.
A 201 response starts at status: "pending_approval", requires_approval: true, version: 0. It immediately reserves total_locked_minor = amount_total_minor + fee_total_minor. The established two-employee approval flow must finish before matching begins. Creation is not a completed bank transfer. This is separate from Agent commission withdrawals.
The Merchant's Payout sale matrix for all four sources is pinned at creation. This P2P profile uses the wl_pool fee basis. Read fee_total_minor and total_locked_minor from the response; do not assume the reserved amount equals principal alone. 402 insufficient_balance means available funds cannot cover principal plus fee and no order is created. An ineligible beneficiary returns 409 beneficiary_not_verified; an amount below policy returns 422 amount_below_p2p_minimum.
Read orders and balances
Withdrawal IDs are wdr_ plus 32 lowercase UUID hexadecimal characters. Use GET /v1/pay/withdrawals/{id} for the full current state. Public statuses are pending_approval, open, partially_filled, settled, expired, canceled and rejected.
Principal is split into four counters, with A = amount_total_minor: A = filled_minor + reserved_minor + remaining_minor + reversed_minor. reserved_minor is allocated to a payer whose transfer has not yet been confirmed. An open order can therefore have remaining_minor: 0, reserved_minor: A, and filled_minor: 0. Zero remaining does not mean paid. Check the current status and confirmed filled amount. A fully paid order without reversals is settled, has filled_minor = A, and has reserved_minor = remaining_minor = reversed_minor = 0.
Deposit and withdrawal lists accept limit 1–100 (default 20), status, a UTC updated_after such as 2026-10-17T00:00:00Z, and the returned next_cursor as cursor. Cursors have the form cur_<epochmicro>_<uuidhex>; return them unchanged. Results sort by descending created_at, using ID to break ties. Start a new first page when changing filters. has_more: false returns next_cursor: null. Pagination reads current data; it does not freeze order status across pages.
The balance response keeps pending clearing separate from withdrawable funds:
{
"livemode": true,
"available_minor": 250000,
"pending_clearing_minor": 10000,
"reserved_minor": 5000,
"currency": "THB",
"as_of": "2026-10-17T00:00:00.000Z"
}Poll events without changing the webhook contract
Call GET /v1/pay/events?since_sequence=0&limit=20. Persist next_sequence as a decimal string and pass it back as since_sequence to continue. Each event's sequence is also a string and increases per Merchant.
This profile includes only the business snapshots for withdrawal.created, withdrawal.partial_fill, withdrawal.terminal and deposit.settled. data.object retains that event's existing business snapshot; it is not a full Withdrawal object. GET the withdrawal for current state and amounts. When a snapshot contains a UUID withdrawal_id, construct its public ID as wdr_ plus the UUID without hyphens.
The signed webhook envelope retains its seven existing fields: id, object, type, api_version, livemode, created_at, data. sequence and account are added only as polling response metadata. Verify Ed25519 against the original raw webhook bytes as described in signature verification; reconstructed polling JSON is not the signed webhook body.
SQL validation, deployment and provider host/key/policy configuration remain required before enabling real payments.
