# Bayar Digital — Dokumentasi Developer (Full) > Payment gateway semua bank & QRIS untuk merchant Indonesia. Full documentation for AI agents. --- # Overview Dokumentasi integrasi **Payment Gateway Bayar Digital** untuk developer Tenant. Dengan API ini, sistem kamu bisa: - Membuat invoice pembayaran untuk customer - Mendapatkan notifikasi real-time via webhook saat customer bayar - Mengecek status pembayaran - Membatalkan invoice ## Cara Kerja ```mermaid sequenceDiagram participant S as Server Kamu participant API as Bayar Digital API participant W as Android Worker participant C as Customer S->>API: 1. GET /gateway/accounts API-->>S: Daftar payment account S->>API: 2. POST /gateway/payments API-->>S: Payment detail + checkout_url S->>C: 3. Tampilkan instruksi bayar C->>C: 4. Transfer / QRIS sesuai nominal W->>API: 5. Deteksi pembayaran masuk API->>S: 6. Webhook POST → status PAID ``` **Singkatnya:** 1. **Ambil akun** → `GET /gateway/accounts` untuk lihat rekening tujuan 2. **Buat invoice** → `POST /gateway/payments`, sistem generate nominal unik 3. **Customer bayar** → Tampilkan instruksi via `checkout_url` atau UI kamu 4. **Deteksi otomatis** → Android Worker di perangkatmu mendeteksi transfer masuk 5. **Notifikasi** → Kamu terima webhook `PAID`, update status order ## Yang Perlu Kamu Siapkan | Komponen | Fungsi | |----------|--------| | Backend server | Simpan API Key, panggil Gateway API | | Database order | Simpan `payment_code`, `payment_total`, `status` | | Webhook endpoint | Terima notifikasi perubahan status | | Android device khusus | Install Worker untuk deteksi otomatis | | Cron job (opsional) | Rekonsiliasi berkala sebagai fallback | ## Alur Dokumen Baca dokumentasi sesuai urutan berikut: | # | Topik | Deskripsi | |---|-------|-----------| | 1 | [Persiapan](./persiapan) | Setup API Key, base URL, rate limit | | 2 | [Android Worker](./android-worker) | Install aplikasi deteksi pembayaran | | 3 | [Payment Account](./payment-account) | Lihat daftar rekening tujuan | | 4 | [Payment Create](./payment-create) | Buat invoice pembayaran | | 5 | [Checkout](./checkout) | Halaman bayar untuk customer | | 6 | [Payment Get](./payment-get) | Cek status pembayaran | | 7 | [Payment Cancel](./payment-cancel) | Batalkan invoice | | 8 | [Payment Match](./payment-match) | Cocokkan pembayaran manual | | 9 | [Payment Mutations](./payment-mutations) | Lihat mutasi terdeteksi | | 10 | [Webhook](./webhook) | Setup notifikasi real-time | | 11 | [Status & Error Code](./status-code) | Referensi kode error | ## Daftar Endpoint | Method | Endpoint | Deskripsi | |--------|----------|-----------| | `GET` | `/gateway/accounts` | Daftar rekening aktif | | `POST` | `/gateway/payments` | Buat invoice baru | | `GET` | `/gateway/payments` | Daftar invoice | | `GET` | `/gateway/payments/:code` | Detail invoice | | `DELETE` | `/gateway/payments/:code` | Batalkan invoice | | `POST` | `/gateway/payments/:code/match` | Cocokkan manual (butuh 2FA) | | `GET` | `/gateway/channels/:id/instructions` | Instruksi pembayaran | | `GET` | `/gateway/mutations` | Mutasi bank terdeteksi | --- # Persiapan Setup awal yang harus dilakukan sebelum bisa menggunakan Payment Gateway. ## Prasyarat | Langkah | Detail | |---------|--------| | 1. Daftar akun | Registrasi di [dashboard Bayar Digital](https://bayar.digital) | | 2. Buat Merchant | Buat entitas bisnis → dapatkan **API Key** (`pk_...`) | | 3. Setup Rekening | Tambah rekening bank atau QRIS di dashboard | | 4. Setup Android Worker | Instal aplikasi Worker di HP khusus ([panduan](./android-worker)) | | 5. Siapkan endpoint webhook | Buat endpoint di server kamu untuk terima notifikasi | ## Base URL ``` https://api.bayar.digital ``` Semua endpoint Gateway menggunakan prefix `/gateway/`. ## Autentikasi Kirim API Key via header `X-Api-Key` di setiap request. ``` X-Api-Key: pk_550e8400e29b41d4a716446655440000... ``` API Key didapatkan dari Dashboard → menu Merchant → **Generate API Key**. :::warning Simpan API Key di **environment variable** server backend kamu. Jangan pernah hardcode di frontend atau repository publik. ::: ```bash # .env BAYAR_DIGITAL_API_KEY=pk_550e8400e29b41d4a716446655440000... BAYAR_DIGITAL_BASE_URL=https://api.bayar.digital ``` Lihat [Status & Error Code](./status-code) untuk daftar lengkap error auth dan kode lainnya. ## Rate Limit | Grup | Limit per Merchant | |------|-------------------| | **Read** (`GET` endpoints) | 3.000 request/menit | | **Write** (`POST`, `DELETE`) | 600 request/menit | Jika terlampaui: ```json { "success": false, "code": "rate_limited", "message": "rate limited" } ``` Response header `X-RateLimit-Reset` memberi tahu berapa detik hingga limit reset. ## Response Format Semua response API mengikuti format berikut. ### Success (200) ```json { "success": true, "message": "ok", "data": { ... } } ``` ### Created (201) ```json { "success": true, "message": "created", "data": { ... } } ``` ### Paginated ```json { "success": true, "message": "ok", "data": [ ... ], "pagination": { "total": 100, "count": 20, "per_page": 20, "current_page": 1, "total_pages": 5 } } ``` ### Error ```json { "success": false, "code": "ERROR_CODE", "message": "Pesan error" } ``` ### Validation Error ```json { "success": false, "code": "validation_error", "message": "validation error", "errors": { "field_name": "deskripsi error" } } ``` --- # Android Worker Aplikasi Android yang dipasang di perangkat khusus milik Anda untuk **mendeteksi pembayaran masuk** secara otomatis. Tanpa Worker aktif, sistem tidak bisa mendeteksi transfer customer secara otomatis. :::info **Konteks:** Setelah [Persiapan](./persiapan) selesai, langkah selanjutnya adalah setup Android Worker sebagai "mata" sistem untuk mendeteksi transfer masuk. ::: ## Cara Kerja Worker dipasang di HP khusus yang **juga terinstall aplikasi mobile banking** (BCA Mobile, Livin' Mandiri, dll). Ketika ada notifikasi transfer masuk, Worker langsung: 1. Membaca notifikasi dari aplikasi bank 2. Mengekstrak nominal dan pengirim 3. Mengirim data ke server Bayar Digital 4. Sistem mencocokkan dengan invoice yang PENDING ## Persiapan | Item | Keterangan | |------|------------| | Perangkat Android khusus | **Jangan pakai HP pribadi.** Sediakan device khusus yang selalu online | | Koneksi internet stabil | Worker perlu kirim data ke server secara real-time | | Aplikasi banking terpasang | Install aplikasi bank di perangkat yang **sama** | | API Key | Siapkan API Key Merchant dari Dashboard | :::warning Worker harus berjalan 24 jam. Gunakan perangkat yang selalu terhubung listrik dan internet. ::: ## Instalasi ### 1. Download APK Download APK Worker dari Dashboard Bayar Digital atau link berikut: [Download Worker APK](https://bayar.digital/downloads/app-worker-latest.apk) ### 2. Install APK Kirim file APK ke perangkat Android, lalu buka dan install. ### 3. Masukkan API Key Buka aplikasi Worker, masukkan **API Key** (`pk_...`) yang didapat dari Dashboard. ### 4. Registrasi Perangkat Setelah masukkan API Key, aplikasi akan otomatis mendaftarkan perangkat ke server. Status perangkat menjadi **PENDING**. ### 5. Approve Device di Dashboard Login ke Dashboard Bayar Digital → menu **Pairing Device** → klik **Setujui** pada perangkat yang baru mendaftar. ### 6. Berikan Izin Aplikasi akan meminta 3 izin. Semua **wajib** diberikan agar Worker berfungsi: | Izin | Cara Memberikan | |------|-----------------| | **Notifikasi** | Pop-up akan muncul → tap **Izinkan** | | **Akses Notifikasi** | Masuk ke *Settings → Notification Access* → aktifkan **bayar.digital Worker** | | **Optimasi Baterai** | Pop-up akan muncul → tap **Izinkan** / **Nonaktifkan** | ### 7. Selesai Worker mulai berjalan. Status di Dashboard berubah menjadi **Aktif**. Aplikasi akan menampilkan notifikasi *"Listening for bank notifications"* sebagai tanda sedang berjalan. ## Troubleshooting ### Payment tetap PENDING padahal customer sudah transfer | Kemungkinan | Solusi | |-------------|--------| | HP mati / offline | Cek koneksi internet perangkat | | Baterai hemat daya aktif | Nonaktifkan optimasi baterai di settings | | Izin akses notifikasi hilang | Cek Settings → Notification Access | | Worker tidak terlihat | Cek notifikasi foreground "Listening for bank notifications" | | Aplikasi bank tidak terinstall | Install aplikasi bank di device yang **sama** | ### Cara Verifikasi 1. Cek status device di Dashboard → **Pairing Device** → pastikan **Aktif** 2. Cek status payment via API: `GET /gateway/payments/{payment_code}` 3. Cek apakah mutasi masuk: `GET /gateway/mutations?only_unmatched=true` Jika masalah berlanjut, hubungi tim support Bayar Digital. **Lanjutan:** Setelah Worker aktif, lihat [Payment Account](./payment-account) untuk melihat rekening tujuan yang tersedia. --- # Payment Account Melihat daftar rekening aktif yang tersedia untuk digunakan sebagai tujuan pembayaran. :::info Sebelum membuat invoice, kamu perlu tahu rekening tujuan mana yang tersedia. Gunakan `account_id` dari response ini saat [Payment Create](./payment-create). ::: | Method | URL | |--------|-----| | `GET` | `/gateway/accounts` | Tidak ada parameter. | Header | Wajib | Deskripsi | |--------|-------|-----------| | `X-Api-Key` | Ya | API Key merchant | Tidak ada request body (GET). ```bash curl https://api.bayar.digital/gateway/accounts \ -H "X-Api-Key: pk_..." ``` ```json { "success": true, "message": "ok", "data": [ { "account_id": "550e8400-e29b-41d4-a716-446655440000", "account_number": "1234567890", "account_name": "PT Merchant Contoh", "account_min_amount": 10000, "account_max_amount": 10000000, "account_active": true, "account_quota": 30, "channel_id": "660e8400-e29b-41d4-a716-446655440001", "channel_type": "TRANSFER", "channel_name": "Bank Central Asia", "app_id": "990e8400-e29b-41d4-a716-446655440002", "app_name": "BCA Mobile", "app_package": "com.bca" }, { "account_id": "550e8400-e29b-41d4-a716-446655440003", "account_number": "QRIS-001", "account_name": "PT Merchant Contoh", "account_min_amount": 1000, "account_max_amount": 5000000, "account_active": true, "account_quota": 30, "channel_id": "660e8400-e29b-41d4-a716-446655440004", "channel_type": "QRIS", "channel_name": "QRIS", "app_id": null, "app_name": null, "app_package": null } ] } ``` ```json { "success": false, "code": "tenant_api_key_required", "message": "tenant api key required" } ``` ```json { "success": false, "code": "internal_error", "message": "internal server error" } ``` ## Response Fields | Field | Tipe | Deskripsi | |-------|------|-----------| | `account_id` | uuid | **Gunakan ini** sebagai `account_id` saat create payment | | `account_number` | string/null | Nomor rekening. QRIS: label/nama merchant | | `account_name` | string/null | Nama pemilik rekening | | `account_min_amount` | int64 | Minimal nominal pembayaran | | `account_max_amount` | int64/null | Maksimal nominal (null = tak terbatas) | | `account_active` | bool | Status aktif | | `account_quota` | int | Sisa hari kuota | | `channel_id` | uuid | ID channel bank | | `channel_type` | string | `TRANSFER` (transfer bank) atau `QRIS` | | `channel_name` | string/null | Nama bank | | `app_id` | uuid/null | ID aplikasi mobile banking | | `app_name` | string/null | Nama aplikasi mobile banking | | `app_package` | string/null | Package name Android | ## Error | Code | Status | Artinya | |------|--------|---------| | `tenant_api_key_required` | 403 | API Key tidak valid | | `internal_error` | 500 | Server error, coba lagi | ## Cara Memilih Account - **Transfer bank** → pilih `channel_type: "TRANSFER"` - **QRIS** → pilih `channel_type: "QRIS"` Simpan `account_id` sebagai konfigurasi di sistem kamu, lalu kirim sebagai `account_id` saat create payment. **Lanjutan:** Sudah tahu rekening tujuan? Lanjut ke [Payment Create](./payment-create) untuk membuat invoice. --- # Payment Create Membuat invoice pembayaran baru. Sistem otomatis menambahkan **nominal unik** (1-999) agar transfer customer bisa dideteksi. :::info Setelah mendapat `account_id` dari [Payment Account](./payment-account), langkah ini membuat invoice yang akan dibayar customer. `payment_code` bersifat **idempotency key** — request dengan kode yang sama akan ditolak (409), mencegah duplikasi invoice. ::: Sistem generate angka acak 1-999 sebagai nominal unik. Customer WAJIB membayar sebesar `payment_total` (`payment_amount + payment_unique`), bukan `payment_amount`. ``` payment_total = 50.000 + 123 = 50.123 ``` Tampilkan di UI: ``` Total yang harus dibayar: Rp 50.123 (Rp 50.000 + kode unik Rp 123) ``` | Method | URL | |--------|-----| | `POST` | `/gateway/payments` | Tidak ada parameter URL. | Header | Wajib | Deskripsi | |--------|-------|-----------| | `X-Api-Key` | Ya | API Key merchant | | `Content-Type` | Ya | `application/json` | | Field | Tipe | Wajib | Deskripsi | |-------|------|-------|-----------| | `account_id` | uuid | Ya | ID akun tujuan (dari `GET /gateway/accounts`) | | `payment_code` | string | Ya | Idempotency key. Kode unik dari sistem kamu | | `payment_amount` | int64 | Ya | Nominal invoice sebelum nominal unik | | `payment_expired_at` | string | Ya | Batas waktu (ISO 8601, harus di masa depan) | | `customer_name` | string | Ya | Nama customer | | `customer_email` | string | Tidak | Email (maks 255). Minimal email atau phone | | `customer_phone` | string | Tidak | Telepon (maks 50). Minimal email atau phone | | `payment_webhook_url` | string | Tidak | Webhook endpoint khusus invoice ini | | `payment_return_url` | string | Tidak | Redirect setelah customer bayar | | `customer_orders` | array | Tidak | Daftar item pesanan | **Order Item:** | Field | Tipe | Wajib | Deskripsi | |-------|------|-------|-----------| | `sku` | string | Tidak | SKU produk | | `name` | string | Ya | Nama produk | | `price` | int64 | Ya | Harga satuan | | `quantity` | int | Ya | Jumlah | | `subtotal` | int64 | Ya | Akan di-override sistem = `price × quantity` | | `product_url` | string | Tidak | URL halaman produk | | `image_url` | string | Tidak | URL gambar produk | :::info Jika `customer_orders` diisi, total seluruh `subtotal` harus sama dengan `payment_amount`. ::: ```bash curl -X POST https://api.bayar.digital/gateway/payments \ -H "X-Api-Key: pk_..." \ -H "Content-Type: application/json" \ -d '{ "account_id": "550e8400-e29b-41d4-a716-446655440000", "payment_code": "INV-2026-0001", "payment_amount": 50000, "payment_expired_at": "2026-10-11T12:00:00Z", "customer_name": "Budi Santoso", "customer_email": "budi@example.com", "customer_phone": "081234567890" }' ``` ```json { "account_id": "550e8400-e29b-41d4-a716-446655440000", "payment_code": "INV-2026-0001", "payment_amount": 50000, "payment_expired_at": "2026-10-11T12:00:00Z", "customer_name": "Budi Santoso", "customer_email": "budi@example.com", "customer_phone": "081234567890", "payment_webhook_url": "https://yourserver.com/webhooks/bayar", "payment_return_url": "https://yourserver.com/orders/INV-2026-0001", "customer_orders": [ { "sku": "SKU001", "name": "Produk A", "price": 50000, "quantity": 1, "subtotal": 50000, "product_url": "https://yourserver.com/products/produk-a", "image_url": "https://yourserver.com/images/produk-a.jpg" } ] } ``` **201 Created** ```json { "success": true, "message": "created", "data": { "payment_id": "660e8400-e29b-41d4-a716-446655440010", "payment_code": "INV-2026-0001", "payment_amount": 50000, "payment_unique": 123, "payment_total": 50123, "payment_status": "PENDING", "payment_expired_at": "2026-10-11T12:00:00Z", "payment_updated_at": "2026-06-11T10:00:00Z", "payment_webhook_url": "https://yourserver.com/webhooks/bayar", "payment_checkout_url": "https://bayar.digital/checkout/660e8400-e29b-41d4-a716-446655440010", "payment_return_url": "https://yourserver.com/orders/INV-2026-0001", "customer_name": "Budi Santoso", "customer_email": "budi@example.com", "customer_phone": "081234567890", "customer_orders": [ { "sku": "SKU001", "name": "Produk A", "price": 50000, "quantity": 1, "subtotal": 50000, "product_url": "https://yourserver.com/products/produk-a", "image_url": "https://yourserver.com/images/produk-a.jpg" } ], "account_id": "550e8400-e29b-41d4-a716-446655440000", "account_number": "1234567890", "account_name": "PT Merchant Contoh", "channel_id": "660e8400-e29b-41d4-a716-446655440001", "channel_name": "Bank Central Asia", "channel_type": "TRANSFER", "channel_instructions": [], "is_manual_match": false, "manual_matched_mutation_id": null } } ``` **Validation Error (400)** ```json { "success": false, "code": "validation_error", "message": "validation error", "errors": { "customer_name": "customer name is required" } } ``` **Conflict (409)** ```json { "success": false, "code": "payment_code_conflict", "message": "payment code conflict" } ``` **Conflict (409)** ```json { "success": false, "code": "unique_amount_conflict", "message": "unique amount conflict" } ``` ## Response Fields | Field | Tipe | Deskripsi | |-------|------|-----------| | `payment_id` | uuid | ID unik invoice | | `payment_code` | string | Kode invoice dari sistem kamu | | `payment_amount` | int64 | Nominal asli (tanpa nominal unik) | | `payment_unique` | int64 | Nominal unik (1-999) | | `payment_total` | int64 | **Total yang harus dibayar** = amount + unique | | `payment_status` | string | `PENDING` | | `payment_expired_at` | datetime | Batas waktu pembayaran | | `payment_updated_at` | datetime | Waktu update terakhir | | `payment_webhook_url` | string/null | Webhook endpoint invoice ini | | `payment_checkout_url` | string | URL halaman checkout publik | | `payment_return_url` | string/null | Redirect URL | | `customer_name` | string | Nama customer | | `customer_email` | string/null | Email customer | | `customer_phone` | string/null | Telepon customer | | `customer_orders` | array | Item pesanan | | `account_id` | uuid | ID akun tujuan | | `account_number` | string | Nomor rekening tujuan | | `account_name` | string | Nama pemilik rekening | | `channel_id` | uuid | ID channel bank | | `channel_name` | string | Nama bank | | `channel_type` | string | `TRANSFER` / `QRIS` | | `channel_instructions` | array | Instruksi pembayaran | | `is_manual_match` | bool | Dicocokkan manual? | | `manual_matched_mutation_id` | uuid/null | ID mutasi terkait | ## Simpan di Sistem Kamu Setelah create payment, simpan field berikut: | Field | Gunakan Untuk | |-------|---------------| | `payment_id` | Referensi unik invoice | | `payment_code` | Kode invoice kamu | | `payment_total` | Nominal yang harus dibayar customer | | `payment_status` | Status awal: `PENDING` | | `payment_checkout_url` | Redirect customer (opsional) | | `payment_webhook_url` | Webhook yang akan dikirimi notifikasi | | `account_number` | Nomor rekening tujuan | | `channel_instructions` | Instruksi pembayaran | ## Error | Code | Status | Artinya | |------|--------|---------| | `invalid request body` | 400 | Format JSON salah | | `validation_error` | 400 | Field tidak valid (cek `errors`) | | `invalid_expired_at` | 400 | Format `payment_expired_at` salah | | `account_not_owned` | 403 | `account_id` bukan milik Anda | | `no_active_quota` | 403 | Kuota akun habis | | `tenant_api_key_required` | 403 | API Key tidak valid | | `unique_amount_conflict` | 409 | Gagal generate nominal unik, coba lagi | | `payment_code_conflict` | 409 | `payment_code` sudah dipakai (idempotency) | | `internal_error` | 500 | Server error, coba lagi | **Lanjutan:** Invoice sudah dibuat! Arahkan customer ke [Checkout](./checkout) atau tampilkan detail pembayaran di UI kamu. --- # Checkout Halaman publik Bayar Digital untuk customer melakukan pembayaran. Customer **tidak perlu login** atau punya akun. :::info Setelah invoice dibuat, kamu bisa redirect customer ke halaman checkout ini. `payment_checkout_url` diberikan saat create payment sebagai URL absolut penuh. **Alternatif:** Kalau tidak ingin redirect, kamu bisa tampilkan detail pembayaran (nomor rekening, nominal total, instruksi) di UI kamu sendiri. Ambil data dari response `POST /gateway/payments` atau `GET /gateway/payments/{code}`, lalu pantau status via [Webhook](./webhook). ::: ## Alur Redirect ```mermaid sequenceDiagram participant S as Server Kamu participant B as Bayar Digital participant C as Customer S->>B: POST /gateway/payments B-->>S: checkout_url S->>C: Redirect ke checkout_url C->>B: Buka halaman checkout C->>C: Transfer / QRIS B->>S: Webhook: PAID B-->>C: Redirect ke return_url ``` ### Yang Customer Lihat | Status | Tampilan | |--------|----------| | `PENDING` | Instruksi pembayaran, nominal total, batas waktu mundur | | `PAID` | Konfirmasi sukses + tombol kembali ke merchant | | `EXPIRED` / `CANCELLED` | Halaman tidak tersedia | **Transfer bank:** menampilkan nomor rekening dan nominal `payment_total`. **QRIS:** menampilkan QR Code dinamis dengan nominal spesifik. ### return_url - Customer otomatis redirect ke `return_url` setelah status `PAID` - Parameter `?payment_code={payment_code}` otomatis ditambahkan - Hanya HTTPS yang diizinkan :::warning **Jangan** jadikan redirect sebagai sumber kebenaran. Gunakan **webhook** untuk memastikan status payment. ::: ## Public API Halaman checkout menggunakan endpoint publik (tanpa autentikasi). | Method | URL | |--------|-----| | `GET` | `/checkout/{payment_id}` | | Parameter | Tipe | Wajib | Deskripsi | |-----------|------|-------|-----------| | `payment_id` | uuid | Ya | ID invoice (dari `payment_checkout_url`) | Endpoint publik — tidak perlu header. Tidak ada request body (GET). ```bash curl https://api.bayar.digital/checkout/660e8400-e29b-41d4-a716-446655440010 ``` **Transfer Bank:** ```json { "success": true, "message": "ok", "data": { "payment_code": "INV-2026-0001", "amount_original": 50000, "amount_unique": 123, "amount_total": 50123, "status": "PENDING", "expires_at": "2026-10-11T12:00:00Z", "created_at": "2026-06-11T10:00:00Z", "customer_name": "Budi Santoso", "customer_email": "budi@example.com", "customer_phone": "081234567890", "return_url": "https://yourserver.com/orders/INV-2026-0001", "redirect_url": "https://yourserver.com/orders/INV-2026-0001?payment_code=INV-2026-0001", "order_items": "[{\"name\":\"Produk A\",\"price\":50000,\"quantity\":1,\"subtotal\":50000}]", "account_number": "1234567890", "account_name": "PT Merchant Contoh", "bank_name": "BCA", "bank_type": "TRANSFER", "app_name": "BCA Mobile", "instructions": "[]" } } ``` **QRIS — Field Tambahan:** | Field | Tipe | Deskripsi | |-------|------|-----------| | `qris_id` | string/null | NMID merchant | | `qris_name` | string/null | Nama merchant | | `qris_city` | string/null | Kota | | `qris_payload` | string/null | QRIS content string (untuk generate QR Code) | :::info `qris_payload` bersifat **dinamis** dengan nominal spesifik. Static QR tidak pernah diekspos. ::: ```json { "success": false, "code": "not_found", "message": "payment not found" } ``` ## Response Fields | Field | Tipe | Deskripsi | |-------|------|-----------| | `payment_code` | string | Kode invoice | | `amount_original` | int64 | Nominal asli (tanpa nominal unik) | | `amount_unique` | int64 | Nominal unik | | `amount_total` | int64 | Total yang harus dibayar | | `status` | string | Status payment | | `expires_at` | datetime | Batas waktu pembayaran | | `created_at` | datetime | Waktu pembuatan | | `customer_name` | string | Nama customer | | `customer_email` | string/null | Email customer | | `customer_phone` | string/null | Telepon customer | | `return_url` | string/null | Redirect URL | | `redirect_url` | string/null | URL redirect dengan parameter `payment_code` | | `order_items` | string | Item pesanan (JSON string) | | `account_number` | string | Nomor rekening tujuan | | `account_name` | string | Nama pemilik rekening | | `bank_name` | string | Nama bank | | `bank_type` | string | `TRANSFER` | | `app_name` | string | Nama aplikasi mobile banking | | `instructions` | string | Instruksi pembayaran (JSON string) | **Lanjutan:** Pantau status pembayaran via [Payment Get](./payment-get) atau setup [Webhook](./webhook) untuk notifikasi otomatis. --- # Payment Get Mengecek status pembayaran atau rekonsiliasi order. :::info Gunakan endpoint ini untuk mengecek status invoice kapan saja. Untuk update status real-time, setup [Webhook](./webhook). Lihat [Status & Error Code](./status-code) untuk daftar lengkap status payment. ::: ## Get by Payment Code Ambil detail satu invoice berdasarkan `payment_code` dari sistem kamu. | Method | URL | |--------|-----| | `GET` | `/gateway/payments/{payment_code}` | | Parameter | Tipe | Wajib | Deskripsi | |-----------|------|-------|-----------| | `payment_code` | string | Ya | Kode invoice dari sistem kamu | | Header | Wajib | Deskripsi | |--------|-------|-----------| | `X-Api-Key` | Ya | API Key merchant | Tidak ada request body (GET). ```bash curl https://api.bayar.digital/gateway/payments/INV-2026-0001 \ -H "X-Api-Key: pk_..." ``` ```json { "success": true, "message": "ok", "data": { "payment_id": "660e8400-e29b-41d4-a716-446655440010", "payment_code": "INV-2026-0001", "payment_amount": 50000, "payment_unique": 123, "payment_total": 50123, "payment_status": "PAID", "payment_expired_at": "2026-10-11T12:00:00Z", "payment_updated_at": "2026-06-11T10:05:00Z", "payment_webhook_url": "https://yourserver.com/webhooks/bayar", "payment_checkout_url": "https://bayar.digital/checkout/660e8400-e29b-41d4-a716-446655440010", "payment_return_url": "https://yourserver.com/orders/INV-2026-0001", "customer_name": "Budi Santoso", "customer_email": "budi@example.com", "customer_phone": "081234567890", "customer_orders": [ { "name": "Produk A", "price": 50000, "quantity": 1, "subtotal": 50000 } ], "account_id": "550e8400-e29b-41d4-a716-446655440000", "account_number": "1234567890", "account_name": "PT Merchant Contoh", "channel_id": "660e8400-e29b-41d4-a716-446655440001", "channel_name": "Bank Central Asia", "channel_type": "TRANSFER", "channel_instructions": [], "is_manual_match": false, "manual_matched_mutation_id": null } } ``` ```json { "success": false, "code": "not_found", "message": "payment not found" } ``` ### Response Fields | Field | Tipe | Deskripsi | |-------|------|-----------| | `payment_id` | uuid | ID unik invoice | | `payment_code` | string | Kode invoice dari sistem kamu | | `payment_amount` | int64 | Nominal asli (tanpa nominal unik) | | `payment_unique` | int64 | Nominal unik (1-999) | | `payment_total` | int64 | **Total yang harus dibayar** = amount + unique | | `payment_status` | string | `PENDING` / `PAID` / `EXPIRED` / `CANCELLED` | | `payment_expired_at` | datetime | Batas waktu pembayaran | | `payment_updated_at` | datetime | Waktu update terakhir | | `payment_webhook_url` | string/null | Webhook endpoint invoice ini | | `payment_checkout_url` | string | URL halaman checkout publik | | `payment_return_url` | string/null | Redirect URL | | `customer_name` | string/null | Nama customer | | `customer_email` | string/null | Email customer | | `customer_phone` | string/null | Telepon customer | | `customer_orders` | array/null | Item pesanan | | `account_id` | uuid | ID akun tujuan | | `account_number` | string/null | Nomor rekening / QRIS URL | | `account_name` | string/null | Nama pemilik rekening | | `channel_id` | uuid/null | ID channel | | `channel_name` | string/null | Nama bank | | `channel_type` | string/null | `TRANSFER` / `QRIS` | | `channel_instructions` | array | Instruksi pembayaran | | `is_manual_match` | bool | Dicocokkan manual? | | `manual_matched_mutation_id` | uuid/null | ID mutasi terkait (jika manual match) | ### Error | Code | Status | Artinya | |------|--------|---------| | `tenant_api_key_required` | 403 | API Key tidak valid | | `not_found` | 404 | `payment_code` tidak ditemukan | | `internal_error` | 500 | Server error, coba lagi | ## List Payments Ambil daftar invoice, diurutkan dari terbaru. | Method | URL | |--------|-----| | `GET` | `/gateway/payments` | | Parameter | Tipe | Default | Maks | Deskripsi | |-----------|------|---------|------|-----------| | `page` | integer | 1 | — | Halaman | | `per_page` | integer | 20 | 100 | Data per halaman | | Header | Wajib | Deskripsi | |--------|-------|-----------| | `X-Api-Key` | Ya | API Key merchant | Tidak ada request body (GET). ```bash curl "https://api.bayar.digital/gateway/payments?page=1&per_page=20" \ -H "X-Api-Key: pk_..." ``` ```json { "success": true, "message": "ok", "data": [ { "payment_id": "660e8400-e29b-41d4-a716-446655440010", "payment_code": "INV-2026-0001", "payment_amount": 50000, "payment_unique": 123, "payment_total": 50123, "payment_status": "PAID", "payment_expired_at": "2026-10-11T12:00:00Z", "payment_updated_at": "2026-06-11T10:05:00Z", "payment_webhook_url": "https://yourserver.com/webhooks/bayar", "payment_checkout_url": "https://bayar.digital/checkout/660e8400-e29b-41d4-a716-446655440010", "payment_return_url": "https://yourserver.com/orders/INV-2026-0001", "customer_name": "Budi Santoso", "customer_email": "budi@example.com", "customer_phone": "081234567890", "customer_orders": [], "account_id": "550e8400-e29b-41d4-a716-446655440000", "account_number": "1234567890", "account_name": "PT Merchant Contoh", "channel_id": "660e8400-e29b-41d4-a716-446655440001", "channel_name": "Bank Central Asia", "channel_type": "TRANSFER", "channel_instructions": [], "is_manual_match": false, "manual_matched_mutation_id": null } ], "pagination": { "total": 50, "count": 1, "per_page": 20, "current_page": 1, "total_pages": 3 } } ``` ```json { "success": false, "code": "internal_error", "message": "internal server error" } ``` ### Response Fields Sama dengan [Get by Payment Code](#response-fields) — data dalam bentuk array + objek `pagination`. | Field Pagination | Tipe | Deskripsi | |-----------------|------|-----------| | `total` | int | Total data | | `count` | int | Data di halaman ini | | `per_page` | int | Data per halaman | | `current_page` | int | Halaman saat ini | | `total_pages` | int | Total halaman | ### Error | Code | Status | Artinya | |------|--------|---------| | `tenant_api_key_required` | 403 | API Key tidak valid | | `internal_error` | 500 | Server error, coba lagi | **Lanjutan:** Untuk operasi lain, lihat [Payment Cancel](./payment-cancel) atau [Payment Match](./payment-match). --- # Payment Cancel Membatalkan invoice yang masih `PENDING`. Status berubah menjadi `CANCELLED`. :::info Hanya invoice dengan status `PENDING` yang bisa dibatalkan. Invoice `PAID` / `EXPIRED` / `CANCELLED` tidak bisa dibatalkan. Jika customer masih perlu bayar, buat invoice baru dengan `payment_code` baru. ::: | Method | URL | |--------|-----| | `DELETE` | `/gateway/payments/{payment_code}` | | Parameter | Tipe | Wajib | Deskripsi | |-----------|------|-------|-----------| | `payment_code` | string | Ya | Kode invoice dari sistem kamu | | Header | Wajib | Deskripsi | |--------|-------|-----------| | `X-Api-Key` | Ya | API Key merchant | Tidak ada request body (DELETE). ```bash curl -X DELETE https://api.bayar.digital/gateway/payments/INV-2026-0001 \ -H "X-Api-Key: pk_..." ``` ```json { "success": true, "message": "ok", "data": null } ``` ```json { "success": false, "code": "payment_not_cancellable", "message": "payment not found or not pending" } ``` ## Response Fields | Field | Tipe | Deskripsi | |-------|------|-----------| | `success` | bool | `true` | | `message` | string | `"ok"` | | `data` | null | Selalu `null` | ## Error | Code | Status | Artinya | |------|--------|---------| | `tenant_api_key_required` | 403 | API Key tidak valid | | `payment_not_cancellable` | 404 | Invoice tidak ditemukan atau bukan `PENDING` | | `internal_error` | 500 | Server error, coba lagi | **Lanjutan:** Jika customer sudah transfer tapi tidak terdeteksi otomatis, lihat [Payment Match](./payment-match) untuk mencocokkan manual. --- # Payment Match Mencocokkan invoice `PENDING` menjadi `PAID` secara manual. :::info Gunakan jika customer sudah transfer tapi Worker tidak mendeteksi otomatis. Fitur ini memerlukan **2FA (TOTP)** yang diaktifkan di Dashboard → **Akun Saya** → **2FA**. Scan QR code dengan Google Authenticator, lalu gunakan kode 6 digit yang muncul. ::: | Method | URL | |--------|-----| | `POST` | `/gateway/payments/{payment_code}/match` | | Parameter | Tipe | Wajib | Deskripsi | |-----------|------|-------|-----------| | `payment_code` | string | Ya | Kode invoice yang akan dicocokkan | | Header | Wajib | Deskripsi | |--------|-------|-----------| | `X-Api-Key` | Ya | API Key merchant | | `X-Totp-Code` | Ya | Kode 6 digit dari Google Authenticator | | Field | Tipe | Wajib | Deskripsi | |-------|------|-------|-----------| | `mutation_id` | uuid | Tidak | ID mutasi yang ingin dikaitkan (jika ada) | ```bash curl -X POST https://api.bayar.digital/gateway/payments/INV-2026-0001/match \ -H "X-Api-Key: pk_..." \ -H "X-Totp-Code: 123456" \ -H "Content-Type: application/json" \ -d '{ "mutation_id": "550e8400-e29b-41d4-a716-446655440020" }' ``` ```json { "mutation_id": "550e8400-e29b-41d4-a716-446655440020" } ``` ```json { "success": true, "message": "ok", "data": null } ``` ```json { "success": false, "code": "invalid totp code", "message": "invalid totp code" } ``` ```json { "success": false, "code": "totp_not_enabled", "message": "totp not enabled" } ``` ## Response Fields | Field | Tipe | Deskripsi | |-------|------|-----------| | `success` | bool | `true` | | `message` | string | `"ok"` | | `data` | null | Selalu `null` | ## Error | Code | Status | Artinya | |------|--------|---------| | `X-Totp-Code header is required` | 400 | Header TOTP tidak disertakan | | `invalid totp code` | 400 | Kode TOTP salah | | `tenant_api_key_required` | 403 | API Key tidak valid | | `totp_not_enabled` | 403 | 2FA belum diaktifkan di akun Anda | | `not_found` | 404 | Invoice tidak ditemukan atau bukan `PENDING` | | `internal_error` | 500 | Server error, coba lagi | **Lanjutan:** Untuk melihat mutasi transfer yang terdeteksi, lihat [Payment Mutations](./payment-mutations). --- # Payment Mutations Mendapatkan daftar mutasi/transaksi masuk yang terdeteksi oleh Android Worker dari rekening Anda. :::info Data mutasi berguna untuk rekonsiliasi dan mencocokkan payment secara manual jika otomatis gagal. **Cara kerja:** Worker mendeteksi notifikasi transfer → kirim ke server → sistem otomatis cocokkan dengan invoice `PENDING` berdasarkan nominal. Jika cocok, invoice jadi `PAID`. Jika tidak, mutasi tetap `unmatched` dan bisa dicocokkan manual via [Payment Match](./payment-match). Gunakan parameter `only_unmatched=true` untuk melihat mutasi yang belum otomatis cocok. ::: | Method | URL | |--------|-----| | `GET` | `/gateway/mutations` | | Parameter | Tipe | Default | Deskripsi | |-----------|------|---------|-----------| | `page` | integer | 1 | Halaman | | `per_page` | integer | 20 | Data per halaman (max 100) | | `only_unmatched` | bool | `false` | Tampilkan hanya yang **belum** cocok | | Header | Wajib | Deskripsi | |--------|-------|-----------| | `X-Api-Key` | Ya | API Key merchant | Tidak ada request body (GET). ```bash curl "https://api.bayar.digital/gateway/mutations?only_unmatched=true" \ -H "X-Api-Key: pk_..." ``` ```json { "success": true, "message": "ok", "data": [ { "id": "550e8400-e29b-41d4-a716-446655440020", "device_id": "660e8400-e29b-41d4-a716-446655440030", "title": "Transfer dari BUDI SANTOSO", "body": "BCA ke 1234567890 a/n PT Merchant Contoh", "posted_at": "2026-06-11T10:30:00Z", "amount_parsed": 50123, "sender_parsed": "BUDI SANTOSO", "type_parsed": "CREDIT", "currency_parsed": "IDR", "is_matched": false, "matched_payment_id": null, "matched_payment_code": null, "created_at": "2026-06-11T10:30:05Z", "device_name": "HP Kantor", "package_name": "com.bca", "app_name": "BCA Mobile", "app_version": "6.2.0", "account_number": "1234567890", "account_name": "PT Merchant Contoh", "bank_name": "Bank Central Asia", "bank_type": "TRANSFER" } ], "pagination": { "total": 50, "count": 1, "per_page": 20, "current_page": 1, "total_pages": 3 } } ``` ```json { "success": false, "code": "internal_error", "message": "internal server error" } ``` ## Response Fields | Field | Tipe | Deskripsi | |-------|------|-----------| | `id` | uuid | ID mutasi | | `device_id` | uuid | ID Android Worker yang mendeteksi | | `title` | string/null | Judul notifikasi transfer | | `body` | string/null | Detail notifikasi | | `posted_at` | datetime | Waktu transaksi | | `amount_parsed` | int64 | Nominal transfer | | `sender_parsed` | string/null | Nama pengirim (terdeteksi otomatis) | | `type_parsed` | string/null | `CREDIT` (masuk) / `DEBIT` (keluar) | | `currency_parsed` | string/null | Mata uang (contoh: `IDR`) | | `is_matched` | bool | Apakah sudah dipasangkan ke invoice | | `matched_payment_id` | uuid/null | ID invoice yang cocok | | `matched_payment_code` | string/null | Kode invoice yang cocok | | `created_at` | datetime | Waktu terdeteksi oleh sistem | | `device_name` | string/null | Nama perangkat Worker | | `package_name` | string/null | Package name aplikasi bank | | `app_name` | string/null | Nama aplikasi bank | | `app_version` | string/null | Versi aplikasi bank | | `account_number` | string/null | Rekening tujuan | | `account_name` | string/null | Nama pemilik rekening | | `bank_name` | string/null | Nama bank | | `bank_type` | string/null | `TRANSFER` / `QRIS` | ## Error | Code | Status | Artinya | |------|--------|---------| | `tenant_api_key_required` | 403 | API Key tidak valid | | `internal_error` | 500 | Server error, coba lagi | **Lanjutan:** Setup [Webhook](./webhook) untuk notifikasi real-time saat status payment berubah. --- # Channel Instructions Mendapatkan panduan/langkah-langkah pembayaran untuk channel bank tertentu. Informasi ini bisa kamu tampilkan ke customer. :::info Setiap channel bank bisa punya instruksi berbeda. Gunakan `channel_id` dari response [Payment Account](./payment-account) atau [Payment Create](./payment-create) untuk mengambil panduan langkah demi langkah. ::: | Method | URL | |--------|-----| | `GET` | `/gateway/channels/{channel_id}/instructions` | | Parameter | Tipe | Wajib | Deskripsi | |-----------|------|-------|-----------| | `channel_id` | uuid | Ya | ID channel (dari response `GET /gateway/accounts`) | | Header | Wajib | Deskripsi | |--------|-------|-----------| | `X-Api-Key` | Ya | API Key merchant | Tidak ada request body (GET). ```bash curl https://api.bayar.digital/gateway/channels/660e8400-e29b-41d4-a716-446655440001/instructions \ -H "X-Api-Key: pk_..." ``` ```json { "success": true, "message": "ok", "data": { "channel_id": "660e8400-e29b-41d4-a716-446655440001", "instructions": [ { "step": 1, "title": "Buka aplikasi BCA Mobile", "content": "Login ke aplikasi BCA Mobile Anda" }, { "step": 2, "title": "Pilih m-Transfer", "content": "Pilih menu m-Transfer > ke Rekening BCA Virtual Account" }, { "step": 3, "title": "Masukkan nominal", "content": "Transfer sesuai total yang tertera" } ] } } ``` ```json { "success": false, "code": "channel_id is required", "message": "channel id is required" } ``` ## Response Fields | Field | Tipe | Deskripsi | |-------|------|-----------| | `channel_id` | uuid | ID channel | | `instructions` | array | Daftar langkah pembayaran | ### Instruction Item | Field | Tipe | Deskripsi | |-------|------|-----------| | `step` | int | Urutan langkah | | `title` | string | Judul langkah | | `content` | string | Penjelasan langkah | ## Error | Code | Status | Artinya | |------|--------|---------| | `channel_id is required` | 400 | Parameter `channel_id` kosong | | `tenant_api_key_required` | 403 | API Key tidak valid | | `internal_error` | 500 | Server error, coba lagi | **Lanjutan:** Setup [Webhook](./webhook) untuk notifikasi otomatis saat payment berubah status. --- # Webhook Bayar Digital mengirim **HTTP POST** ke server kamu saat status payment berubah. :::info **Konteks:** Ini adalah cara utama untuk mendapat notifikasi real-time. Webhook lebih cepat dan andal daripada polling manual. ::: ## Setup Webhook URL dikonfigurasi di Dashboard Tenant pada menu **Merchant**. Kamu bisa: 1. **Webhook URL default** di level Merchant — semua notifikasi dikirim ke URL ini 2. **Override per invoice** — kirim `payment_webhook_url` saat `POST /gateway/payments` Selain URL, kamu juga bisa mengatur **Webhook Secret** untuk verifikasi signature. ## Event Webhook dikirim pada event berikut: | Event | Status Baru | Keterangan | |-------|-------------|------------| | Customer bayar | `PAID` | Terdeteksi otomatis oleh sistem | | Manual match | `PAID` | Kamu cocokkan manual via API/Dashboard | | Kedaluwarsa | `EXPIRED` | Melewati `payment_expired_at` | | Dibatalkan | `CANCELLED` | Kamu batalkan via API/Dashboard | ## Payload ```json { "payment_id": "660e8400-e29b-41d4-a716-446655440010", "payment_code": "INV-2026-0001", "status": "PAID", "amount": 50123, "paid_at": "2026-06-11T10:05:00Z" } ``` ### Field | Field | Tipe | Deskripsi | |-------|------|-----------| | `payment_id` | uuid | ID invoice | | `payment_code` | string | Kode invoice dari sistem kamu | | `status` | string | `PAID` / `EXPIRED` / `CANCELLED` | | `amount` | int64 | **Total** yang dibayar (original + nominal unik) | | `paid_at` | datetime | Waktu bayar (ISO 8601). Ada hanya jika `PAID` | ### Contoh per Status **PAID:** ```json { "payment_id": "660e8400-e29b-41d4-a716-446655440010", "payment_code": "INV-2026-0001", "status": "PAID", "amount": 50123, "paid_at": "2026-06-11T10:05:00Z" } ``` **EXPIRED:** ```json { "payment_id": "660e8400-e29b-41d4-a716-446655440010", "payment_code": "INV-2026-0001", "status": "EXPIRED", "amount": 50123 } ``` **CANCELLED:** ```json { "payment_id": "660e8400-e29b-41d4-a716-446655440010", "payment_code": "INV-2026-0001", "status": "CANCELLED", "amount": 50123 } ``` ## Signature Verification Jika kamu mengisi **Webhook Secret** di Dashboard, setiap request webhook akan menyertakan header: ``` X-Signature: ``` Signature dihitung sebagai **HMAC-SHA256** dari *raw JSON body* menggunakan Webhook Secret. ### Cara Verifikasi ```javascript const crypto = require('crypto'); function verifyWebhook(body, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(JSON.stringify(body)) .digest('hex'); return expected === signature; } ``` ```php function verifyWebhook($body, $signature, $secret) { $expected = hash_hmac('sha256', json_encode($body), $secret); return hash_equals($expected, $signature); } ``` ```python import hmac, hashlib, json def verify_webhook(body, signature, secret): expected = hmac.new( secret.encode(), json.dumps(body, separators=(',', ':')).encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) ``` ## Handler Requirements Endpoint webhook kamu harus: 1. **Verifikasi signature** — jika menggunakan Webhook Secret 2. **Validasi payment_code** — pastikan terdaftar di sistem kamu 3. **Validasi amount** — cocokkan dengan `payment_total` yang kamu simpan 4. **Idempotent** — aman dipanggil berkali-kali 5. **Balas 200 OK** — secepat mungkin :::tip Jika proses update order butuh waktu lama, simpan payload ke antrean dulu, balas `200 OK`, lalu proses async. ::: ### Contoh Lengkap (Node.js) ```javascript app.post('/webhooks/bayar', express.json(), (req, res) => { // 1. Verifikasi signature if (process.env.WEBHOOK_SECRET) { const expected = crypto .createHmac('sha256', process.env.WEBHOOK_SECRET) .update(JSON.stringify(req.body)) .digest('hex'); if (req.headers['x-signature'] !== expected) { return res.status(401).json({ status: 'invalid signature' }); } } const { payment_id, payment_code, status, amount } = req.body; // 2. Cari order di DB // 3. Validasi amount // 4. Update status (idempotent) res.json({ status: 'received' }); }); ``` ### Contoh Lengkap (PHP) ```php Route::post('webhooks/bayar', function (Request $request) { $secret = config('services.bayar.webhook_secret'); if ($secret) { $expected = hash_hmac('sha256', $request->getContent(), $secret); if (!hash_equals($expected, $request->header('X-Signature', ''))) { return response()->json(['status' => 'invalid signature'], 401); } } $data = $request->validate([ 'payment_code' => 'required|string', 'status' => 'required|in:PAID,EXPIRED,CANCELLED', 'amount' => 'required|integer', ]); // Cari order, validasi amount, update status (idempotent) return response()->json(['status' => 'received']); }); ``` ### Contoh Lengkap (Python) ```python @app.post("/webhooks/bayar") async def webhook(request: Request): body = await request.body() secret = os.environ.get("WEBHOOK_SECRET") if secret: expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, request.headers.get("x-signature", "")): raise HTTPException(401, "invalid signature") data = await request.json() # Cari order, validasi amount, update status (idempotent) return {"status": "received"} ``` ## Retry Logic Jika endpoint kamu tidak membalas `2xx`, sistem akan retry secara otomatis: | Retry ke- | Jeda | |-----------|------| | 1 | 60 detik | | 2 | 120 detik | | 3 | 240 detik | | 4 | 480 detik | | 5 | 960 detik | Setelah 5 kali gagal, webhook dianggap **FAILED** dan berhenti di-retry otomatis. Kamu bisa kirim ulang manual dari Dashboard. ## Idempotency Webhook bisa terkirim lebih dari sekali. Pastikan handler kamu aman: - Jika order sudah `PAID` → abaikan webhook `PAID` berikutnya - Jangan ubah status `PAID` kembali ke `PENDING` atau status lain - Log semua payload untuk audit - Gunakan `payment_id` sebagai key idempotent ## Troubleshooting ### Webhook selalu gagal? | Kemungkinan | Solusi | |-------------|--------| | URL tidak bisa diakses dari internet | Pastikan endpoint kamu publik | | Response bukan 2xx | Balas `200 OK` setelah terima | | Timeout > 15 detik | Simpan ke queue, balas cepat | | SSL certificate tidak valid | Pastikan HTTPS valid | ### Cara cek riwayat webhook Gunakan endpoint di Dashboard: | Method | Endpoint | Deskripsi | |--------|----------|-----------| | `GET` | `/tenant/callbacks` | Riwayat semua webhook | | `GET` | `/tenant/callbacks/:id` | Detail webhook | | `POST` | `/tenant/callbacks/:id/retry` | Kirim ulang webhook | **Lanjutan:** Lihat [Status & Error Code](./status-code) untuk referensi lengkap kode error dan retry strategy. --- # Status & Error Code ## HTTP Status | Status | Arti | |--------|------| | `200` | OK | | `201` | Created (create payment) | | `400` | Bad request / validation error | | `401` | API Key kosong atau tidak valid | | `403` | Tidak punya akses | | `404` | Tidak ditemukan | | `409` | Conflict (duplicate, cancel gagal) | | `429` | Rate limit | | `500` | Server error | ## Status Payment | Status | Arti | Terminal | |--------|------|----------| | `PENDING` | Menunggu pembayaran | Tidak | | `PAID` | Terkonfirmasi lunas | Ya | | `EXPIRED` | Melewati batas waktu | Ya | | `CANCELLED` | Dibatalkan merchant | Ya | ## Status Webhook | Status | Arti | |--------|------| | `PENDING` | Menunggu dikirim | | `RETRYING` | Gagal, akan di-retry | | `SUCCESS` | Berhasil (response 2xx dari server kamu) | | `FAILED` | Gagal total (melebihi maks retry) | ## Error Code | Code | HTTP | Penyebab | Solusi | |------|------|----------|--------| | `unauthorized` | 401 | API Key kosong / tidak valid | Kirim `X-Api-Key` yang benar | | `tenant_api_key_required` | 403 | API Key bukan merchant tenant | Gunakan API Key dari merchant | | `bad_request` | 400 | JSON / body tidak valid | Perbaiki format request | | `validation_error` | 400 | Field tidak valid | Cek `errors` di response | | `invalid_expired_at` | 400 | Format `payment_expired_at` salah / sudah lewat | Kirim ISO 8601 di masa depan | | `not_found` | 404 | Payment tidak ditemukan | Cek `payment_code` | | `payment_not_cancellable` | 404 | Payment bukan `PENDING` | Cek status sebelum cancel | | `account_not_owned` | 403 | `account_id` bukan milik merchant | Ambil dari `GET /gateway/accounts` | | `no_active_quota` | 403 | Kuota akun habis | Top up di Dashboard | | `totp_not_enabled` | 403 | 2FA belum diaktifkan | Setup 2FA di pengaturan profil | | `payment_code_conflict` | 409 | `payment_code` sudah dipakai | Gunakan kode unik | | `unique_amount_conflict` | 409 | Gagal buat nominal unik | Retry beberapa detik lagi | | `rate_limited` | 429 | Terlalu banyak request | Tunggu sesuai `X-RateLimit-Reset` | | `internal_error` | 500 | Server error | Retry dengan backoff | ## Error Handling ### Retry yang Aman Hanya retry untuk error **sementara**: `429` (rate limit) dan `500` (server error). ```javascript async function callAPI(request, maxRetries = 3) { for (let i = 0; i <= maxRetries; i++) { const res = await request(); if (res.status !== 429 && res.status < 500) return res; const wait = parseInt(res.headers.get('X-RateLimit-Reset') || i + 1, 10); await new Promise(r => setTimeout(r, wait * 1000)); } throw new Error('request failed after retries'); } ``` ```php function callAPI(callable $request, int $maxRetries = 3): array { for ($i = 0; $i <= $maxRetries; $i++) { $res = $request(); if ($res['status'] !== 429 && $res['status'] < 500) return $res; $wait = $res['headers']['X-RateLimit-Reset'] ?? ($i + 1); sleep((int) $wait); } throw new \RuntimeException('request failed after retries'); } ``` ```python import time def call_api(request, max_retries=3): for i in range(max_retries + 1): res = request() if res.status_code != 429 and res.status_code < 500: return res wait = int(res.headers.get('X-RateLimit-Reset', i + 1)) time.sleep(wait) raise Exception('request failed after retries') ``` ### Error yang JANGAN di-retry otomatis | Error | Alasan | Tindakan | |-------|--------|----------| | `payment_code_conflict` (409) | `payment_code` sudah dipakai | Gunakan kode baru | | `validation_error` (400) | Field tidak valid | Perbaiki request | | `account_not_owned` (403) | `account_id` salah | Ambil dari `GET /gateway/accounts` | | `no_active_quota` (403) | Kuota habis | Top up di Dashboard | | `payment_not_cancellable` (404) | Status bukan `PENDING` | Cek status dulu | | `unique_amount_conflict` (409) | Nominal unik bentrok | Bisa retry setelah beberapa detik | ## Ringkasan | Error | Retry? | Aksi | |-------|--------|------| | `429` Rate limit | Ya | Tunggu `X-RateLimit-Reset` | | `500` Server error | Ya | Backoff | | `400` Validation | Tidak | Perbaiki request | | `409` Duplicate code | Tidak | Pakai kode baru | | `409` Unique amount | Ya | Tunggu & retry | | `404` Not cancellable | Tidak | Cek status dulu | | `403` Account | Tidak | Ambil dari `GET /gateway/accounts` | | `401` Unauthorized | Tidak | Periksa API Key | **Ini halaman terakhir dokumentasi.** Jika ada masalah, hubungi tim support Bayar Digital. --- _Generated from bayar.digital documentation. Updated 2026-06-17._