Skip to content

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_method defaults to automatic — 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.
  • description is 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}/cancel the old intent, and a new intent for the new basket (a new Idempotency-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) — unclaim as well. Do not put the pay_url anywhere 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 as branch, till, cashier; they come back in the webhook and in GET, and the person does not see them.
  • terminal_id is only for the terminal presentment (given with qr, 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»