Агуулгыг алгасах

15 минутад анхны test төлбөр

Юу хийх: test түлхүүрээр qr төлбөр (payment intent) үүсгэж, хүний талыг тоглуулж, webhook хүлээн авч, мөнгийг capture хийнэ. Хугацаа: ~15 минут. Мөнгө хөдлөхгүй — бүгд test горимд (livemode: false). Сервер тал: modules/miniapp 1.9.0.

Энэ хуудсыг дуусгахад танд: худалдагч апп (kind: "merchant"), scope-той test API түлхүүр, гарын үсэг шалгадаг webhook endpoint ба хүлээн авагч, requires_capture → succeeded болсон нэг test intent байна. Ойлголтуудыг Төлбөрийн амьдралын мөчлөг, Төлбөрийн урсгалууд ба claim-аас уншина.

0. Юу хэрэгтэй

Gerege App-ын бүртгэл geregeapp.mn-д eID-ээр нэвтэрсэн
Байгууллага Таны админ эсвэл менежер байдаг байгууллага (workspace) — §1
Хэрэгсэл curl, jq, Python 3 (webhook хүлээн авагчид), cloudflared эсвэл ngrok (tunnel)

Хуулийн этгээдгүй бол

Хувь хүнд зориулсан sandbox байгууллага (нэг товчоор, eID-ийн төлөөлөлгүй) — «удахгүй». Одоогоор байгууллагатай хамт ажиллагчаасаа таныг гишүүнээр нэмүүлэх хүсэлт гаргана (/join/<slug> холбоос → админ батална), эсвэл өөрийн компанийн нэрийн өмнөөс байгууллагаа нээнэ (eID-д байгууллагын ADMIN төлөөлөгчөөр бүртгэлтэй хүн: профайл → Байгууллагууд).

1. «Mini апп-ууд»-ыг байгууллагадаа суулгах

Байгууллагын админ geregeapp.mn → байгууллагаа сонгоод → App Store (/apps) → «Mini апп-ууд» (mn.gerege.miniapp) → Суулгах. Энэ нь хөгжүүлэгчийн консол ба Gerege App-аар төлөх API-г байгууллагад нээнэ. Нэг удаа л хийнэ; суулгасан мөчөөс test горим бүхэлдээ ажиллана — review хэрэггүй.

2. Консолын API-д нэвтрэх

Худалдагч апп бүртгэх (§3) нь одоогоор консолын API-аар л (консолын бүртгэх цонхны «Худалдагч» сонголт — «удахгүй»); доорх §4–§5-ыг ч энэ хуудас API-аар хийнэ. Консолын API нь таны хүний session-ээр (Authorization: Bearer) ажиллана:

  1. geregeapp.mn-д байгууллагаа идэвхтэй болгоно (толгойн байгууллага солигч).
  2. Хөтчийн DevTools → Application (Storage) → Cookies → https://geregeapp.mn → session_token-ийн утгыг хуулна.
export GEREGE=https://geregeapp.mn/api/v1/miniapp
export GEREGE_SESSION='<session_token-ийн утга>'

Session бол таны бүрэн нэвтрэлт

Хэнд ч бүү өг, repo, лог, чатад бүү хуул. Хамгийн ихдээ 12 цаг амьдарна; гарахад (sign out) үхнэ. Зөвхөн консолын (/dev/…) ба хүний талын тоглуулгад (§7) хэрэглэнэ — таны сервер API түлхүүрээр (§4) ярина.

3. Худалдагч апп бүртгэх

curl -sS "$GEREGE/dev/apps" \
  -H "Authorization: Bearer $GEREGE_SESSION" -H 'Content-Type: application/json' \
  -d '{"kind": "merchant", "slug": "my-shop-test", "name": "Миний дэлгүүр"}' | tee app.json | jq
export APP_ID=$(jq -r .id app.json)

Хүлээгдэх хариу 201 (товчилсон):

