๐Ÿ“‹ 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

1
Dapatkan token dari account-service โ–พ

Login/issue di account-service; terima access token RS256.

2
Panggil endpoint payment โ–พ

Kirim header Authorization: Bearer <access_token> ke setiap /api/v1/* (kecuali webhooks).

3
Sertakan reference_id milikmu โ–พ

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

GET /healthz

Selalu 200 selama proses hidup.

Example Response
{ "status": "ok" }
GET /healthz/ready

200 bila DB reachable dan public key tersedia; 503 bila ada check gagal. Body memuat status tiap check.

Example Response
{ "status": "ready", "checks": { "db": "ok", "public_key": "ok" } }
GET /api/v1/payment-methods

Daftar kapabilitas (provider ร— channel) dari provider yang terkonfigurasi. supports_status_inquiry menandakan row provider tsb bisa dipanggil refresh-status. Deterministik (sorted).

Example Response
{
  "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": "โ€ฆ"
}
POST /api/v1/payments

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

FieldTypeRequiredDescription
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
Example Request Body
{
  "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" }
}
Example Response
{
  "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": "โ€ฆ"
}
GET /api/v1/payments

History ter-paginate, di-filter query string. Dibatasi pada data milik principal pemanggil.

Query Parameters

ParamTypeRequiredDescription
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)
Example Response
{
  "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-โ€ฆ"
}
GET /api/v1/payments/{id}

Ambil satu row by UUID. 404 bila tidak ada; 403 bila milik principal lain.

Example Response
{
  "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": "โ€ฆ"
}
GET /api/v1/payments/by-reference/{reference-id}

Lookup by reference_id milik konsumer. 404 bila tidak ada.

Example Response
{
  "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": "โ€ฆ"
}
POST /api/v1/payments/{id}/refresh-status

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.

Example Response
{
  "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": "โ€ฆ"
}
POST /api/v1/payments/{id}/refund

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

FieldTypeRequiredDescription
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
Example Request Body
{ "reference_id": "refund-2026-0001", "amount": 25000, "reason": "customer cancelled" }
Example Response
{
  "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": "โ€ฆ"
}
POST /api/v1/webhooks/{provider}

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.

Example Response
{ "success": true, "data": { "received": true }, "request_id": "โ€ฆ" }

๐Ÿ” Credentials

Konfigurasi credentials untuk testing API

Authorization: Bearer <value>
X-Callback-Token: <value>
๐Ÿ’ก Info: Credentials tersimpan per environment di browser Anda (localStorage) dan tidak dikirim ke server.

Cara Menggunakan

1
Isi credentials di atas
2
Klik "Endpoints" di sidebar untuk memilih API
3
Klik "Try It" pada endpoint yang ingin di-test
4
Pilih metode autentikasi dan klik Send

๐Ÿ” 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).

Token contains: sub, org_id, iss, exp, permissions, principal_type

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

PermissionDescription