Skip to content

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/miniapp 1.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

  1. 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_url or the terminal's url. Machine → your backend → Gerege.
  • A claimless intent cannot be created with a person's token (Bearer), and an app intent 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 is 403 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 same Idempotency-Key — the same link); never in GET, 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_url or 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's label/location, the sub-merchant's name and the amount — before Face ID. Gerege App does not show a merchant intent's description (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, claimed again); one person holds ≤ 3 unconfirmed claims at once (429 too_many_open_claims); an intent claimed by somebody else answers 409 already_claimed + retry_after: 90; you release it with POST /payment-intents/{id}/unclaim (Node paymentIntents.unclaim, Go PaymentIntents.Unclaim).
  • user_ref — null until the claim, then random per payment u_<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 older mapp_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_capture when 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 Settlement object (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
  • expired and canceled. A claimless intent that runs out of time is payment_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 with GET. 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»