{
  "id": "00000000-0000-4000-8000-000000000001",
  "slug": "my-shop-test", "name": "Миний дэлгүүр", "kind": "merchant",
  "scopes": ["profile.basic", "payments"], "category": "services",
  "status": "draft", "review_status": "draft",
  "live_client_id": "mapp_…", "test_client_id": "mapp_test_…",
  "has_live_secret": false, "has_test_secret": false,
  "webhook_url": null, "return_hosts": [], "payouts_configured": false, "version": 1
}
  • slug — 2–32 тэмдэгт, a-z, 0-9, -, платформд нэг л: 409 slug_taken бол өөр нэр. Хувийн sandbox байгууллагад slug sbx--ээр эхэлнэ (sbx-my-shop; Sandbox).
  • Худалдагч Gerege App-ын launcher-т хэзээ ч гарахгүй, manifest шаардахгүй (Худалдагч ба mini app).

4. Test API түлхүүр авах

Preset «Checkout сервер» (checkout_server) = payments:read, payments:write, refunds:write, events:read — энэ хуудсанд хэрэгтэй бүх эрх:

curl -sS "$GEREGE/dev/apps/$APP_ID/keys" \
  -H "Authorization: Bearer $GEREGE_SESSION" -H 'Content-Type: application/json' \
  -d '{"name": "quickstart", "environment": "test", "preset": "checkout_server"}' | tee test-key.json | jq
export GEREGE_CLIENT_ID=$(jq -r .client_id test-key.json)
export GEREGE_CLIENT_SECRET=$(jq -r .client_secret test-key.json)
{ "id": "…", "object": "api_key", "name": "quickstart", "environment": "test",
  "client_id": "gk_test_…", "secret_hint": "…", "status": "active", "legacy": false,
  "scopes": ["payments:read", "payments:write", "refunds:write", "events:read"], "allowed_ips": [],
  "client_secret": "gks_test_…", "notice": "Энэ secret-ийг дахин харуулахгүй — одоо хадгална уу" }

Secret нэг удаа харагдана (сервер зөвхөн SHA-256-ийг нь хадгална; secret_hint нь сүүлийн 4 тэмдэгт — аль нь болохыг танихад). Үүнээс хойшх бүх худалдагчийн дуудлага Authorization: Basic base64(client_id:client_secret) — curl-д -u. Scope дутвал 403 insufficient_scope + required_scope. Солих (…/keys/{kid}/roll) ба хүчингүй болгох (…/keys/{kid}/revoke) — консолоос. Хуучин POST /dev/apps/{id}/credentials (mapp_test_… / gma_cs_…, бүх эрхтэй) ажилласаар; live түлхүүр одоогоор тэр замаар л (шинэ API-д дахин нэвтрэлт шаардана — «удахгүй»). Secret зөвхөн таны сервер дээр: браузер, мобайл апп, терминал, машинд хэзээ ч (Аюулгүй байдлын загвар).

5. Webhook хүлээн авагч

Gerege webhook-ийг зөвхөн нийтийн https хаяг руу илгээнэ — localhost руу шууд биш. Тиймээс локал серверээ tunnel-ээр гаргана.

5.1 Tunnel (тусдаа терминалд) — хэвлэсэн https://….trycloudflare.com хаягийг тэмдэглэнэ:

cloudflared tunnel --url http://127.0.0.1:8787

5.2 Test орчны webhook endpoint үүсгэж, secret авна (event_types хоосон = бүх event; апп-д орчин бүрт ≤ 5 endpoint, тус бүр өөрийн secret-тэй):

export TUNNEL=https://<таны-tunnel>.trycloudflare.com
curl -sS "$GEREGE/dev/apps/$APP_ID/webhook-endpoints" \
  -H "Authorization: Bearer $GEREGE_SESSION" -H 'Content-Type: application/json' \
  -d "{\"url\": \"$TUNNEL/gerege\", \"environment\": \"test\", \"event_types\": []}" | tee webhook.json | jq
