Payments guide
This page goes deeper than the quickstart: the checkout lifecycle, the trust model, callback handling, and how to reconcile edge cases.
The lifecycle of a checkout
POST /intent POST /pa/checkout customer pays
your DB: cart ───► intent C ─────────► signed launch form ───► gateway result
│ (one per intent) │
│ ▼
│ notify_url / return_url on your site
│ │
│ POST /paymentasia/record_payment (forward payload)
│ │
▼ signature verified by Ping Business
status S / F / U ◄────────── order + order items + payment created (S)
Intent statuses
| Status | Name | Terminal | Notes |
|---|---|---|---|
C |
Created | No | Intent exists; launch may or may not have been requested. |
R |
Redirected | No | Customer was sent to the gateway. Can persist if they abandon the page. |
S |
Succeeded | Yes | Verified payment recorded; order exists. The only status that means money. |
F |
Failed | Yes | Gateway reported a verified failure. |
U |
Unknown | Yes (for UI) | Result couldn't be confirmed; reconcile before fulfilling. |
You may move an intent's status and details forward yourself (e.g. mark R when you render the launch form) via POST /intent update mode — but you can never set S. Only the verified payment flow can. Attempts return 403.
Immutability rules
- After creation, an intent's
identifier,merchant_id,store_id,customer_id,order_id,currency,amount, andcreated_atcannot change. Wrong total? Create a new intent. - The
intent_detailssnapshot binds the launch:/pa/checkoutrevalidates the snapshot against the current approved catalog and rejects drifted prices or unapproved products. - One signed launch per intent. Identical
/pa/checkoutretries return the stored response; conflicting retries and terminal intents return409. To retry a failed payment, create a new intent with a new merchant reference.
Payment networks
Read your enabled networks from your merchant record and offer exactly those — the API rejects disabled networks with 403:
CreditCard · Alipay · Wechat · CUP · Fps · Octopus · PayMe
Octopus: the total must be an exact multiple of HKD 0.10. Incompatible totals are rejected with 400 before anything is signed — either round your pricing or hide Octopus for incompatible carts.
Callback handling in depth
notify_url — the server-to-server truth
- Must be a public HTTPS endpoint on your backend.
- The gateway POSTs form-encoded (sometimes JSON) fields including
merchant_reference,amount,currency,status,request_reference, andsign. - Forward the payload unmodified to
POST /paymentasia/record_payment. Don't filter, rename, or "clean up" fields — the signature covers them. - Respond
200to the gateway once the forward succeeds. If your forward fails transiently, let the gateway retry (it will) — recording is idempotent. - Never mark anything paid based on your own parsing of the payload. If
record_paymentdoesn't returnok: true, it isn't paid.
return_url — the customer's browser
- Purely navigational. Render a "checking your payment…" page that polls your backend, which reads
GET /intents?identifier=.... - The browser often arrives before the notify callback lands. Poll with a bounded interval (e.g. every 3–5 s for up to 5 minutes) instead of concluding failure.
- If callback-looking fields (including
sign) are present on the return hit, you may forward them torecord_paymentas a fast path — verification and idempotency make this safe.
Verification failures
record_payment rejects, among others: invalid signature (400), reference/amount/currency mismatch with the intent (400), unknown intent (404). A rejection means the payload doesn't prove payment for that intent — investigate before fulfilling anything.
Reconciliation
Handle these cases explicitly in your integration:
| Situation | What you see | What to do |
|---|---|---|
| Customer abandons on the gateway page | Intent stuck in R/C |
Expire the checkout in your UI after your polling window; create a fresh intent if they return. Optionally set expires_at on the intent. |
| Notify arrives late | R for minutes, then S |
Keep the poll window generous; fulfill whenever S appears. |
| Duplicate notifies | Multiple identical callbacks | Forward them all; replays return "idempotent": true. |
| Your notify endpoint was down | No callback received | The gateway retries. As a backstop, poll pending intents (see below) and reconcile stale ones. |
Intent shows U |
Unknown terminal state | Don't fulfill. Surface to your ops team / Ping Business support with the checkout_reference. |
A simple daily reconciliation job: GET /intents?status=R (and C) with created_from/created_to bounds, and alert on anything older than your checkout window.
After success: orders, order items, payments
On verified success Ping Business creates, atomically:
- an Order —
status,currency,subtotal_amount,total_amount, customer/store scope; - Order items — one per snapshot line, with checkout-time
unit_amountandamount; - a Payment —
status: "S",amount,currency, and the gatewayreference.
All three are read-only through the API (GET /orders, GET /order_items, GET /payments). Use the payment's reference when you need to talk to support or match gateway settlement reports.
Money amounts everywhere are fixed-point decimal strings ("300.00"); currencies are three uppercase letters. Timestamps are returned as server-formatted date/time strings — parse them, don't string-match.