๐ Overview
Payment gateway / adapter untuk ekosistem ikavia.com. Service ini
menerjemahkan request bayar/transfer dari service konsumer menjadi
panggilan ke provider pembayaran eksternal (Xendit, BCA SNAP, โฆ), lalu
mencatat history keluar-masuk uang (audit trail) untuk rekonsiliasi.
Bukan billing service: tidak ada wallet/saldo/ledger, tidak ada
subscription/dunning, tidak menyimpan data user/organization. Identitas
pemanggil diambil dari JWT yang diterbitkan account-service (disimpan
apa adanya sebagai requested_by_sub / requested_by_org, bukan FK).
Konteks bisnis (siapa menagih siapa, kapan jatuh tempo) tetap di konsumer
dan dihubungkan via reference_id yang mereka tentukan.
Tech: Go 1.26 (gin + GORM), PostgreSQL 16. Deploy: PM2,
reverse-proxy nginx + Let's Encrypt di payment.ikavia.com.
> โ๏ธ Status saat ini: staging. Endpoint baca/auth/health berfungsi
> penuh, namun pembuatan charge belum aktif sampai kredensial provider
> (Xendit/BCA) dikonfigurasi. POST /api/v1/payments mengembalikan
> 400 provider not configured sampai saat itu.
๐ณ Charge & disburse
Satu endpoint generik, banyak channel
๐ช Provider webhooks
Endpoint publik, verifikasi signature per-adapter
๐งพ History & rekonsiliasi
Audit trail in/out
Base URL
Constraints
- Idempotency: satu reference_id unik per (provider) intent pembayaran.
- Tidak ada wallet/saldo/ledger โ service hanya mencatat history in/out.
- Tidak ada tabel user/organization โ identitas dari JWT account-service (literal, bukan FK).
- Webhook provider bersifat publik; diverifikasi via signature adapter, bukan JWT.
- Kebijakan bisnis (eligibility refund, cap, jatuh tempo) adalah domain konsumer, bukan service ini.
๐ณ Charge & disburse
POST /api/v1/payments membuat charge inbound (QRIS, VA, e-wallet, card) atau disbursement/refund outbound. Provider & channel dipilih eksplisit di body. Idempotent pada (provider, reference_id) โ mengulang request yang sama mengembalikan row yang sudah ada.
๐ช Provider webhooks
POST /api/v1/webhooks/:provider menerima callback provider. Tanpa JWT โ tiap adapter memverifikasi signature/token-nya sendiri (mis. Xendit x-callback-token). Status transaksi lokal di-update dari event yang sudah terverifikasi.
๐งพ History & rekonsiliasi
Catatan minimal per transaksi: arah, provider, channel, amount, status, requester (dari JWT), plus payload mentah provider untuk audit. Background job menandai row pending yang lewat expires_at menjadi expired. Konsumer bisa pull status via refresh-status.
๐ Bearer JWT Authentication Flow
Step-by-step authentication menggunakan Bearer JWT
Login/issue di account-service; terima access token RS256.
Kirim header Authorization: Bearer <access_token> ke setiap /api/v1/* (kecuali webhooks).
Tentukan reference_id unik per intent pembayaran; dipakai sebagai kunci idempotency dan untuk lookup balik.
Endpoint webhook tidak memakai JWT โ diverifikasi via signature provider.
๐ Service Flow Diagram
Visualisasi alur data antara services
Selalu 200 selama proses hidup.
{ "status": "ok" }
200 bila DB reachable dan public key tersedia; 503 bila ada check gagal. Body memuat status tiap check.
{ "status": "ready", "checks": { "db": "ok", "public_key": "ok" } }
Daftar kapabilitas (provider ร channel) dari provider yang terkonfigurasi. supports_status_inquiry menandakan row provider tsb bisa dipanggil refresh-status. Deterministik (sorted).
{
"success": true,
"data": {
"methods": [
{ "provider": "bca", "channel": "qris", "supports_status_inquiry": true },
{ "provider": "midtrans", "channel": "card", "sub_channels": ["visa","mastercard","jcb"], "supports_status_inquiry": true },
{ "provider": "midtrans", "channel": "ewallet", "sub_channels": ["gopay","shopeepay","dana","ovo"], "supports_status_inquiry": true },
{ "provider": "midtrans", "channel": "qris", "supports_status_inquiry": true },
{ "provider": "midtrans", "channel": "va", "sub_channels": ["bca","bni","bri","mandiri","permata","cimb"], "supports_status_inquiry": true },
{ "provider": "xendit", "channel": "card", "supports_status_inquiry": false },
{ "provider": "xendit", "channel": "ewallet", "sub_channels": ["ID_OVO","ID_DANA","ID_LINKAJA","ID_SHOPEEPAY","ID_ASTRAPAY","ID_JENIUSPAY"], "supports_status_inquiry": false },
{ "provider": "xendit", "channel": "qris", "supports_status_inquiry": false },
{ "provider": "xendit", "channel": "va", "sub_channels": ["BCA","BNI","BRI","MANDIRI","PERMATA","BSI","CIMB"], "supports_status_inquiry": false }
]
},
"request_id": "โฆ"
}
Membuat charge (inbound) atau disbursement (outbound) via provider. Idempotent pada (provider, reference_id): mengulang body yang sama mengembalikan row yang ada (200). provider โ {xendit, bca, midtrans}; channel โ {qris,va,ewallet,card,disbursement}; amount โฅ 1 (satuan terkecil; IDR = rupiah penuh). Errors: 400 provider tidak terkonfigurasi/validasi, 409 reference_id dipakai principal lain, 502 panggilan provider gagal.
Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
| provider | string | required | xendit | bca | midtrans |
| reference_id | string | required | ID unik milik konsumer (1โ128 char) |
| channel | string | required | qris | va | ewallet | card | disbursement |
| amount | integer | required | Nominal, โฅ 1 (IDR rupiah penuh) |
| currency | string | optional | Default IDR |
| channel_params | object | optional | Param spesifik channel (mis. ewallet: channel_code, mobile_number) |
| provider_context | object | optional | Identifier teknis provider (mis. BCA merchant_id, terminal_id) untuk multi-merchant routing |
| metadata | object | optional | Free-form, diteruskan ke provider bila didukung |
| expires_at | string | optional | RFC3339; dibatasi oleh kebijakan lifetime service |
{
"provider": "xendit",
"reference_id": "order-2026-0001",
"channel": "ewallet",
"amount": 25000,
"currency": "IDR",
"channel_params": { "channel_code": "ID_DANA", "mobile_number": "+628123456789" },
"metadata": { "order_id": "order-2026-0001" }
}
{
"success": true,
"data": {
"id": "3b9cโฆ",
"provider": "xendit",
"reference_id": "order-2026-0001",
"provider_payment_id": "ewc_โฆ",
"direction": "inbound",
"channel": "ewallet",
"status": "pending",
"amount": 25000,
"currency": "IDR",
"requested_by_sub": "a2f3โฆ",
"charge_artifact": { "checkout_url": "https://โฆ" },
"expires_at": "2026-06-08T07:00:00Z",
"created_at": "2026-06-08T06:00:00Z",
"updated_at": "2026-06-08T06:00:00Z"
},
"request_id": "โฆ"
}
History ter-paginate, di-filter query string. Dibatasi pada data milik principal pemanggil.
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
| provider | string | optional | xendit | bca |
| channel | string | optional | qris | va | ewallet | card | disbursement |
| status | string | optional | pending | succeeded | failed | expired | refunded |
| direction | string | optional | inbound | outbound |
| reference-id | string | optional | Filter exact reference_id |
| page | integer | optional | Halaman (mulai 1) |
| page-size | integer | optional | Item per halaman (default 20) |
{
"success": true,
"data": {
"items": [
{
"id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a",
"provider": "xendit",
"reference_id": "order-2026-0001",
"provider_payment_id": "ewc_a1b2c3d4",
"direction": "inbound",
"channel": "ewallet",
"status": "succeeded",
"amount": 25000,
"currency": "IDR",
"requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
"requested_by_org": "545c61c3-b211-406a-bd0a-0140e7bbe941",
"metadata": { "order_id": "order-2026-0001" },
"charge_artifact": { "checkout_url": "https://checkout.xendit.co/web/ewc_a1b2c3d4" },
"expires_at": "2026-06-08T07:00:00Z",
"succeeded_at": "2026-06-08T06:05:12Z",
"created_at": "2026-06-08T06:00:00Z",
"updated_at": "2026-06-08T06:05:12Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
},
"request_id": "b1d2e3f4-โฆ"
}
Ambil satu row by UUID. 404 bila tidak ada; 403 bila milik principal lain.
{
"success": true,
"data": {
"id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a",
"provider": "xendit",
"reference_id": "order-2026-0001",
"provider_payment_id": "ewc_a1b2c3d4",
"direction": "inbound",
"channel": "ewallet",
"status": "succeeded",
"amount": 25000,
"currency": "IDR",
"requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
"requested_by_org": "545c61c3-b211-406a-bd0a-0140e7bbe941",
"metadata": { "order_id": "order-2026-0001" },
"charge_artifact": { "checkout_url": "https://checkout.xendit.co/web/ewc_a1b2c3d4" },
"expires_at": "2026-06-08T07:00:00Z",
"succeeded_at": "2026-06-08T06:05:12Z",
"created_at": "2026-06-08T06:00:00Z",
"updated_at": "2026-06-08T06:05:12Z"
},
"request_id": "โฆ"
}
Lookup by reference_id milik konsumer. 404 bila tidak ada.
{
"success": true,
"data": {
"id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a",
"provider": "xendit",
"reference_id": "order-2026-0001",
"provider_payment_id": "ewc_a1b2c3d4",
"direction": "inbound",
"channel": "ewallet",
"status": "succeeded",
"amount": 25000,
"currency": "IDR",
"requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
"created_at": "2026-06-08T06:00:00Z",
"updated_at": "2026-06-08T06:05:12Z"
},
"request_id": "โฆ"
}
Re-query provider untuk status terkini (dipakai saat webhook telat/sunyi). 501 bila provider tidak mendukung status inquiry (mis. Xendit saat ini), 403 bila bukan milik principal, 502 bila inquiry provider gagal.
{
"success": true,
"data": {
"id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a",
"provider": "bca",
"reference_id": "order-2026-0001",
"provider_payment_id": "00000019021000000001",
"direction": "inbound",
"channel": "qris",
"status": "succeeded",
"amount": 25000,
"currency": "IDR",
"requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
"succeeded_at": "2026-06-08T06:05:12Z",
"created_at": "2026-06-08T06:00:00Z",
"updated_at": "2026-06-08T06:05:12Z"
},
"request_id": "โฆ"
}
Membalik charge inbound yang sudah berhasil lewat endpoint refund provider. provider, channel, currency, provider_context di-inherit dari row asli. Service tidak menerapkan kebijakan bisnis refund (eligibility, cap) โ itu domain konsumer. Errors: 403 bukan milik principal, 404 row asli tidak ada, 422 row tanpa provider id / bukan charge inbound, 501 provider tidak mendukung refund.
Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
| reference_id | string | required | Reference unik untuk row refund (outbound) |
| amount | integer | required | Nominal refund, โฅ 1 |
| reason | string | optional | Alasan (โค 256 char) |
| metadata | object | optional | Free-form |
{ "reference_id": "refund-2026-0001", "amount": 25000, "reason": "customer cancelled" }
{
"success": true,
"data": {
"id": "9f1e4a7c-2b3d-4e5f-8a90-0c1d2e3f4a5b",
"provider": "xendit",
"reference_id": "refund-2026-0001",
"provider_payment_id": "rfd_9z8y7x",
"direction": "outbound",
"channel": "ewallet",
"status": "succeeded",
"amount": 25000,
"currency": "IDR",
"requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
"charge_artifact": {
"provider_refund_id": "rfd_9z8y7x",
"amount": 25000,
"currency": "IDR",
"refunded_at": "2026-06-08T08:10:00Z"
},
"metadata": { "original_id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a" },
"succeeded_at": "2026-06-08T08:10:00Z",
"created_at": "2026-06-08T08:10:00Z",
"updated_at": "2026-06-08T08:10:00Z"
},
"request_id": "โฆ"
}
Satu route untuk semua provider; :provider diresolusi ke registry saat runtime. Adapter memverifikasi signature (Xendit x-callback-token, BCA SNAP signature) lalu mem-parse event ke bentuk channel-agnostic dan meng-update status row. Responses: 200 {received:true}, 400 payload buruk, 401 signature gagal, 404 provider tidak dikenal/terkonfigurasi.
{ "success": true, "data": { "received": true }, "request_id": "โฆ" }
๐ Credentials
Konfigurasi credentials untuk testing API
Cara Menggunakan
๐ Authentication
Methods and permissions for API access
Authentication Methods
Bearer JWT
Header: Authorization: Bearer <access_token>
Source: account-service (https://account.ikavia.com)
Access token RS256 yang diterbitkan account-service. Payment service memverifikasi signature memakai public key yang di-fetch dari GET <account>/api/v1/auth/public-key (envelope Ikavia, di-cache ke .pem per kid, refresh berkala + on unknown-kid). Divalidasi juga iss=account-service dan exp. Service ini tidak pernah menyimpan JWT secret.
Note: Claim sub disimpan sebagai requested_by_sub dan org_id sebagai
requested_by_org pada tiap row โ dipakai untuk ownership check
(Get/Refund/RefreshStatus menolak 403 bila principal berbeda).
Provider Signature
Header: X-Callback-Token: <token / signature per provider>
Source: payment provider (Xendit, BCA, โฆ)
Khusus endpoint webhook POST /api/v1/webhooks/:provider. Bukan JWT. Tiap adapter memverifikasi skema-nya sendiri โ Xendit memakai header x-callback-token, BCA memakai SNAP asymmetric signature. Gagal verifikasi โ 401.
Permissions
| Permission | Description |
|---|