export ENDPOINT_ID=$(jq -r .endpoint.id webhook.json)
export GEREGE_WEBHOOK_SECRET=$(jq -r .webhook_secret webhook.json)
{ "endpoint": { "id": "we_…", "object": "webhook_endpoint", "environment": "test", "livemode": false,
                "url": "https://<таны-tunnel>.trycloudflare.com/gerege", "event_types": [], "status": "active" },
  "webhook_secret": "whsec_…", "notice": "Энэ secret-ийг дахин харуулахгүй — одоо хадгална уу" }

5.3 Хүлээн авагч — webhook.py (Python-ы стандарт сан л):

import hashlib, hmac, json, os, time
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["GEREGE_WEBHOOK_SECRET"].encode()

def verify(header: str, body: bytes) -> bool:
    t, sigs = None, []
    for part in header.split(","):
        key, _, value = part.strip().partition("=")
        if key == "t" and value.isdigit():
            t = int(value)
        elif key == "v1":
            sigs.append(value)
    if t is None or abs(time.time() - t) > 300:      # 5 минутаас хуучин бол replay
        return False
    want = hmac.new(SECRET, str(t).encode() + b"." + body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(want, s) for s in sigs)

class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", 0)))  # түүхий byte — parse-ээс өмнө
        if not verify(self.headers.get("Gerege-Signature", ""), body):
            self.send_response(400); self.end_headers(); return
        event = json.loads(body)
        obj = event["data"]["object"]
        print(event["id"], event["type"], obj.get("id"), obj.get("status"), flush=True)
        self.send_response(204); self.end_headers()  # 2xx-ыг 10 с дотор

HTTPServer(("127.0.0.1", 8787), Hook).serve_forever()
python3 webhook.py

5.4 Шалгах — гарын үсэгтэй webhook.test-ийг одоо илгээнэ:

curl -sS -X POST "$GEREGE/dev/apps/$APP_ID/webhook-endpoints/$ENDPOINT_ID/test" \
  -H "Authorization: Bearer $GEREGE_SESSION" | jq '{ok, status_code}'

{"ok": true, "status_code": 204} гарч, хүлээн авагчийн терминалд evt_… webhook.test None None хэвлэгдэнэ. Гарын үсгийн бүх дүрэм: Webhook.

6. qr intent үүсгэх

Таны сервер сагс бүрт нэг intent үүсгэнэ. Энэ жишээ manual capture — мөнгийг эхлээд барьцаалж (hold), бараа өгсний дараа л авна (Барьцаа ба capture).

curl -sS "$GEREGE/payment-intents" -u "$GEREGE_CLIENT_ID:$GEREGE_CLIENT_SECRET" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: order-1001' \
  -d '{"amount": 3500, "description": "Захиалга №1001", "reference": "order-1001",
       "presentment": "qr", "expires_in": 600,
       "capture_method": "manual", "capture_within": 300}' | tee pi.json | jq
export PI=$(jq -r .id pi.json)
export PAY_TOKEN=$(jq -r '.pay_url | split("/") | last' pi.json)

Хүлээгдэх хариу 201:

{
  "id": "pi_8Hn2Wq4Zc6Xv1Bm3Np5Rt7Ly",
  "object": "payment_intent",
  "status": "requires_confirmation",
  "amount": 3500,
  "currency": "MNT",
  "description": "Захиалга №1001",
  "reference": "order-1001",
  "user_ref": null,
  "livemode": false,
  "capture_method": "manual",
  "refunded_amount": 0,
  "fee": null,
  "net": null,
  "failure_code": null,
  "expires_at": "2026-10-27T03:10:00Z",
  "capture_before": null,
  "confirmed_at": null,
  "created_at": "2026-10-27T03:00:00Z",
  "metadata": {},
  "presentment": "qr",
  "terminal": null,
  "sub_merchant": null,
  "return_url": null,
  "claimed_at": null,
  "min_age": null,
  "capture_within": 300,
  "pay_url": "https://geregeapp.mn/pay/Qk3vX9aB2cD4eF6gH8jK0mNp"
}
  • Ижил Idempotency-Key + ижил body-оор дахин илгээвэл 200 — ижил intent, ижил pay_url; өөр body → 409 idempotency_conflict (Давталт).
  • pay_url нь зөвхөн энэ хариунд (ба давталтад) байна — GET, жагсаалт, webhook-д хэзээ ч. Түүнд дүн, хүлээн авагч байхгүй; лог-д бүү бич.
  • Бодит касс энэ pay_url-ийг дэлгэцэндээ QR болгон зурна (qrencode -t ansiutf8 "$(jq -r .pay_url pi.json)").

