PSP Integration Flow — Overview (Internal)
AUDIENCE: INTERNAL ONLY. Dipindahkan dari Public site ke Internal pada 2026-04-30 karena berisi info tentang tier PSP, identitas acquirer (Mandiri/BRI/BNI), dan arsitektur multi-tenant yang bukan untuk konsumsi publik. Public-facing doc untuk Tier 3 partner ada di
api_docs/public/docs/api-docs/partner-api.md(tanpa mention PSP).
High-level flow bi-directional antara sistem PSP (Payment Service Provider) dan Kesles Merchant. Versi terstruktur lengkap (endpoint, payload, encryption spec) ada di PSP Integration Contract. Dokumen ini sebagai overview alur untuk onboarding engineer internal / partner team yang sudah NDA-locked.
Sistem production-ready untuk dua tier:
- PSP Internal Kesles (
payment.kesles.com) — single-tenant, akses penuh termasuk endpoint inbound/psp/v1/events,/psp/v1/settlements, dannmid-assignment. - PSP Bank / Acquirer eksternal (mis. Mandiri, BRI, BNI) — multi-tenant. Tiap bank dapat HMAC credential terpisah; sistem otomatis memfilter agar bank hanya melihat merchant yang NMID-nya diterbitkan oleh acquirer tersebut. Endpoint write-side (NMID assignment, payment events) dibatasi hanya untuk PSP Internal.
Untuk onboarding PSP Bank, hubungi tim integrasi Kesles dengan info: SWIFT 4-letter bank code, daftar IP allowlist untuk akses production, dan PIC teknis untuk koordinasi rotation kunci HMAC.
Service & Port Map
| Service | Port | Endpoint Prefix | Keterangan |
|---|---|---|---|
payment-service | 8085 | /psp/v1/events · /psp/v1/settlements | Event receiver (transaksi, NMID status, settlement) |
dashboard_api | 8082 | /api/psp/v1/merchants/* | Merchant lookup (read-only) |
dashboard_api | 8082 | /api/psp/v1/merchants/{id}/nmid-assignment | NMID assignment callback |
POST /api/psp/v1/payment-events/* di dashboard_api (port 8082) sudah dihapus (Phase 4). payment-service (port 8085) /psp/v1/events adalah satu-satunya receiver PSP webhook. Semua caller harus pakai /psp/v1/events.
Base URL
| Environment | URL | Status |
|---|---|---|
| Production | https://api-merchant.kesles.com | DNS ✅ · cert SAN ✅ · nginx vhost ✅ · backend live |
| Staging | https://api-merchant-staging.kesles.com | belum di-provision |
| Local dev | http://127.0.0.1:8085 (payment-service) · http://127.0.0.1:8082 (dashboard_api) | Loopback 401 missing_auth_headers jika tanpa credential ✅ |
Peran & Tanggung Jawab
| Sistem | Owner data | Exposure |
|---|---|---|
| Kesles Merchant | Merchant profile, status, transaction record | Mobile app, dashboard, partner API |
| PSP | QRIS master, settlement, bank credentials, PSP-side merchant/terminal identifier | H2H ke bank, customer payment page |
Alur 1 — PSP Lookup Merchant
PSP menerima QRIS payment → butuh merchant info dari Kesles berdasarkan NMID.
PSP Kesles Merchant (dashboard_api:8082)
│ │
├─ GET /api/psp/v1/merchants/ ────>│ verify HMAC
│ by-nmid/{nmid} │ verify IP allowlist
│ X-API-Key-ID: key-id │ filter bank_code
│ X-Timestamp: unix_secs │ return merchant data
│ X-Signature: hmac-sha256=.. │
│<──── 200 { merchant: {...} } ────┤
Auth: HMAC-SHA256 — lihat Integration Contract.
Alur 2A — PSP Forward Transaction Event (Flow A: Customer → Merchant)
Transaksi sukses di bank dengan NMID milik merchant Kesles → PSP push event ke payment-service.
PSP payment-service (8085) core_api (8080)
│ │ │
├─ POST /psp/v1/events ───────────>│ verify HMAC │
│ X-PSP-Key-ID: key-id │ cek external_event_id │
│ X-PSP-Timestamp: rfc3339 │ nmid → merchant lookup │
│ X-PSP-Signature: <hex,noprefix>│ INSERT payment.transactions │
│ { │ │
│ "external_event_id": "...", │── POST /internal/ │
│ "event_type": "transaction │ transaction-status ───────>│
│ .success", │ (X-Internal-API-Key) │
│ "nmid": "ID102...", │ │──► FCM push
│ "gross_amount": 75000, │<── 202 Accepted ─ ────────────┤ ke merchant
│ "transaction_code": "...", │ │
│ ...flat fields... │
│ } │
│<─ 200 {status:"success",flow:"A"}┤
Idempotency: external_event_id unique. Request yang sama dikirim ulang → HTTP 200 {"status":"duplicate_skipped"}, tidak diproses ulang.
Alur 2B — PSP Forward Transaction Event (Flow B: Merchant Beli Produk Kesles)
Transaksi sukses di bank dengan NMID korporat Kesles (ID9999999999999) → PSP push event → payment-service → notify order-service.
PSP payment-service (8085) order-service
│ │ │
├─ POST /psp/v1/events ───────────>│ verify HMAC │
│ { "nmid": "ID9999999999999", │ cek nmid = korporat │
│ "event_type": "transaction │ JANGAN insert ke │
│ .created", │ payment.transactions merchant│
│ "gross_amount": 250000, ...} │── notify order-service ─────>│
│ │ │
│<──── 200 { ack: true, flow:"B" }─┤
NMID korporat ID9999999999999 dikonfigurasi di db_kesles_merchant_payment.payment.qris_config.
Alur 3 — PSP Forward Settlement
Settlement harian dari bank → payment-service.
PSP payment-service (8085)
│ │
├─ POST /psp/v1/settlements ──────>│ verify HMAC
│ { "external_event_id": "...", │ INSERT payment.settlements
│ "nmid": "ID102...", │
│ "gross_amount": 3240000, ... }│
│<──── 200 { ack: true } ──────────┤
Alur 4 — NMID Status Changed
Bank suspend/aktifkan NMID → payment-service update status merchant.
PSP payment-service (8085)
│ │
├─ POST /psp/v1/events ───────────>│ verify HMAC
│ { "event_type": │ lookup merchant by nmid
│ "nmid.status_changed", │ UPDATE merchant.status
│ "nmid": "ID102...", │ push notification ke owner
│ "nmid_new_status": "suspended│
│ "nmid_old_status": "active", │
│ "nmid_reason": "fraud_detected"
│ } │
│<──── 200 { ack: true } ──────────┤
Alur 5 — Lifecycle Merchant (Kesles → PSP)
✅ Status: TERIMPLEMENTASI (2026-05-20), dispatch OFF default. Outbound client (
psp_outbound_client.go) live dengan mode switchPSP_OUTBOUND_MODE(defaultdisabled). Lifecycle dispatcher (psp_outbound_triggers.go) di-wire ke flow approve / PATCH / DELETE merchant. Saat tim PSP siap, ops set 4 env vars + restart — tidak perlu deploy code baru. Lihat contract §5.0 untuk detail mode & aktivasi.
Merchant di-approve / suspend / activate / terminate di Kesles dashboard → otomatis sync ke PSP saat mode=dispatch.
Kesles Merchant (dashboard_api) PSP (payment.kesles.com)
│ │
│ (a) Operator approve / PATCH / DELETE merchant │
│ di dashboard │
│ (b) firePSPMerchantLifecycleAsync │
│ decide: register|suspend|reactivate| │
│ terminate|update │
│ (c) INSERT psp.outbound_requests (audit row) │
│ │
├─ POST/PATCH payment.kesles.com/api/... ────────>│ verify HMAC
│ (signed via psp_integration_go.Signer) │ update merchant status
│<──── 200/2xx ──────────────────────────────────┤
│ │
│ (d) UPDATE psp.outbound_requests │
│ (response_status + duration_ms + body) │
│ │
│ Audit visible: /api/dashboard/integrations/ │
│ psp-outbound/audit │
Mode behavior:
| Mode | Behavior |
|---|---|
disabled (default) | Skip semua step b–d, no-op |
audit_only | Eksekusi b–c, skip HTTP call |
dispatch | Eksekusi semua step b–d (production) |
Penanganan Error
| Status | Keterangan | Retry? |
|---|---|---|
| 200/201 | Success | No |
| 401 | HMAC / timestamp / IP invalid | No — fix config |
| 404 | Merchant tidak ditemukan / tidak aktif | No |
| 429 | Rate limit exceeded | Yes, exponential backoff |
| 5xx | Server error | Yes, max 4 kali (1s, 2s, 4s, 8s) |
Praktik Terbaik
- Subscribe event via HTTP POST, bukan polling — menghemat rate limit
- Store
X-Request-IDdari response untuk trace kalau perlu eskalasi ke support - Implement circuit breaker di sisi PSP — kalau Kesles down, jangan retry terus sampai thundering herd
- Pakai dual-secret mode saat Kesles notify rotasi secret, supaya zero-downtime
- Perhatikan perbedaan signed_payload order antara legacy (timestamp\nmethod\npath\nbody) dan new (method\npath\ntimestamp\nbody)
Langkah Berikutnya
- Integration Contract — detail endpoint, payload schema, HMAC spec
- Events API — reference event types dan field
- PSP Tester — sandbox untuk testing integrasi