Security checklist¶
Go through this list before going live, and every quarter after. Time: 30 minutes. You need: your backend's code and configuration, and access to the console. The model behind it: Security model.
Storing secrets¶
- API key secrets (
gks_live_…,gks_test_…; the old default key'sgma_cs_…) and webhook secrets (whsec_…) live only on servers — in environment variables or a secret manager. Never in a mobile app, a browser, JavaScript, a terminal, a vending machine, a POS or a kiosk: they call your backend, and your backend calls Gerege. - No secret is in a git repository, a Docker image, a log, an error message or analytics (
.envis in.gitignore). Client ids (gk_…,mapp_…) are not secret — they may be logged. - A mini app's access and refresh tokens (
gma_at_…,gma_rt_…) stay in the backend; the frontend has only your own session cookie. - Test and live keys are stored separately; only the people who need the live secret can see it.
Keys — least privilege, rotation, revocation¶
- A separate, named key per server ("checkout-api", "bi-readonly") — if one leaks, you stop only that one. At most 20 active keys per app and environment.
- The fewest scopes: start from a preset —
checkout_server(payments:read,payments:write,refunds:write,events:read),read_only(BI, accounting),terminal_installer… Scopes never change later (400 scopes_immutable) — different rights are a new key. A missing scope is403 insufficient_scopewithrequired_scope. - Roll:
POST /api/v1/miniapp/dev/apps/{id}/keys/{kid}/roll{"overlap_seconds": …}— a new secret with the same name and scopes; the old one keeps working for the overlap (0 – 7 days, 24 hours by default). Deploy the new one within it — no downtime. A key withexpires_atwarns its creator and the admins before it expires. - Live keys: creating, rolling or re-addressing one needs
miniapp.keys.liveand a sign-in within the last ten minutes (403 fresh_sign_in_required→ sign in again and repeat). So does the default key's live secret (POST …/credentials{"environment": "live"}), whose rotation is immediate, with no overlap — prepare the deploy first, or use the keys API's roll (with an overlap). Creating, rolling and revoking a live key notifies the organisation's admins. - Revoke:
POST …/keys/{kid}/revoke{"reason": "…"}(a reason is required, ≤ 200) — immediate, with no fresh sign-in (stopping a leak is never slowed down). A revoked, expired or rolled key all answer the same401 invalid_client. - IP allowlist (
allowed_ips, ≤ 20 addresses/CIDRs): set your live servers' outgoing addresses — anything else is403 ip_not_allowed(ipis the address the platform saw, behind its trusted proxies). Changing a live key's allowlist needs a fresh sign-in. - Webhook secret: an endpoint's
POST …/webhook-endpoints/{eid}/rotate-secret— a newwhsec_…, and the old one keeps signing alongside for 24 hours (twov1). During that time your handler accepts[new, old], then drops the old one. For endpoint №1 (POST …/webhook{"url"}) every call issues a new secret.
Webhooks¶
- Verify every event's signature over the raw body (before JSON parsing), comparing in constant time, against
every
v1;|now − t| ≤ 300seconds (Verifying signatures). - Refuse an unsigned or wrongly signed request with
400and do nothing. - Deduplicate on
Gerege-Event-Id; re-read the intent from the API before handing anything over (Idempotent handler). - The endpoint is
https://and public; test and live use separate endpoints (different secrets); never send live events to a public "request bin".
Verifying payments¶
- The amount always comes from your server. Your backend creates the intent with its own order's amount; never with an amount or description from a browser, an app or a machine.
- Do not trust
return_url?pi=…. After a redirect checkout the person comes back to your page with?pi=pi_…— anyone can type that. From your server, with your own key,GET /payment-intents/{id}and checkstatus(succeeded/requires_capture),amount,referenceandlivemodeagainst the order. - A mini app bridge's
requestPaymentanswer, a Gerege App screen or a URL is no proof of payment; the proof is your backend'sGETor a webhook whose signature you verified. - One order = one intent = one
Idempotency-Key(the order number). Retry with the same key — a new key makes a second payment (Idempotency).
Terminals and stickers¶
- Terminals, stickers, NFC tags and QRs hold no secret — only the terminal's URL. The machine asks your backend for its basket.
- Lock every NFC tag after writing it (Gerege App's "Write sticker" checks and locks); inspect stickers regularly for one pasted over another (Testing NFC).
- Give terminals a clear
labelandlocation— Gerege App's payment sheet shows the registered name and place before Face ID, so a person can recognise the wrong machine. - Release the claim of somebody who walked away with
POST /payment-intents/{id}/unclaim; an unconfirmed claim also lapses after 90 seconds, and one person cannot hold more than 3 unconfirmed claims at once. - The key that registers and edits terminals (
terminals:write) is on your server, not on the machine; give installers a separate key with theterminal_installerpreset. - A signed URL on every read (NTAG 424 DNA, SUN) — coming soon.
Text the person sees¶
-
description(≤ 80 characters) is shown on the person's payment sheet — a short order summary ("Order #8812", "Water 0.5l"). Never URLs, phone numbers, "secret code" or "send the confirmation code" phrasing. - No tobacco or alcohol product names in
description(App Store rule 1.4.3); the age check stays on the machine or your side, or usemin_age. - A refund's
reasonis visible only to your API and webhooks, never in the person's notification — still, put no personal data in it.
Personal data¶
- A merchant never receives the person's name, register number, account or phone.
user_refis random per claim — not a stable id that links one person's payments (Personal data). - A mini app receives the person's pairwise
sub(different per app) and only the data of scopes they granted (profile.email,profile.phone) (Mini app launch & tokens). - Keep what you receive as the law requires, say so plainly in your privacy policy, and delete what you no longer need.
Detecting secrets¶
The prefixes are there for scanners: gks_live_ / gks_test_ (API key secrets — ending in a 6-character CRC32
checksum, so a well-formed key can be told from noise without a database), gma_cs_ (default key secret), whsec_
(webhook secret), gml_ (launch code), gma_at_ (access token), gma_rt_ (refresh token). Client ids (gk_live_ /
gk_test_ + 20 characters, mapp_ / mapp_test_) are not secret. For example, gitleaks rules:
[[rules]]
id = "gerege-api-key-secret"
description = "Gerege App API key secret"
regex = '''gks_(live|test)_[A-Za-z0-9_-]{43}_[0-9A-Za-z]{6}'''
[[rules]]
id = "gerege-client-secret"
description = "Gerege App default key secret"
regex = '''gma_cs_[A-Za-z0-9_-]{43}'''
[[rules]]
id = "gerege-webhook-secret"
description = "Gerege App webhook secret"
regex = '''whsec_[A-Za-z0-9_-]{43}'''
[[rules]]
id = "gerege-token"
description = "Gerege App access / refresh token"
regex = '''gma_(at|rt)_[A-Za-z0-9_-]{43}'''
If a secret leaks¶
- Stop it at once — an API key:
POST …/keys/{kid}/revoke(immediate); the default key:POST …/credentials(the old one stops at once); a webhook secret:rotate-secret(the old one keeps working for 24 hours — if it leaked, drop it from your handler straight away). - Deploy the new secret.
- In the console's audit (
key_created,key_rolled,key_revoked,credentials_rotated, endpoint changes …) check who did what, check the key'slast_used_at, and look for unknown actions in the payment and refund lists. - Contact Support — never send the secret itself.