Till, POS, kiosk — a QR code on the screen¶
On this page
What: show a dynamic QR code for every basket on the till/kiosk screen, and print the receipt once the person has scanned it and paid with Gerege App. Time: ~2 hours. You need: a merchant app and a test credential (registration), a till ↔ your backend connection, a webhook URL.
A new qr intent for every basket → pay_url as a QR code on the screen → the person scans it with Gerege App's
QR button → payment_intent.claimed («Утсаараа баталгаажуулна уу», "Confirm on your phone") →
payment_intent.succeeded (receipt). If you need a permanent, printed QR/NFC (a machine, a table, a door) —
terminals and stickers.
1. Flow¶
Till / kiosk Your backend Gerege Person (Gerege App)
│ basket closed │ │ │
│── POST /sales ───────▶│ POST /payment-intents │ │
│ │ {presentment:"qr", …} │ │
│ │────────────────────────────▶│ │
│◀── {qr: pay_url, 3500}│◀──── {id, pay_url} ─────────│ │
│ QR + «3 500₮» on screen │◀──── scans QR (claim) ────│
│ │◀── payment_intent.claimed ──│ │
│◀── «Утсаараа баталгаажуулна уу» │◀──── Face ID → pays ──────│
│ │◀── payment_intent.succeeded │ │
│◀── «Төлөгдлөө» → print receipt │ │
The till holds no secret: it talks only to your backend (its own token, TLS) and shows the pay_url.
«Төлөгдлөө» = "Paid".
2. An intent per basket¶
curl https://geregeapp.mn/api/v1/miniapp/payment-intents \
-u "$GEREGE_CLIENT_ID:$GEREGE_CLIENT_SECRET" \
-H 'Content-Type: application/json' -H 'Idempotency-Key: sale-0042-1188' \
-d '{"amount": 3500, "description": "Касс №2 · баримт 1188", "reference": "sale-0042-1188",
"expires_in": 180, "presentment": "qr", "metadata": {"branch": "sbd-1", "till": "2"}}'
- A short
expires_in(120–300 s): the next person must not be able to scan a QR code left on the screen. Idempotency-Key= your sale number: even if the till → backend request is repeated, one intent, one QR code.capture_methoddefaults toautomatic— the money is taken as soon as the person confirms. For a kiosk where the goods come afterwards (food, fuel, a locker),manual+capture_within— the vending pattern.descriptionis shown in the person's wallet statement (Gerege App's payment screen does not show it): the shop/till/receipt number — not personal data.- With several owners (a franchise, a marketplace),
sub_merchant_id(sub-merchants).
3. Drawing the QR code¶
- Its content is exactly
pay_url(https://geregeapp.mn/pay/<24 characters>) — do not change it, shorten it or pass it through another domain. - Error correction M (or Q), a quiet zone ≥ 4 modules, dark on a white background, no inverted colours.
- On screen, ≥ 3 cm a side (readable from 30–40 cm); no glare on a bright screen.
- Next to the QR code: the amount, your shop's name, «Gerege App-аар уншуулна уу» ("Scan with Gerege App"), the time left.
- Do not print the QR code on a receipt and say «дараа төл» ("pay later") — that is the invoice flow.
4. Waiting for the result — webhook or polling¶
Combine both: the webhook is fast (sent once, straight away, as soon as the event is committed); polling is the backstop for when the webhook is late or lost.
Event / status |
Till |
|---|---|
payment_intent.claimed (claimed_at set) |
«Утсаараа баталгаажуулна уу» ("Confirm on your phone") |
payment_intent.succeeded / succeeded |
«Төлөгдлөө» ("Paid") → print the receipt, close the basket |
payment_intent.failed / failed |
«Амжилтгүй — дахин оролдоно уу» ("Failed — please try again") (the person may pay again until the time runs out) |
payment_intent.expired / expired |
clear the QR code, «Дахин эхлүүлэх» ("Start again") |
payment_intent.canceled / canceled |
the person declined in the app, or you cancelled |
The till asks your backend every 1–2 s (not Gerege — the till holds no secret); the backend answers with what it
learnt from the webhook and, if it has heard nothing for more than 10 s, checks with GET /payment-intents/{id}.
The server API's limit is 600 requests a minute (burst 120) — if every till polled Gerege directly, it would run out
quickly.
5. Cancelling, changing the basket¶
- The cashier cancelled / the basket changed →
POST /payment-intents/{id}/cancelthe old intent, and a new intent for the new basket (a newIdempotency-Key). While the person is paying (processing),409 in_progress— wait a few seconds and read it again. - The person scanned and walked away — an unconfirmed claim ends by itself in 90 seconds, or release it at
once with
POST /payment-intents/{id}/unclaim(the next person can scan). One person holds ≤ 3 unconfirmed claims at a time. - The wrong person scanned (someone standing behind) —
unclaimas well. Do not put thepay_urlanywhere other than the QR code. - Refunding a paid one —
POST /payment-intents/{id}/refunds(full or partial, within 90 days).
6. If the till or the network drops¶
| What happened | What to do |
|---|---|
| Till → backend dropped | the till keeps the sale in its local log and, once connected, asks the backend by the sale number |
| Backend → Gerege timeout | repeat with the same Idempotency-Key — the same intent, the same pay_url |
| Webhook did not arrive | backend GET /payment-intents/{id} (or ?reference=) — polling is the backstop |
| The person paid but the till is stuck «waiting» | ask the backend again by reference; if succeeded, print the receipt |
| The till's end-of-day close | GET /payment-intents?reference=… or reconciliation |
7. Several tills, branches¶
metadata(≤ 20 keys) — your IDs such asbranch,till,cashier; they come back in the webhook and inGET, and the person does not see them.terminal_idis only for theterminalpresentment (given withqr,400 validation_error). If every till needs a permanent QR/NFC, register the till as a terminal — terminals and stickers.- A report broken down by branch/till — «coming soon» (dashboard). For now, aggregate it yourself with
metadata.
8. Code¶
Node (@gerege/miniapp-node) — drawing the QR code is a library of your choice (for example qrcode):
const pi = await gerege.paymentIntents.create(
{ presentment: "qr", amount: sale.total, description: `Касс №${sale.till} · баримт ${sale.no}`,
reference: sale.id, expires_in: 180, metadata: { branch: sale.branch, till: String(sale.till) } },
{ idempotencyKey: sale.id },
);
till.showQR(pi.pay_url, sale.total); // on the screen only; never written to a log
Go (sdk/go):
pi, err := c.PaymentIntents.Create(ctx, sale.ID, miniapp.CreatePaymentIntentParams{
Amount: sale.Total, Description: fmt.Sprintf("Касс №%d · баримт %s", sale.Till, sale.No),
Reference: sale.ID, ExpiresIn: 180, Presentment: miniapp.PresentmentQR,
Metadata: map[string]string{"branch": sale.Branch},
})
A complete webhook receiver example: examples/node/src/webhook.ts, examples/go/cmd/webhook/main.go.
9. Implementation status¶
| Part | Status |
|---|---|
qr intent, pay_url, claim (90 s, ≤ 3), unclaim, immediate webhook |
✓ |
Gerege App for iOS's QR scanner (the QR button, bottom right) recognises /pay/<token> → claim → payment screen |
✓ |
| A report broken down by branch and till | «coming soon» |