APIs integration

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, and created_at cannot change. Wrong total? Create a new intent.
  • The intent_details snapshot binds the launch: /pa/checkout revalidates the snapshot against the current approved catalog and rejects drifted prices or unapproved products.
  • One signed launch per intent. Identical /pa/checkout retries return the stored response; conflicting retries and terminal intents return 409. 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, and sign.
  • Forward the payload unmodified to POST /paymentasia/record_payment. Don't filter, rename, or "clean up" fields — the signature covers them.
  • Respond 200 to 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_payment doesn't return ok: 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 to record_payment as 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 Orderstatus, currency, subtotal_amount, total_amount, customer/store scope;
  • Order items — one per snapshot line, with checkout-time unit_amount and amount;
  • a Paymentstatus: "S", amount, currency, and the gateway reference.

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.