· API Docs

Deposit (Payin) — V2

AUTO

Payin V2 (AUTO) creates an incoming payment without forcing you to choose a channel up front. The gateway selects QR or bank transfer automatically by provider weight at request time — so you get the best available route with a single call.

What's new vs V1

  • receiving_type is not a request field — by default the gateway auto-selects the channel (AUTO). Optionally send channel (bank, qrcode, or crypto) to force a specific rail.
  • New payment_theme field — themes the hosted Pay Page (e.g. "aurora"); blank or unknown falls back to "classic".
  • The gateway may route to QR or bank transfer automatically; the channel it picked is returned as receiving_type in the webhook callback.

Typical steps:

  1. Create a payin with POST /v2/payins (omit channel and the gateway auto-selects; or send channel to force bank/qrcode/crypto)
  2. Open the returned paypage_url (hosted Pay Page) or send the link to the payer to complete the payment
  3. Wait for the webhook callback when the payin reaches a final status; GET /v1/payins/{id} is available if you want to check on demand
  4. To attach a slip, do it through the Pay Page first; if that runs into trouble, fall back to POST /v1/payins/{id}/slip (multipart field "slip": jpg/jpeg/png/jfif, max 10MB; up to 10 uploads per payin shared across both channels)
Merchant BackendG2Wallet APIPayer1POST /v2/payins{request_amount, merchant_ref_id}(no receiving_type → AUTO)2AUTO selects channelby provider weight3201 Created{id, paypage_url, status: pending}4Show QR / details — or send Pay Page linkScan & pay56Webhook callback{status: success | failed |rejected | refunded | expired}opt[Check on demand]GET /v1/payins/{id}7current status — see the status table8opt[Slip needed]POST /v1/payins/{id}/slip (multipart)9200 OK10
Payin V2 (AUTO) flow: the gateway auto-selects the channel at runtime

Create a payin (AUTO)

By default you omit channel and the gateway auto-selects the rail (QR or bank transfer). Optionally send channel — bank, qrcode, or crypto — to force a specific rail; crypto also takes a network. There is still no receiving_type in the request. Add the optional payment_theme to control the Pay Page look.

HTTP
POST /v2/payins
Content-Type: application/json

{
  "merchant_ref_id": "unique-order-ref-id-2025001",
  "customer_id": "0198cbc6-fd02-7459-a671-78c306d0a1ad",
  "customer_account_id": "0198cbc7-35ec-7c8c-8994-d3ac3e7f5255",
  "currency_code": "THB",
  "request_amount": 1000,
  "payment_theme": "aurora",
  "notes": "Payin for invoice #INV2025001",
  "redirect_url": "https://merchant.example.com/payment/success"
}

Request fields

FieldTypeRequiredDescription
merchant_ref_idstring · max 255RequiredYour own reference for this order/invoice. Must be unique within your organization and is never freed — even after the payin fails, expires, or is cancelled. Sending an already-used value is rejected with 409 RESOURCE_EXISTS; when a retry hits this, look the original up with GET /v1/payins?merchant_ref_id=... instead of creating a new reference.
customer_idstring · UUIDRequiredUUID of an existing customer (create one first via the Customers API).
customer_account_idstring · UUIDRequiredUUID of the customer account that will receive the payment (belongs to customer_id).
currency_codestringRequiredISO 4217 currency code, e.g. THB.
request_amountnumber · integer, aligned to currency unit_stepRequiredAmount to receive as integer minor units aligned to the currency unit_step. Per-transaction min/max depend on your organization: check GET /v1/me/limits — min_deposit / max_deposit are the effective limits (configured_* are the raw organization-level bounds).
payment_themestring · max 32OptionalTheme key for the hosted Pay Page, e.g. "aurora". Blank or unknown falls back to "classic". — Pay Page
notesstring · max 255OptionalFree-text note attached to the payin.
redirect_urlstring · max 2048OptionalHTTPS URL (max 2048 chars) the Pay Page auto-redirects the payer to (5-second countdown) when the payin ends as success or expired — other outcomes do not redirect, and no query params are appended (no id/status), so embed your own reference in the URL.
channelstring · enumOptionalForce the receiving rail: bank (the default when omitted — the gateway auto-selects), qrcode, or crypto. Omit it and the gateway auto-selects QR/bank transfer (AUTO).
networkstring · enumOptionalCrypto network when channel = crypto (for example TRC20); omit to use the provider default network. Ignored for other channels.

Response

On success the API returns 201 Created with the payin wrapped in the standard SuccessResponse envelope. status is "pending". Unlike V1, V2 does not return any receiving_* payment details — open the returned paypage_url to let the payer complete the payment (hosted Pay Page); the picked channel arrives later as receiving_type in the webhook callback. transfer_amount is the exact amount the payer must send (request_amount plus at most 0.99, per allocation); the payin auto-expires at expired_at if unpaid. fee_amount / net_amount at creation time are estimates — the final values arrive with the callback.

Success (201 Created)

JSON
HTTP/1.1 201 Created
Content-Type: application/json

