Skip to content

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's gma_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 (.env is 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 is 403 insufficient_scope with required_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 with expires_at warns its creator and the admins before it expires.
  • Live keys: creating, rolling or re-addressing one needs miniapp.keys.live and 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 same 401 invalid_client.
  • IP allowlist (allowed_ips, ≤ 20 addresses/CIDRs): set your live servers' outgoing addresses — anything else is 403 ip_not_allowed (ip is 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 new whsec_…, and the old one keeps signing alongside for 24 hours (two v1). 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| ≤ 300 seconds (Verifying signatures).
  • Refuse an unsigned or wrongly signed request with 400 and 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 check status (succeeded/requires_capture), amount, reference and livemode against the order.
  • A mini app bridge's requestPayment answer, a Gerege App screen or a URL is no proof of payment; the proof is your backend's GET or 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 label and location — 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 the terminal_installer preset.
  • 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 use min_age.
  • A refund's reason is 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_ref is 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

  1. 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).
  2. Deploy the new secret.
  3. In the console's audit (key_created, key_rolled, key_revoked, credentials_rotated, endpoint changes …) check who did what, check the key's last_used_at, and look for unknown actions in the payment and refund lists.
  4. Contact Support — never send the secret itself.