7. Хүний тал — claim ба баталгаажуулалт

Бодит амьдралд хүн QR-ийг Gerege App-аар уншуулж, төлөх цонхонд Face ID дарна. Апп ард нь хоёр дуудлага хийдэг: claim (intent-ийг өөртөө авах) ба confirm (данс сонгож төлөх).

Утсаар уншуулах

Gerege App (iOS)-ын апп доторх QR товч ба NFC уншигч нь pay_url-ийг claim хийж төлөх цонхыг нээнэ — TestFlight 121-ээс хойшх build-д. Утасны камераар уншуулах (Universal Link) ба компьютер дээрх /pay хуудас — «удахгүй». Таны утсанд тэр build байхгүй бол апп-ын хийдэг хоёр дуудлагыг өөрийн session-ээр доорх шиг хийнэ — сервер дээрх зам яг ижил. Test intent-ийг зөвхөн апп-ыг бүртгэсэн хүн (та) ба тест хэрэглэгчид claim хийнэ; бусдад тэр intent байхгүй мэт (404 unknown_payment).

7.1 Claim:

curl -sS "$GEREGE/payments/claim" -H "Authorization: Bearer $GEREGE_SESSION" \
  -H 'Content-Type: application/json' -d "{\"token\": \"$PAY_TOKEN\"}" | jq
{
  "id": "pi_8Hn2Wq4Zc6Xv1Bm3Np5Rt7Ly", "status": "requires_confirmation",
  "amount": 3500, "currency": "MNT", "description": "Захиалга №1001",
  "livemode": false, "capture_method": "manual",
  "merchant": {"slug": "my-shop-test", "name": "Миний дэлгүүр", "icon_url": ""},
  "account_id": null, "failure_code": null,
  "expires_at": "2026-10-27T03:10:00Z", "confirmed_at": null, "created_at": "2026-10-27T03:00:00Z",
  "presentment": "qr", "terminal": null, "sub_merchant": null, "return_url": null
}

Хүлээн авагчид: evt_… payment_intent.claimed pi_… requires_confirmation. Баталгаажаагүй claim 90 секундэд дуусна — дараа нь өөр хүн авч болно; тэр хүртэл дахин claim хийх нь танд idempotent.

7.2 Confirm — төлөх данс (Gerege Wallet) сонгоно:

export ACCOUNT=$(curl -sS https://geregeapp.mn/api/v1/bank/me/accounts \
  -H "Authorization: Bearer $GEREGE_SESSION" | jq -r '.accounts[0].id')
curl -sS -X POST "$GEREGE/payments/$PI/confirm" -H "Authorization: Bearer $GEREGE_SESSION" \
  -H 'Content-Type: application/json' -H "Idempotency-Key: confirm-$PI" \
  -d "{\"account_id\": \"$ACCOUNT\"}" | jq '{id, status, account_id}'
{ "id": "pi_8Hn2Wq4Zc6Xv1Bm3Np5Rt7Ly", "status": "requires_capture", "account_id": "0b6c…" }

Хүлээн авагчид: evt_… payment_intent.requires_capture pi_… requires_capture. Test горимд мөнгө хөдлөхгүй — дүнгийн сүүлийн хоёр орон үр дүнг шийднэ: …01 → failed (test_declined), …02 → canceled, бусад → энд шиг (Test ба live).

