Pay with Gerege App¶
An open API that lets any outside system — a web shop, a mobile app, a till, a kiosk, a vending machine, an invoice — take a payment from a person with Gerege App. Server side:
modules/miniapp1.7.0 (2026-10-27). The engine, webhooks, settlements and sandbox are exactly those of mini app payments (Mini app payments); the difference is that the intent is created without anybody's token, and the person later takes it for themselves (claim) inside Gerege App.This page is the common ground (registration, auth, claim, sandbox, reference). Step-by-step guides for each flow — §4. «coming soon» = not implemented in this version (§11).
1. What a merchant is¶
A merchant is a kind of mini app: kind: "merchant". It has the same registry, client id/secret (live, test),
webhook, review, settlements and analytics — but it never appears in the launcher and needs no entry URL, origin or
manifest.
kind: "miniapp" |
kind: "merchant" |
|
|---|---|---|
| Where the person pays | Your web app inside Gerege App, bridge requestPayment |
Your web/app/machine: pay_url (redirect, QR) or the terminal's sticker/NFC |
| Creating the intent | the person's access token (Bearer) |
the app's client credential (Basic) |
presentment |
app |
qr · redirect · terminal |
The person's name (user_ref) |
pairwise sub, fixed per app |
random per payment u_<32 hex> (no consent was given, so there is no fixed name) |
| Manifest, launcher | required | none |
A platform with many owners (each machine with a different owner) makes every owner a sub-merchant inside one merchant app — sub-merchants.
2. Registration and review¶
- The developer console of your Gerege App workspace (API:
POST /api/v1/miniapp/dev/apps):
{ "kind": "merchant", "slug": "vending", "name": "Vending.mn", "category": "services",
"icon_url": "https://vending.mn/icon-512.png", "developer_name": "Вендинг ХХК", "developer_email": "dev@vending.mn" }
The scope is automatically profile.basic + payments. kind can be changed later only while the app is a draft
or rejected. name and icon_url are shown to the person in the payment screen (in the form review approved).
2. API key: from the console (API: POST …/dev/apps/{id}/keys, preset «Checkout сервер» ("Checkout server"),
«Вендинг / kiosk backend» ("Vending / kiosk backend") …) → an id gk_test_… and a secret gks_test_… — the secret
is shown once. A live key is for now made with POST …/dev/apps/{id}/credentials {"environment": "live"} (the
older mapp_… / gma_cs_… pair — with every scope of its environment; creating live keys with the new API is
«coming soon»). Webhook: POST …/dev/apps/{id}/webhook {"url"} → whsec_… once (Webhooks).
3. For a redirect checkout, register your return hosts: PATCH …/dev/apps/{id} {"return_hosts": ["shop.mn"]} —
≤ 5, exact hosts (web checkout §2).
4. Review: POST …/dev/apps/{id}/submit. A merchant needs no manifest (…/manifest/check → 409 merchant_no_manifest);
but privacy_url, terms_url, support_url, an active live webhook endpoint (№1 from webhook_url, or any you
added — the refusal's path is still webhook_url) and a live secret are required, and the https legal
pages must answer 200 (a mailto: support is allowed) — anything missing is 400 not_ready (details). Then the
operator approves and sets the app's payout account (PUT /review/apps/{id}/payout).
Even before approval you can try everything with a test key (§6).
3. Auth¶
Every merchant call: Authorization: Basic base64(<key id>:<secret>) — gk_<env>_…:gks_<env>_…
(or the older mapp_…:gma_cs_…). Base URL
https://geregeapp.mn/api/v1/miniapp. Limit: 600 a minute (burst 120) — each for intents, terminals and sub-merchants.
- The client secret lives only on your server. A terminal, machine, kiosk, browser or mobile app NEVER holds the
secret — they only show the
pay_urlor the terminal'surl. Machine → your backend → Gerege. - A claimless intent cannot be created with a person's token (
Bearer), and anappintent cannot be created with a client credential — the two paths never mix (400 presentment_required). - The credential's environment (test / live) decides the intent's
livemode; a key sees only intents of its own environment. - Scopes: each key can do only what its scopes allow —
payments:read(read and list intents),payments:write(create, capture, cancel, unclaim),refunds:write,terminals:read/:write,sub_merchants:read/:write,payout_links:write,settlements:read. A missing one is403 insufficient_scope+required_scope. Presets: «Checkout сервер» ("Checkout server": payments, refunds), «Вендинг / kiosk backend» ("Vending / kiosk backend": - terminals, sub-merchants, payout-link), «Терминал суулгагч» ("Terminal installer"), «Зөвхөн унших» ("Read only").
A test key is never given the registration-writing scopes (
terminals:write,sub_merchants:write,payout_links:write). Rolling a key (with an overlap) and revoking it — in the console; IP allowlists — «coming soon».
4. Which guide is mine¶
| Your system | presentment |
Guide |
|---|---|---|
| Web shop | redirect |
Web shop |
| Your own iOS / Android app | redirect (the return is your Universal Link) |
Mobile app |
| Till, POS, kiosk — a QR on the screen for each basket | qr |
Till, POS, kiosk |
| Vending machine — take the money after the goods come out | terminal + manual |
Vending machine |
| A fixed point (machine, table, door) — QR/NFC sticker | terminal |
Terminals and NFC stickers |
| Many owners (marketplace, machine owners) | any of the above + sub_merchant_id |
Sub-merchants |
| Invoices, monthly bills | redirect (through your link) |
Invoices |
| A web app that opens inside Gerege App | app |
Mini app payments |
Common topics: holds (manual capture), reconciliation.
5. pay_url, claim, user_ref¶
pay_url=https://geregeapp.mn/pay/<24 characters>— present only in the response that creates a claimless intent (and in a replay with the sameIdempotency-Key— the same link); never inGET, lists or webhooks. It is the claim's bearer secret: do not log it, do not show it to anybody but that person. The token carries no amount and no recipient; the server stores only its SHA-256.- Claim — when the person scans the
pay_urlor holds their phone to the terminal, Gerege App makes the intent that person's (payment_intent.claimed). The payment screen shows the merchant's registered name, the terminal'slabel/location, the sub-merchant's name and the amount — before Face ID. Gerege App does not show a merchant intent'sdescription(M4) — it goes only into the person's wallet statement. - Claim protection (a terminal's code is printed, so anybody can claim from afar): an unconfirmed claim ends after
90 seconds and the next person can take it (a new
user_ref,claimedagain); one person holds ≤ 3 unconfirmed claims at once (429 too_many_open_claims); an intent claimed by somebody else answers409 already_claimed+retry_after: 90; you release it withPOST /payment-intents/{id}/unclaim(NodepaymentIntents.unclaim, GoPaymentIntents.Unclaim). user_ref—nulluntil the claim, then random per paymentu_<32 hex>: you cannot link one person's purchases to each other. Name, civil registration number, phone, account — never sent.min_age(13–21, not lower than the app's) — Gerege checks the payer at claim and at confirm: no eID →403 verification_required, under age →403 age_restricted.
6. Sandbox (test mode)¶
- With a test key (
gk_test_…, or the oldermapp_test_…) you create test intents and claim, capture, cancel, refund and unclaim them. No money moves,livemode: false; no payout account or core banking is needed. - A test key can only read terminals and sub-merchants — their registration is one for both environments, so
writing (create, edit, payout-link) takes a live key or the console (
403 live_only). On a terminal, a test basket does not block a live one. - A test claimless intent can be claimed only by the app's owner and its test users (active members of your
organisation — console); to everybody else that intent does not exist (
404). - The outcome is decided by the amount's last two digits (Mini app payments §6):
…01→failed(test_declined),…02→canceled, anything else →succeeded(requires_capturewhen manual). - Webhook deliveries, redelivery,
webhook.test— in the console (console §1). - «Симуляц» ("Simulation"), which imitates the person's side without a phone — «coming soon».
7. API reference¶
All under the /api/v1/miniapp prefix, Basic.
| Method | Path | Response |
|---|---|---|
| POST | /payment-intents |
201 / 200 (replay) — intent + pay_url. Idempotency-Key required |
| GET | /payment-intents/{id} · /payment-intents?reference=&status=&limit=&after= |
intent · {data, has_more} |
| POST | /payment-intents/{id}/capture · /cancel · /unclaim |
intent (capture may be 202) |
| POST | /payment-intents/{id}/refunds |
{amount?, reason?} + Idempotency-Key → 201 / 202 / 200 |
| GET · POST | /terminals |
{data: [Terminal]} · 201/200 Terminal (live) |
| GET · PATCH | /terminals/{id} |
Terminal |
| GET · POST | /sub-merchants |
{data: [SubMerchant]} · 201/200 SubMerchant (live) |
| GET · PATCH | /sub-merchants/{id} |
SubMerchant |
| POST | /sub-merchants/{id}/payout-link |
201 PayoutLink (live) |
| GET | /sub-merchants/{id}/payout-links |
{data: [PayoutLink]} (the last 50) |
| GET | /settlements · /settlements/{id} |
Mini app payments §7 |
| GET | /pay/{token} · /pay/t/{code} |
public, no auth: merchant name, terminal, amount, status (no description, reference or who is paying); 120 a minute |
Fields for creating a claimless intent (in addition to the other fields of Mini app payments §2):
| Field | Rule |
|---|---|
presentment |
qr · redirect · terminal (required with Basic) |
terminal_id |
required for terminal, forbidden otherwise |
sub_merchant_id |
optional; taken from the terminal automatically (400 if different) |
return_url |
required for redirect (allowed for qr, terminal); host in return_hosts; https; ≤ 2048 |
min_age |
13–21, not lower than the app's |
capture_within |
manual only: 60–604 800 s, default 604 800 |
New intent fields (on every intent, app too): presentment, terminal, sub_merchant, return_url,
claimed_at, min_age, capture_within; user_ref is null until the claim.
{ "id": "pi_…", "object": "payment_intent", "status": "requires_confirmation", "amount": 3500, "currency": "MNT",
"description": "Захиалга #77120", "reference": "77120", "user_ref": null, "livemode": true, "capture_method": "manual",
"refunded_amount": 0, "fee": null, "net": null, "failure_code": null, "expires_at": "…", "capture_before": null,
"confirmed_at": null, "created_at": "…", "metadata": {"machine": "VM-0007"},
"presentment": "terminal", "terminal": "4b8e…", "sub_merchant": "9a7c…", "return_url": null,
"claimed_at": null, "min_age": null, "capture_within": 180,
"pay_url": "https://geregeapp.mn/pay/Qk3v…" }
8. Settlements¶
The same as Mini app payments §7, with one difference: the stream is per (app, sub-merchant).
- Settlements off (the default; production is now
shadow): the full captured amount goes straight to payout — for a sub-merchant to its approved account, otherwise to the app's. - When the operator turns them on: each sub-merchant has its own clearing; fee and schedule follow the app's rule.
- The
Settlementobject (GET /settlements,settlement.paid) carries an extra"sub_merchant": "<id>" | null(null= the app's own stream). Details: sub-merchants §5.
9. Webhook¶
The same signature and retries as Webhooks. The events that matter to a merchant:
| type | When |
|---|---|
payment_intent.claimed |
The person took the intent for themselves (user_ref, claimed_at set) — «утсаараа баталгаажуулна уу» ("confirm on your phone") |
payment_intent.requires_capture |
Manual: the money is held — hand over the goods, then capture / cancel |
payment_intent.succeeded |
Paid |
payment_intent.expired |
A claimless intent ran out of time — whether it was claimed or not; also when a manual hold's capture_within ran out (failure_code: "authorization_expired") |
payment_intent.canceled |
You (or the person) cancelled |
payment_intent.failed |
Confirmation failed (failure_code); the person may try again until it expires |
refund.succeeded · refund.failed · settlement.paid |
Refunds, settlements |
expiredandcanceled. A claimless intent that runs out of time ispayment_intent.expired. A mini app (app) intent stays as before:payment_intent.canceled+status: "expired"— backends written against OMAS do not break.- Every intent event carries
presentment,terminal,sub_merchant(ids). - An event is sent once, immediately after it commits (a machine waits in seconds); on failure the usual retry schedule applies.
- At-least-once: an event may arrive again and order is not guaranteed — deduplicate by
id, and make your decision after re-reading the intent withGET. Receiver examples:examples/node/src/webhook.ts,examples/go/cmd/webhook/main.go.
10. Errors¶
The shape is the same as in Error codes: {"error": "<message in Mongolian>", "code": "<code>", …};
credential errors in the OAuth shape {"error": "invalid_client", "error_description"}.
| HTTP | code | When | What to do |
|---|---|---|---|
| 400 | presentment_required |
an intent over Basic without presentment (or with app) |
qr / redirect / terminal |
| 400 | validation_error |
details[].path: presentment, terminal_id, sub_merchant_id, return_url, min_age, capture_within, … |
fix the field |
| 400 | idempotency_key_required |
Idempotency-Key missing |
send your order number |
| 400 | invalid_sub_merchant |
the terminal's sub_merchant_id is wrong |
|
| 400 | code_immutable |
tried to change a sub-merchant's code |
a new sub-merchant |
| 401 | invalid_client |
wrong client id/secret, Basic missing | credential |
| 403 | insufficient_scope |
the key lacks this route's scope (required_scope) |
the key's scopes |
| 403 | live_only |
writing a terminal, sub-merchant or payout-link with a test key | a live key or the console |
| 409 | terminal_busy |
the terminal has an open intent (payment_intent: its id) |
cancel that one, then again |
| 409 | terminal_disabled · sub_merchant_disabled |
inactive | PATCH status: active |
| 409 | payouts_not_configured |
live: no approved payout for the app or the sub-merchant | operator / payout-link |
| 409 | reference_taken |
another terminal has this reference |
|
| 409 | not_unclaimable |
unclaim of an intent whose confirmation is under way | wait |
| 409 | idempotency_conflict |
the key was used with a different body | |
| 409 | not_capturable · expired · not_cancelable · in_progress |
the status does not allow the capture/cancel (with payment_intent) |
read the intent and decide |
| 422 | account_unsupported |
the payout account's holder is neither a Gerege App person nor an organisation | another account |
| 422 | no_admin |
an organisation's account, but that organisation has no admin to approve | the organisation appoints an admin |
| 502 | core_unavailable |
core banking did not answer («мэдэгдсэнгүй», "unknown") | again |
| 503 | directory_unavailable |
the organisation's admins could not be determined for now | again |
| 503 | payments_unavailable · pay_url_unavailable |
the installation has no core banking / PUBLIC_ORIGIN |
again, operator |
The full list of codes — Error codes. Network errors, 429, 5xx — resending with the
same Idempotency-Key is safe.
11. Implementation status¶
| Part | Status |
|---|---|
kind: "merchant", return_hosts, merchant review (legal pages + operator) |
✓ (API; the console screen is in the core) |
Claimless intents (qr, redirect, terminal), pay_url, min_age, capture_within, claim protection, unclaim |
✓ |
Terminals, sub-merchants, payout linking approved by the owner (terms), settlements per (app, sub-merchant) |
✓ |
payment_intent.claimed / .expired, immediate webhook delivery, public GET /pay/{token}, /pay/t/{code} |
✓ |
Node SDK (paymentIntents.create(params, opts), terminals, subMerchants), Go client (miniapp.NewClient) |
✓ |
Working examples: examples/node, examples/go (checkout, vending, webhook) |
✓ |
Scoped, named, rotatable API keys (gk_ / gks_, miniapp 1.8.0) |
✓ (creating live keys with the new API, IP allowlists — «coming soon») |
Gerege App iOS: QR, in-app NFC reader, /pay/ Universal Link → claim → payment screen; payout approval screen; NFC sticker writer |
✓ (Universal Link and background NFC reading — once the core's AASA with /pay/* is in production) |
The https://geregeapp.mn/pay/<token> QR page on a computer (the core's shell) |
«coming soon» |
App Clip (https://appclip.geregeapp.mn/pay?t=<code>; paying inside the Clip with eID + an SMS code) |
«coming soon» |
Partial capture with an amount (POST /capture {amount}) |
«coming soon» — full capture + refund |
Multiple webhook endpoints (≤ 5 per environment, filters, secret rotation), events API (GET /events, miniapp 1.9.0) |
✓ |
| Dashboard, reports, simulation, print page | «coming soon» |
| Terminals where the person types the amount; paying by holding the phone to the terminal (NFC & SE); NTAG 424 DNA | «coming soon» |