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 design | Standard cart with no special UX needs |
| Multi-step checkout where Scan to Pay is one of several payment methods | Single-payment-method checkout |
| Large merchant with dedicated front-end engineering capacity | Small merchant or PSP without ongoing UI maintenance budget |
| You want to keep the customer on your domain for analytics / re-engagement | You 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:
- You render the QR on your own checkout page.
- Only one callback — the encrypted webhook to your notification URL. (Or you poll
queryRefif 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. SetexpiryDateto ~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
queryRef pollingIf 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.queryRefreturnsHTTP 406 Not Acceptableif 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 thetransactionId,bankResponse.retrievalReferenceNumber, andbankResponse.authCodefrom 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
- Render the QR → Rendering the QR
- Handle the webhook → Webhooks
- Poll for status if webhook isn't available → Querying transactions
- Mobile checkout via deep link → App-to-app
- Cancel a code if the customer abandons → Managing QR codes
- Refund or reverse → Refunds and reversals
- Test the flow end to end → Sandbox and test cards
Updated 4 days ago