8. Capture

Бараагаа өгсөн (эсвэл үйлчилгээгээ үзүүлсэн) бол мөнгийг авна — capture_within (энд 300 с) дотор:

curl -sS -X POST "$GEREGE/payment-intents/$PI/capture" -u "$GEREGE_CLIENT_ID:$GEREGE_CLIENT_SECRET" \
  | jq '{id, status, capture_before, confirmed_at}'
{ "id": "pi_8Hn2Wq4Zc6Xv1Bm3Np5Rt7Ly", "status": "succeeded",
  "capture_before": "2026-10-27T03:06:12Z", "confirmed_at": "2026-10-27T03:01:40Z" }

Хүлээн авагчид: evt_… payment_intent.succeeded pi_… succeeded. Бараа гараагүй бол capture-ийн оронд POST /payment-intents/$PI/cancel → canceled, барьцаа хүнд буцна. Юу ч хийгээгүй бол capture_before өнгөрөхөд барьцаа өөрөө суллагдаж payment_intent.expired (failure_code: "authorization_expired") ирнэ.

9. Шалгах ба тулгах

Webhook-д сохроор бүү итгэ — чухал алхмын өмнө төлвийг өөрийн түлхүүрээр дахин уншина:

curl -sS "$GEREGE/payment-intents/$PI" -u "$GEREGE_CLIENT_ID:$GEREGE_CLIENT_SECRET" | jq '{status, amount, reference, user_ref}'
curl -sS "$GEREGE/events?limit=20" \
  -u "$GEREGE_CLIENT_ID:$GEREGE_CLIENT_SECRET" | jq -r '.data[] | "\(.id) \(.type)"'

user_ref нь энэ нэг төлбөрийн санамсаргүй нэр (u_<32 hex>) — хүний нэр, регистр, данс танд хэзээ ч ирэхгүй (Хүний мэдээлэл). Events API (GET /events, events:read) нь webhook-ийн яг тэр body-г түлхүүрийн орчноор, хуучнаас нь 30 хоног буцаана — webhook алдсан (сервер унтарсан, машин offline) үед энэ жагсаалтаар тулгана. Хүргэлтийн лог ба дахин илгээх — консолд (Хөгжүүлэгчийн sandbox).

10. Алдаа гарвал

Хариу Шалтгаан Засах
401 {"error": "invalid_client"} client id/secret буруу, хүчингүй/хугацаа дууссан түлхүүр; эсвэл live түлхүүрийг апп батлагдахаас өмнө §4-ийн test түлхүүрийг ашигла
403 insufficient_scope (required_scope) түлхүүрт тэр scope алга шинэ түлхүүр (scope өөрчлөгдөхгүй)
400 idempotency_key_required Idempotency-Key толгой алга (1–128 тэмдэгт) захиалгын дугаараа өг
400 presentment_required Basic-аар presentment-гүй intent qr / redirect / terminal
400 validation_error details[].path талбар буруу (дүн 100–50 000 000, тайлбар 1–80, expires_in 60–1800, capture_within 60–604 800) талбараа засна
404 unknown_payment (claim) token буруу, эсвэл test intent-ийг апп-ын эзэн/тест хэрэглэгч биш хүн өөрийн session-ээ шалга
409 already_claimed (retry_after: 90) өөр хүн claim хийсэн 90 с хүлээ, эсвэл худалдагч unclaim
410 expired expires_at өнгөрсөн шинэ intent
409 not_capturable manual биш, эсвэл хараахан requires_capture биш төлвийг GET-ээр шалга
401 unauthorized (консол) session дууссан эсвэл буруу §2-ыг давт
503 no_secret_key · pay_url_unavailable суулгацын тохиргоо бидэнтэй холбогдоно уу

Бүх код: Алдааны код.

11. Дараа нь