{
  "success": true,
  "data": {
    "id": "0198cbff-db9b-7b6a-9f82-6a55fdfb6575",
    "merchant_ref_id": "unique-order-ref-id-2025001",
    "currency_code": "THB",
    "customer_id": "0198cbc6-fd02-7459-a671-78c306d0a1ad",
    "customer_name": "John Doe",
    "customer_account_id": "0198cbc7-35ec-7c8c-8994-d3ac3e7f5255",
    "customer_account_holder_name": "J. Doe",
    "notes": "Payin for invoice #INV2025001",
    "redirect_url": "https://merchant.example.com/payment/success",
    "request_amount": 1000,
    "transfer_amount": 1000.01,
    "paid_amount": 0,
    "net_amount": 995.00,
    "fee_amount": 5.00,
    "status": "pending",
    "payment_theme": "aurora",
    "paypage_url": "https://pay.example.com/pay/0198cbff-db9b-7b6a-9f82-6a55fdfb6575",
    "finalized_at": null,
    "expired_at": 1755769966,
    "created_at": 1755769366,
    "updated_at": 1755769366
  }
}
FieldDescription
idPayin id — use it to look up the payin via GET /v1/payins/{id} when needed
statusAlways "pending" right after creation
paypage_urlHosted Pay Page URL — send the payer here to complete payment
transfer_amountExact amount the payer must transfer — request_amount plus a fraction of at most 0.99 added by allocation (the extra is a fee charged to the customer)
expired_atUnix seconds when the payin expires if left unpaid — set to roughly 10 minutes after the payin enters processing (system-wide default; not configurable per request or per organization)

Error (4xx / 5xx)

On failure the API returns a non-2xx status with the standard envelope: success is false, data is null, error is human prose, and code is the machine-readable identifier your client should branch on. Use the X-Request-Id response header when contacting support. Example below: 400 Bad Request for an invalid amount.

JSON
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "success": false,
  "data": null,
  "error": "amount must be greater than zero",
  "code": "AMOUNT_INVALID",
  "meta": {
    "request_id": "019f2d24-1a02-7b31-9c44-2f61c0d8a9e1",
    "timestamp": 1783168800
  }
}

How AUTO channel selection works

When you omit channel, G2Wallet evaluates the available providers at request time and assigns the channel automatically (QR / bank transfer). Send an explicit channel (bank/qrcode/crypto) to force a rail instead. Either way the resolved channel is returned as receiving_type in the webhook callback, so your reconciliation logic stays the same.

Webhook callback

Identical to V1. When a payin reaches a terminal status (success / failed / refunded / expired), G2Wallet POSTs the full transaction body to your configured webhook URL. Verify the signature, then update your internal state.

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.

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: 1737029438
X-Signature: 3f7a1c9e5b2d8f04a6c1e7b9d3a5f2c8e0b4d6a8c2f4e6b8d0a2c4e6f8a0b2c4
X-Request-ID: cb_1737029438123456789_123456
X-Transaction-ID: 019db53f-b389-7149-8806-4e64f3be45a8
X-Organization-ID: 0198cbc5-8a3d-7e21-b4f6-9c2d1e0f3a5b
X-Transaction-Type: payin

Request body

JSON
{
  "id": "019db53f-b389-7149-8806-4e64f3be45a8",
  "transaction_type": "payin",
  "status": "success",
  "merchant_ref_id": "ORDER-2026-00081",
  "currency_code": "THB",
  "request_amount": 1000,
  "paid_amount": 1000,
  "transfer_amount": 1000,
  "fee_amount": 25,
  "net_amount": 975,
  "receiving_type": "BANK_TRANSFER",
  "receiving_platform": "KBANK",
  "receiving_identifier": "0800000000",
  "receiving_holder_name": "Sample S.",
  "payment_theme": "aurora",
  "customer_id": "0198cbc6-fd02-7459-a671-78c306d0a1ad",
  "customer_account_id": "0198cbc7-35ec-7c8c-8994-d3ac3e7f5255",
  "customer_name": "John Doe",
  "customer_account_holder_name": "John Doe",
  "created_at": 1737029400,
  "expired_at": 1737030000,
  "updated_at": 1737029438,
  "finalized_at": 1737029438,
  "notes": null
}

Key fields to use

FieldDescription
idPayin id — use it to query /v1/payins/{id}
statuspending | processing | success | failed | rejected | refunded | expired | cancelled (lowercase) — only "success" means funds received; see the Payin statuses table below
merchant_ref_idYour reference id — match to your order/invoice
receiving_typeThe channel the gateway actually selected (QR / BANK_TRANSFER ...)
payment_themeThe Pay Page theme that was applied, if you sent one
paid_amountActual amount the payer sent
transfer_amountThe actual amount the customer needs to transfer — equal to request_amount or with at most 0.99 added by allocation. The extra is a fee charged directly to the customer (separate from the Merchant fee)
fee_amountMerchant fee per the fee setting of your organization, calculated from request_amount — deducted on success before the wallet credit (net_amount = paid − fee)
net_amountAmount credited to your wallet (paid − fee)
finalized_atUnix seconds when the transaction was settled
transaction_typeAlways "payin" for this event

Payin statuses

Every status a payin can be in — across the create response, GET /v1/payins/{id}, list filters, and webhook callbacks. Only "success" means funds were received.

StatusMeaningFinal?Callback?
pendingThe payin is being created — data is still being validated. A short-lived state right after POST.No — moves on to processing—
processingCreated successfully and ready for the customer to pay. Covers the whole active window: waiting for the transfer, and verification after the customer pays or uploads a slip.No—
successFunds received — credit your customer now.Almost — can still flip to rejected → refunded if the payment is later refunded✓
failedThe payin failed.Yes✓
rejectedAwaiting refund — an in-between state on the way to refunded.No — becomes refunded✓
refundedThe money was returned to the payer.Yes✓
expiredReached expired_at without a confirmed payment.Almost — can still flip to success if a verification already in progress completes later✓
cancelledCancelled via POST /v2/payins/{id}/cancel.Yes✕ — no callback; the cancel API response tells you the result

Full request/response details are in the API Reference → Payins