Custom checkout

Build your own e-commerce checkout UI from end to end using the Scan to Pay API. Maximum control, more work to maintain.

Custom checkout is the build-it-yourself e-commerce integration. You render the QR on your own page, you handle customer-facing state, you display success / failure. The customer never leaves your domain. You get total UI control in exchange for owning the entire checkout experience.

If you want fast time-to-live, use Bluebox hosted checkout instead — same underlying API, much less code.


When to choose custom

✅ Good fit❌ Probably overkill
Strong brand-UI requirements; mandatory in-house designStandard cart with no special UX needs
Multi-step checkout where Scan to Pay is one of several payment methodsSingle-payment-method checkout
Large merchant with dedicated front-end engineering capacitySmall merchant or PSP without ongoing UI maintenance budget
You want to keep the customer on your domain for analytics / re-engagementYou don't care about the brief Bluebox redirect
You're rendering the QR on a touchpoint Bluebox can't (e.g. embedded SDK in a desktop POS)Pure web checkout

The flow

   Customer cart            Your backend          Scan to Pay              Scan to Pay app
   ─────────────            ────────────          ───────────              ──────────────
        │                          │                    │                          │
        │ "Pay with Scan to Pay"   │                    │                          │
        ├─────────────────────────►│                    │                          │
        │                          │   POST /code/create                           │
        │                          ├───────────────────►│                          │
        │                          │                    │                          │
        │                          │◄──── code ─────────┤                          │
        │                          │                                               │
        │◄─── QR rendered on your page ─────────────────│                          │
        │     (status polling starts)                                              │
        │                                                                          │
        │                                              [customer scans QR]         │
        │                                                                          │
        │                                              [authorises in app] ───────►│
        │                                                                          │
        │                          │   webhook (CB)     │                          │
        │                          │◄───────────────────┤                          │
        │                          │   ack 200          │                          │
        │                          ├───────────────────►│                          │
        │                          │                                               │
        │◄─── status update (poll or push) ─────────────│                          │
        │     show success / failure                                               │

Two notable differences from Bluebox:

  1. You render the QR on your own checkout page.
  2. Only one callback — the encrypted webhook to your notification URL. (Or you poll queryRef if you can't expose a webhook endpoint.)

Step 1 — Create the code

Use the standard code-create endpoint (same as in-store / face-to-face flows):

curl -X POST "https://qa.scantopay.io/pluto/code/create" \
  -u "$USERNAME:$PASSWORD" \
  -H 'Content-Type: application/json' \
  -d '{
    "merchantReference": "order-2025-05-14-00081",
    "amount": 149.50,
    "useOnce": true,
    "shortDescription": "1x cappuccino, 1x muffin",
    "expiryDate": 1747235520000
  }'

Response:

{
  "code": "0123456789",
  "expiryDate": 1747235520000,
  "codeUrl": null
}

Field reference is on Dynamic QR — the e-commerce custom checkout is essentially a dynamic QR rendered on a web page. The same fields apply.

📘

Use a short expiry for online checkout. Customers either pay within 1–2 minutes or abandon. Set expiryDate to ~2 minutes from now to keep the dashboard clean. The default 30 minutes is too long for an online flow.


Step 2 — Render the QR

You have two options.

Option A — use the Scan to Pay QR endpoint. Embed an <img> pointing at the styled PNG endpoint:

<img
  src="https://qa.scantopay.io/pluto/public/stpqr/0123456789"
  alt="Scan to Pay"
  width="280"
  height="280" />

Option B — render the QR yourself. Encode the 10-digit code as a QR client-side. See Rendering the QR for Node, Python, and Java samples.

Either way, also show the customer:

  • A brief instruction line ("Scan with your Scan to Pay-enabled banking app")
  • The amount, prominently
  • A cancel / back button (which should call DELETE /code/{code} — see Managing QR codes)
📘

Mobile customers. If your checkout page is rendered on a mobile device, the customer can't easily scan a QR on the same screen they're paying from. Detect mobile and replace the QR with an "Open in Scan to Pay app" deep link — see App-to-app.


Step 3 — Wait for the outcome

Two patterns. Pick one, not both — the platform enforces a hard either/or per merchant.

Pattern A — webhook (recommended)

Configure a notification URL on the merchant in the Portal. Scan to Pay POSTs the encrypted webhook to it as soon as the customer finishes. See Webhooks.

On the front-end, poll your own backend (not the platform) for the order's status every 1–2 seconds while the customer is on the QR page. When your backend has received the webhook and updated the order, the next poll picks up the result. Display success or failure accordingly.

Pattern B — queryRef polling

If you can't expose a public HTTPS endpoint, poll the platform directly:

curl -X POST "https://qa.scantopay.io/pluto/code/queryRef" \
  -u "$USERNAME:$PASSWORD" \
  -H 'Content-Type: application/json' \
  -d '{
    "code": "0123456789",
    "merchantReference": "order-2025-05-14-00081"
  }'

Poll no faster than every 5 seconds. Stop polling when you reach a terminal status (SUCCESS or any END_* value). See Querying transactions.

⚠️

Polling is mutually exclusive with webhooks. queryRef returns HTTP 406 Not Acceptable if your merchant has a notification URL configured. Pick one and stick with it.


Step 4 — Render the outcome

Once you know the result:

  • SUCCESS — show a confirmation page with the transaction details. Capture the transactionId, bankResponse.retrievalReferenceNumber, and bankResponse.authCode from the webhook for your records.
  • BANK_REJECTED / CLIENT_TIMEOUT / CLIENT_CANCEL / other failures — show a friendly failure message and let the customer retry with a different card or method. Map the raw status to customer-friendly copy — see ISO response codes.

For the full list of states you might see, refer to Transaction states.


Mobile checkout

For mobile-web customers, instead of showing a QR (which they can't scan from the same device), use App-to-app to deep-link directly into the Scan to Pay app on their phone. See App-to-app on iOS and App-to-app on Android.

The backend code-create call is identical — only the front-end handoff changes.


What's next