Dokumentasi API doit.id
Terima pembayaran QRIS dan Virtual Account dengan satu API sederhana. Tanpa signature rumit, tanpa menunggu: daftar, ambil API key, dan transaksi uji pertamamu jalan dalam hitungan menit.
Mulai dalam 10 menit
1. Buat akun di doit.id/daftar (dashboard sandbox aktif
begitu email dan nomor WhatsApp terverifikasi), lalu masuk ke
portal → menu Developer → buat aplikasi →
salin API key (pb_test_…).
2. Buat pembayaran pertama:
curl -X POST https://pay.doit.id/v1/payments \
-H "Authorization: Bearer pb_test_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: inv-001" \
-d '{"amount": 150000, "rail": "qris", "reference": "INV/001"}'
Respons berisi qr_content: render sebagai gambar QR (mis. npm
qrcode) dan tunjukkan ke pembayar.
3. Set webhook URL di menu Developer, lalu buka pembayaran tadi di
portal, menu Transaksi: klik baris pembayarannya,
panel rincian terbuka di kanan, lalu klik Simulasikan lunas (sandbox). Tombol ini ada di portal,
bukan di halaman bayar yang dilihat pelanggan. Webhook
payment.paid tiba di server-mu. Integrasi selesai.
Autentikasi
Semua request memakai API key di header Authorization:
Authorization: Bearer pb_test_xxx
API key diterbitkan per aplikasi di portal, tampil sekali saat dibuat. Tidak ada signature, timestamp, atau header tambahan; cukup key ini lewat HTTPS.
Test vs live
Environment ditentukan oleh prefix key, bukan base URL, jadi kode integrasi tidak berubah:
| Key | Perilaku |
|---|---|
pb_test_… | Sandbox: transaksi tidak nyata, bisa dilunasi lewat tombol simulasi di portal. |
pb_live_… | Produksi: dana riil, tersedia setelah verifikasi usaha disetujui. |
Idempotency
Semua POST wajib menyertakan header Idempotency-Key. Pakai ID unik
dari sistemmu (mis. nomor invoice). Request ulang dengan key sama mengembalikan hasil pertama
(HTTP 200), tidak pernah membuat transaksi ganda. Timeout? Kirim ulang saja dengan key
yang sama.
Rate limit
120 request per menit per API key. Terlampaui → HTTP 429 dengan header
Retry-After (detik). Hormati nilainya, lalu ulangi request yang sama
(aman, karena idempotency).
Membuat pembayaran
POST /v1/payments
| Field | Tipe | Keterangan |
|---|---|---|
amount | integer, wajib | Rupiah utuh: 150000 = Rp150.000. Tanpa desimal. |
rail | string | "qris", "va", "transfer", atau "any" (default bila dikosongkan). Dengan "any", pelanggan memilih metodenya sendiri di halaman bayar hosted, lengkap dengan biayanya per metode. |
reference | string, wajib | ID tagihan di sistemmu (≤128 karakter). Dikembalikan di webhook. |
va_bank | string | Wajib bila rail=va atau rail=transfer (bca/mandiri/bni/bsi); lihat tabel kode bank di bawah. |
expires_in | integer | Masa berlaku tagihan (detik), ≥60. Default: QRIS 3600, VA 86400. Catatan: gambar QR-nya sendiri berumur lebih pendek, lihat kotak di bawah. |
customer | object | Opsional: {"name","phone"}; nama tampil di VA. |
return_url | string | Opsional, URL http(s) ≤512 karakter; menambah tombol “kembali ke merchant” di halaman bayar hosted. |
metadata | object | Bebas; dikembalikan apa adanya di respons & webhook. |
Respons 201:
{
"id": "pay_x8Kj2…",
"status": "pending",
"amount": 150000,
"rail": "qris",
"reference": "INV/001",
"qr_content": "00020101…", // rail=qris → render jadi QR
"va_number": null, // rail=va → nomor VA
"va_bank": null, // rail=transfer → bank tujuan (bca/mandiri/bni/bsi)
"hosted_url": "https://pay.doit.id/p/pay_x8Kj2…",
"return_url": "https://tokomu.id/invoice/42", // opsional saat create
"provider": "pjp", // penyelenggara berizin yang menerbitkan instrumen
"provider_ref": "A48957157…", // referensi di sisi penyedia, untuk rekonsiliasi
"fee_amount": 4000, // biaya layanan metode ini
"fee_payer": "customer", // siapa yang menanggung (diatur di portal)
"total_amount": 154000, // yang dibayar pelanggan; = amount bila payer merchant
"expires_at": "2026-08-19T15:30:00.000Z",
"paid_at": null,
"metadata": null,
"created_at": "2026-08-19T14:30:00.000Z"
}
pending; hanya instrumennya yang dilepas: rail dan qr_content
kembali null. Di halaman bayar hosted ini tertangani otomatis
(pelanggan membuat QR baru satu klik), jadi cara paling aman adalah mengarahkan pembayar ke
hosted_url. Kalau kamu merender qr_content di UI-mu sendiri, arahkan
pembayar ke hosted_url saat QR-mu kedaluwarsa. Tidak ada webhook untuk peristiwa
ini karena tagihannya belum berakhir.Metode pembayaran & biaya layanan
| Metode | Kode va_bank | Biaya layanan default |
|---|---|---|
| QRIS (semua bank & e-wallet) | … | 0% sampai Rp100.000, di atasnya 0,70%; selalu ditanggung merchant (ketentuan BI) |
| Mandiri Virtual Account | bmri | Rp4.000 |
| BSI Virtual Account | bsyi | Rp4.000 |
| BNI Virtual Account | bnia | Rp3.000 |
| BRI Virtual Account (BRIVA) | brin | Rp3.000 |
| CIMB Niaga Virtual Account | cimb | Rp3.000 |
| Permata Virtual Account | permata | Rp3.000 |
| Maybank Virtual Account | maybank | Rp3.000 |
| Danamon Virtual Account | danamon | Rp3.000 |
| BNC Virtual Account (Bank Neo Commerce) | neo | belum tersedia |
| BCA Virtual Account | bca | belum tersedia (method_unavailable) |
| Transfer Bank BCA (Mutasi Rekening) | bca dengan rail=transfer | Rp0 per transaksi; kode unik jadi bagian nominal, bukan biaya. Butuh langganan Mutasi Rekening (lihat Transfer bank). |
| Transfer Bank Mandiri (Mutasi Rekening) | mandiri dengan rail=transfer | Rp0 per transaksi; kode unik jadi bagian nominal, bukan biaya. Butuh langganan Mutasi Rekening. |
| Transfer Bank BNI (Mutasi Rekening) | bni dengan rail=transfer | Rp0 per transaksi; kode unik jadi bagian nominal, bukan biaya. Butuh langganan Mutasi Rekening. |
| Transfer Bank BSI (Mutasi Rekening) | bsi dengan rail=transfer | Rp0 per transaksi; kode unik jadi bagian nominal, bukan biaya. Butuh langganan Mutasi Rekening. |
Biaya di atas adalah default platform; harga nego per aplikasi bisa berbeda; tarif efektif
untuk aplikasi Anda selalu terlihat di portal → Metode Pembayaran dan di field
fee_amount setiap respons.
Di portal → Metode Pembayaran, tiap aplikasi bisa menyalakan/mematikan metode
(VA per bank, QRIS) dan memilih siapa yang menanggung biaya layanan: Merchant
(tagihan pelanggan tetap; biaya mengurangi penerimaan) atau Customer (biaya
ditambahkan ke total yang dibayar, terlihat transparan di halaman bayar dan di field
fee_amount/total_amount). Metode yang dimatikan menolak transaksi
baru dengan error method_disabled; metode yang belum tersedia di platform
tampil nonaktif dan mengembalikan method_unavailable.
fee_amount pada tagihan lama tidak berubah.Transfer bank (Mutasi Rekening)
Pelanggan mentransfer langsung ke rekening bank usaha Anda sendiri (BCA, Mandiri, BNI, atau BSI), lalu doit.id mengonfirmasinya otomatis dengan membaca mutasi rekening tersebut. Dana tidak pernah singgah di doit.id, jadi tidak ada settlement: uang masuk ke rekening Anda seketika.
Cara kerjanya: buat payment dengan rail=transfer dan va_bank berisi
bca, mandiri, bni, atau bsi. Sistem menambahkan kode unik 3 digit ke nominal,
sehingga total_amount di respons (misalnya Rp150.123) adalah jumlah PERSIS yang harus
ditransfer pelanggan. Halaman bayar hosted_url menampilkan nomor rekening tujuan, nama
pemilik, dan nominal itu, lengkap dengan tombol "Saya sudah transfer" untuk memicu pengecekan
seketika. Begitu mutasi dengan nominal itu terbaca, status menjadi paid dan Anda
menerima webhook payment.paid yang sama seperti rail lain, jadi kode integrasi Anda
tidak perlu berubah.
Nominal tepat = tanpa konfirmasi. Pelanggan yang mentransfer tepat sejumlah
total_amount tidak perlu mengirim bukti atau konfirmasi apa pun. Waktu konfirmasi
mengikuti jadwal pembacaan mutasi bank, jadi jangan menjanjikan angka menit tertentu ke pelanggan.
Bila pelanggan salah nominal (misalnya lupa kode unik), halaman bayar menyediakan kotak
unggah bukti transfer. Anda dikabari lewat email, bukti tampil di rincian pembayaran di
portal, dan setelah mengecek rekening Anda sendiri, klik Tandai lunas. Webhook
payment.paid yang sama terkirim; pembayaran ini diberi penanda
lunas_manual di flags.
- Prasyarat: pasang rekening di portal, menu Mutasi Rekening (isi kredensial internet banking; hanya transit untuk login, tidak kami simpan), dan langganan Mutasi aktif (bayar di muka Rp300.000/bulan atau Rp3.000.000/tahun per rekening). Metode ini baru tampil di halaman bayar setelah rekening terverifikasi.
- Bisa dipakai sebelum KYC selesai. Karena dana masuk langsung ke rekening Anda, metode ini tersedia sambil menunggu verifikasi VA dan QRIS.
- Biaya layanan Rp0 per transaksi; kode unik adalah bagian nominal, bukan biaya, dan pembebanannya terkunci ke merchant.
- BRI sengaja tidak disediakan.
BCA Bisnis API Direct
Untuk rekening perusahaan yang sudah memakai layanan API BCA, pilih BCA Bisnis API Direct
di portal → Mutasi Rekening. Setelah aktif, rekening ini juga muncul pada
GET /v1/mutations?bank=bca dengan API key live. Ini berbeda dari BCA internet banking: jangan masukkan
username internet banking atau KeyBCA. doit.id hanya memakai endpoint baca saldo dan
mutasi rekening, tidak memakai endpoint transfer dana.
- Aktifkan layanan di BCA. Ajukan fitur Informasi Saldo dan Mutasi Rekening untuk rekening perusahaan Anda. Minta hak akses baca saja bila BCA menyediakan pilihan tersebut.
- Whitelist konektor. Saat BCA meminta IP asal, gunakan alamat egress yang diberikan tim doit.id. Anda tidak perlu memberikan akses SSH atau akses ke server perusahaan Anda; konektor dan rotasi koneksinya dikelola doit.id.
- Bayar langganan lalu isi konfigurasi. Pilih langganan BCA Bisnis di portal, kemudian isi nomor rekening, nama rekening, Corporate ID, Client ID, Client Secret, API Key, dan API Secret. Simpan kredensial hanya di form portal, jangan di chat, tiket, atau kode aplikasi Anda.
- Uji lalu aktifkan penerimaan pembayaran. Kartu rekening langsung tersedia untuk memantau mutasi. Sakelar Terima pembayaran menguji pembacaan API sekali lagi sebelum BCA muncul sebagai tujuan transfer di checkout. Rute ini khusus BCA, rekening Mandiri, BNI, atau BSI yang sudah memakai pembaca lain tidak dipindahkan.
detected_at dan
balance pada GET /v1/mutations. Itu waktu pembacaan kami,
bukan jam transaksi di bank, dan mutasi sebelum fitur ini menyala tidak memilikinya. Sejak 14 Sep 2026 daftar mutasi BCA dilayani dari catatan doit.id itu, bukan dengan memanggil BCA setiap kali daftarnya dibuka, karena API BCA ditagih per panggilan. Kesegarannya tetap dijaga pemeriksaan tiap 5 menit, dan pelanggan yang baru membayar tetap bisa memaksa pemeriksaan lewat POST /v1/payments/{id}/recheck. Riwayat BCA yang tersimpan dimulai 13 Sep 2026; permintaan yang lebih tua dari itu akan kosong. BCA menandai transaksi yang belum dibukukan dengan PEND, tanpa tanggal. Untuk baris seperti itu doit.id mengisi date dengan tanggal mutasi pertama kali terbaca dan mengirim posted: false; begitu bank membukukannya, tanggal resmi bank menggantikannya dan posted menjadi true.Butuh jam transaksi dan saldo awal/akhir per mutasi? Minta BCA mengaktifkan Bank Statement SNAP untuk perusahaan Anda. Dokumentasi BCA menyatakan fitur tersebut mengirim timestamp ISO-8601, remark, saldo awal/akhir, dan detail transaksi. Integrasi SNAP membutuhkan kredensial/otorisasi tambahan dari BCA; hubungi care@doit.id setelah fasilitasnya aktif agar kami memigrasikan koneksi tanpa mengganggu checkout.
Saat ada invoice transfer BCA yang masih menunggu, doit.id membaca mutasi otomatis setiap 10 menit. Tombol cek status di halaman bayar juga meminta pemeriksaan langsung. Pelunasan hanya terjadi bila nominal mutasi kredit tepat sama dengan nominal unik invoice yang masih pending.
Checkout link: halaman bayar hosted
Setiap payment membawa hosted_url: halaman bayar siap pakai yang bisa kamu
redirect penuh atau kirim langsung ke pelanggan (WhatsApp, email, SMS), pola yang
di gateway lain dikenal sebagai checkout link. Halaman itu menampilkan nama usahamu,
nominal, pemilih metode, QR yang sudah dirender (atau nomor VA + tombol salin), memperbarui
statusnya sendiri saat pembayaran masuk, dan menangani kedaluwarsa. Kalau kamu tidak ingin
membangun tampilan pembayaran sendiri, cukup teruskan tautan ini. Integrasi minimummu
tinggal dua hal: buat payment, dengarkan webhook.
Tanpa koding sama sekali? Bisa. Di portal menu Transaksi, tombol + Buat tagihan, isi nominal dan referensi, dan kamu langsung mendapat link bayar yang sama untuk dibagikan manual; cocok untuk tagihan sesekali atau sebelum integrasi API jadi.
Kembali otomatis. Bila kamu mengisi return_url, halaman akan mengalihkan
pelanggan kembali ke sistemmu 5 detik setelah pembayaran diterima (dengan hitung mundur
dan tautan manual). Ini penting: tanpa itu, pelanggan sering membiarkan tab pembayaran terbuka
dan tidak pernah melihat konfirmasi di sisimu, padahal dananya sudah masuk.
Dwibahasa. Halaman tampil dalam Bahasa Indonesia secara default; tambahkan
?lang=en pada hosted_url untuk versi Inggris, atau biarkan pelanggan
memakai sakelar ID | EN di pojok kanan atas.
Siklus status
pending ──► paid ──► refunded
├──────► expired ──► paid (bayar telat)
└──────► failed
Hanya lima status, tidak ada yang keenam. Satu transisi yang perlu kamu antisipasi:
expired bisa berubah menjadi paid. Nomor VA dan kode QR di sisi bank
hidup lebih lama daripada masa berlaku tagihan, jadi pelanggan yang terlanjur menyalin nomornya
kadang tetap membayar setelah tagihan kedaluwarsa. Uang yang sudah berpindah tetap kami kreditkan:
webhook payment.paid terkirim seperti biasa (bisa jadi setelah kamu menerima
payment.expired), dan pembayarannya ditandai flag bayar_telat. Kalau
pesananmu sudah dibatalkan, gunakan flag itu untuk memutuskan: penuhi pesanannya, atau refund.
Kasus tak lazim dari penyedia masuk array flags; integrasi sederhana boleh
mengabaikannya dan tetap benar. Nilai yang ada saat ini: bayar_ganda (pembayaran
yang sama dibayar lewat dua instrumen berbeda, mis. pelanggan ganti metode lalu keduanya
terbayar; dana tetap tercatat dan direkap, tidak ada yang hilang) dan bayar_telat
(dibayar setelah kedaluwarsa, lihat di atas).
Membaca pembayaran
GET /v1/payments/{id}: satu pembayaran.
GET /v1/payments?reference=INV/001: cari berdasarkan referensimu.
GET /v1/payments?status=paid&limit=100: daftar, terbaru dulu.
Objek yang dikembalikan selalu berbentuk sama dengan respons create. Gunakan ini sebagai jaring pengaman (mis. cron rekonsiliasi); mekanisme utama pelunasan adalah webhook.
Refund
POST /v1/payments/{id}/refunds
curl -X POST https://pay.doit.id/v1/payments/pay_x8Kj2/refunds \
-H "Authorization: Bearer pb_test_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rf-001" \
-d '{"amount": 50000, "reason": "Kelebihan bayar"}'
amount opsional (default: penuh). Refund parsial boleh berulang sampai total
= nominal asal. Hanya pembayaran berstatus paid yang bisa direfund.
Recurring Billing
Tagih pelanggan Anda berulang (langganan, iuran, cicilan) tanpa membuat tagihan
manual tiap bulan. Anda mendefinisikan paket (nominal + interval), lalu
melanggankan pelanggan. doit.id menerbitkan invoice otomatis pada jadwalnya
dan mengirim webhook invoice.created berisi hosted_url.
hosted_url), dan Anda meneruskannya lewat kanal Anda sendiri (nomor
WhatsApp bisnis Anda, email, SMS). Tidak ada penyimpanan kartu dan tidak ada
auto-debit; pelanggan membayar sendiri, sama seperti pembayaran biasa.Alur singkat:
1. Buat paket POST /v1/billing/plans (sekali)
2. Langgankan POST /v1/billing/subscriptions (per pelanggan)
3. doit.id menerbitkan invoice tiap periode -> webhook invoice.created
4. Anda kirim hosted_url ke pelanggan lewat kanal Anda
5. Pelanggan bayar -> webhook payment.paid (seperti biasa)
Paket (Plan)
POST /v1/billing/plans
| Field | Tipe | Keterangan |
|---|---|---|
name | string, wajib | Nama paket (≤100 karakter), mis. "Internet 20 Mbps". |
amount | integer, wajib | Nominal rupiah utuh per periode. |
interval | object, wajib | {"unit","count"}. unit: day/week/month; count 1–366. Contoh bulanan: {"unit":"month","count":1}. |
rail | string | any (default), qris, va, atau transfer. Dgn any pelanggan memilih di halaman bayar. |
va_bank | string | Wajib bila rail=va atau rail=transfer (bca/mandiri/bni/bsi); lihat tabel kode bank. |
expires_in | integer | Masa berlaku tiap invoice (detik, ≥3600). Default 604800 (7 hari). |
kirim_email | boolean | Default false. Bila true, doit.id mengirim email tautan bayar langsung ke customer_email tiap invoice terbit, tanpa integrasi di sisi Anda. WhatsApp tetap lewat kanal Anda sendiri (webhook). |
curl -X POST https://pay.doit.id/v1/billing/plans \
-H "Authorization: Bearer <api key>" -H "Content-Type: application/json" \
-H "Idempotency-Key: plan-internet-20" \
-d '{"name":"Internet 20 Mbps","amount":150000,"interval":{"unit":"month","count":1}}'
GET /v1/billing/plans untuk daftar. PATCH /v1/billing/plans/{id}
dengan {"active":false} menonaktifkan paket (langganan yang sudah ada
berhenti diterbitkan invoice-nya, tapi tidak terhapus).
Langganan (Subscription)
POST /v1/billing/subscriptions: melanggankan satu pelanggan ke sebuah paket.
| Field | Tipe | Keterangan |
|---|---|---|
plan_id | string, wajib | ID paket. |
customer_name | string, wajib | Nama pelanggan (tampil di VA & halaman bayar). |
customer_phone | string | Opsional; Anda pakai untuk mengirim tagihan sendiri. |
customer_email | string | Opsional; ikut di payload webhook. |
reference | string | ID pelanggan di sistem Anda; jadi prefix reference tiap invoice. |
start_at | string | ISO-8601. Kosong/kini → invoice pertama terbit sekarang. Masa depan → mulai nanti (mis. akhir trial). |
metadata | object | Bebas; disimpan di langganan. |
Respons 201 memuat langganan + first_invoice (payment lengkap dgn
hosted_url) bila invoice pertama langsung terbit, jadi Anda bisa segera
meneruskannya tanpa menunggu webhook.
Kelola: GET /v1/billing/subscriptions (filter ?status=active),
GET /v1/billing/subscriptions/{id} (detail + daftar invoice-nya),
dan POST .../{id}/pause · .../resume · .../cancel.
Pause menahan penerbitan; resume melanjutkan dari periode berikutnya (tidak
memuntahkan tagihan periode tertinggal); cancel permanen.
invoice.created dikirim tiap invoice
recurring terbit. Payload = objek payment biasa (termasuk hosted_url,
reference, amount) plus field subscription
(id, plan, data pelanggan). Saat pelanggan membayar, Anda tetap menerima
payment.paid seperti biasa.Menerima webhook
Set webhook URL per aplikasi di portal. Event dikirim sebagai
POST JSON:
| Event | Kapan |
|---|---|
payment.paid | Pembayaran masuk. Payload lengkap, cukup untuk menandai invoice lunas & membuka layanan tanpa API call tambahan. |
payment.expired | Lewat masa berlaku. |
invoice.created | Invoice recurring baru terbit. Payload = payment (dgn hosted_url) + field subscription. Lihat Recurring Billing. |
payment.proof_uploaded | Pelanggan mengunggah bukti transfer di halaman bayar (khusus rail transfer). Payload = payment + field proof (id, filename, mime, size, amount, note, uploaded_at, url). Status TIDAK berubah; lihat Mutasi rekening & konfirmasi manual. Belum memakainya? Balas 200 dan abaikan. |
refund.succeeded / refund.failed | Hasil refund. |
webhook.test | Dikirim saat Anda menyimpan atau menguji webhook URL di portal (juga oleh probe onboarding). Ditandatangani seperti event asli; balas 2xx dan abaikan isinya. |
{
"id": "evt_9aB3…", // dedup dengan id ini
"type": "payment.paid",
"created_at": "2026-08-19T14:35:12.000Z",
"data": { …objek payment utuh… }
}
Balas 2xx secepatnya (proses berat kerjakan async). Aturan yang menyelamatkan
kamu dari insiden klasik:
- Dedup per
idevent: pengiriman minimal-sekali, duplikat mungkin terjadi. - Jangan andalkan urutan tiba: nilai kebenaran ada di field
status, bukan urutan.
Verifikasi signature
Setiap webhook membawa header PayBridge-Signature:
PayBridge-Signature: t=1755612912,v1=5f8a…
v1 = HMAC-SHA256(signing secret, t + "." + body).
Signing secret ada di menu Developer. Node.js:
const crypto = require('node:crypto');
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map(s => s.split('=')));
const calc = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(calc));
}
PHP:
function verify(string $rawBody, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
$calc = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
return hash_equals($calc, $p['v1']);
}
Retry & replay
Webhook yang tidak dibalas 2xx diulang dengan backoff:
1m → 5m → 15m → 1j → 3j → 6j → 12j → 24j. Setelah itu masuk dead-letter dan bisa
di-replay kapan pun dari halaman Webhook di portal, bersama seluruh riwayat
pengiriman, status, dan jumlah percobaannya.
Embed checkout
Setiap payment sudah punya hosted_url: halaman bayar siap pakai.
Dengan embed.js, halaman itu terbuka sebagai modal di situs
Anda, jadi pelanggan membayar tanpa berpindah halaman. Tanpa dependensi,
satu tag script.
<script src="https://doit.id/embed.js"></script>
<!-- cara 1: tanpa menulis JS: tautan biasa berisi hosted_url -->
<a href="https://pay.doit.id/p/pay_xxx" data-doit-checkout>Bayar</a>
<!-- cara 2: dari kode, dengan callback -->
<script>
doit.open('https://pay.doit.id/p/pay_xxx', {
onPaid: function (ev) { /* ev = {id, status, reference} */ },
onExpired: function (ev) { /* tagihan kedaluwarsa/gagal */ },
onClose: function (ev) { /* modal ditutup; ev = status akhir atau null */ },
closeDelay: 2500 // ms sebelum modal menutup sendiri setelah lunas
});
</script>
Saat tagihan mencapai status akhir, halaman bayar mengirim
postMessage {source:'doit-checkout', id, status, reference}
ke jendela induk; modal menutup sendiri setelah pelanggan melihat tanda lunas.
onPaid untuk pengalaman pengguna (tutup modal, tampilkan sukses).
Untuk membuka layanan atau menandai lunas di database, tetap andalkan webhook
payment.paid yang ditandatangani.AI agent & MCP
Integrasi doit.id bisa dikerjakan (atau dipakai langsung) oleh AI agent:
| Alat | Untuk apa |
|---|---|
llms.txt |
Referensi API ringkas dalam teks polos. Tempelkan URL-nya ke Claude, ChatGPT, atau agent lain agar ia paham seluruh API doit.id: endpoint, field, webhook, dan kode error, tanpa membaca halaman HTML ini. |
| MCP server | Server Model Context Protocol satu berkas, tanpa dependensi (Node.js ≥ 18). AI agent seperti Claude bisa membuat tagihan, mengecek status, dan mengelola tagihan berulang langsung sebagai tool. |
Pasang MCP server:
# unduh
curl -O https://doit.id/mcp/doit-mcp.mjs
# konfigurasi Claude Code / Claude Desktop (mcpServers):
{
"doit": {
"command": "node",
"args": ["/path/ke/doit-mcp.mjs"],
"env": { "DOIT_API_KEY": "pb_test_xxx" }
}
}
Tool yang tersedia: create_payment, get_payment,
list_payments, create_billing_plan,
list_billing_plans, create_subscription,
get_subscription. Pakai key pb_test_ dulu; semua
tool menghormati idempotency secara otomatis.
Mutasi rekening & konfirmasi manual (API)
Untuk tim CS: baca mutasi rekening usaha Anda sendiri dari sistem Anda, lalu tandai tagihan lunas. Alur khasnya: pelanggan transfer, lalu mengonfirmasi ke CS; CS mencari mutasinya, mencocokkan, dan menandai lunas. Semua endpoint di bawah memakai API key live dan hanya menyentuh rekening milik akun Anda. Prasyarat sama dengan Transfer bank: rekening terpasang di portal dan langganan Mutasi aktif.
Daftar mutasi
GET /v1/mutations?bank=mandiri&from=2026-09-01&to=2026-09-12&type=credit&limit=25&page=1
Parameter (semuanya opsional): bank (bca, mandiri,
bni, bsi, atau nama slot seperti bca_bisnis; kosong berarti
semua rekening Anda), from dan to (YYYY-MM-DD),
type (credit atau debit), amount (nominal
persis), q (cari di keterangan), limit (maks 100), page.
{
"data": [
{ "id": "mt_9f2…",
"bank": "mandiri", "jenis_rekening": "mandiri_bisnis", "account_number": "1240007571525",
"date": "2026-09-11", // tanggal di rekening koran bank
"detected_at": "2026-09-11 21:05:31", // saat mutasi terbaca sistem (WIB)
"type": "credit", "amount": 150123, "balance": 933620488,
"description": "TRSF E-BANKING CR ...",
"payment": { "id": "pay_x8Kj2…", "reference": "INV/001", "status": "pending" } }
],
"page": 1, "has_more": true,
"accounts": [ { "bank": "mandiri", "jenis_rekening": "mandiri_bisnis", "label": "Mandiri Bisnis",
"account_number": "1240007571525", "account_name": "Cloud Hosting Indonesia" } ]
}
payment terisi bila nominal mutasi kredit itu sama persis dengan nominal sebuah
tagihan transfer Anda dalam 60 hari terakhir (kode unik yang membuatnya bisa dicocokkan). Bank
hanya mencatat tanggal, bukan jam transaksi; detected_at adalah waktu mutasi terbaca
sistem, bukan jam transfer di bank.
Minta sistem mengecek sekarang
POST /v1/payments/{id}/recheck
Dipakai saat pelanggan bilang sudah transfer. Sistem membaca mutasi rekening saat itu juga dan
mencocokkan nominalnya. Balasannya objek payment ditambah changed. Bila nominal yang
ditransfer tepat, statusnya langsung paid dan webhook payment.paid
terkirim seperti biasa.
Bukti transfer dari pelanggan
Pelanggan yang salah nominal bisa mengunggah bukti transfer di halaman bayar. Bukti itu bisa Anda ambil lewat API, jadi CS tidak perlu meminta konfirmasi ulang: begitu buktinya ada, tinggal dicek dan ditandai lunas.
GET /v1/payments?has_proof=true&status=pending # antrean kerja CS
GET /v1/payments/{id} # detail + array proofs
GET /v1/payments/{id}/proofs # daftar bukti saja
GET /v1/payments/{id}/proofs/{proof_id} # berkasnya (JPG/PNG/PDF)
{
"proofs": [
{ "id": "bkt_7Qa…", "filename": "bukti.jpg", "mime": "image/jpeg", "size": 184320,
"amount": 150000, // nominal menurut pelanggan
"note": "transfer dari BCA a.n. Silvia",
"uploaded_at": "2026-09-12T09:15:22.000Z",
"url": "https://pay.doit.id/v1/payments/pay_x8Kj2…/proofs/bkt_7Qa…" }
]
}
url dibuka dengan API key yang sama (header Authorization), jadi bisa
langsung ditampilkan di dasbor internal Anda. Saat pelanggan mengunggah bukti, kami juga mengirim
webhook payment.proof_uploaded berisi objek payment ditambah proof, jadi
antrean CS bisa muncul tanpa polling. Sistem yang belum mengenal jenis event ini cukup membalas
200 dan mengabaikannya.
Tandai lunas manual
POST /v1/payments/{id}/confirm
{ "by": "cs-rani", "note": "transfer Rp150.000 tanpa kode unik", "mutation_id": "mt_9f2…" }
Hanya untuk transfer bank, karena dananya masuk langsung ke rekening Anda sehingga Anda
yang berwenang memverifikasi. VA dan QRIS dikonfirmasi penyelenggara dan dijawab
400 confirm_not_allowed. Pastikan dana benar-benar sudah masuk sebelum memanggilnya.
Status menjadi paid, webhook payment.paid terkirim, dan pembayaran
diberi penanda lunas_manual di flags. Ketiga isian body bersifat
opsional dan hanya tersimpan sebagai catatan audit.
Alternatif tanpa koding: bukti transfer yang diunggah pelanggan dan tombol Tandai lunas tersedia di portal, menu Pembayaran.
Plugin WooCommerce
Toko WooCommerce bisa menerima QRIS, Virtual Account, dan transfer bank tanpa menulis kode: pasang plugin, isi API key, salin webhook URL. Pelanggan diarahkan ke halaman bayar doit.id, dan pesanan berubah menjadi Processing begitu pembayaran masuk. Mendukung checkout klasik maupun Checkout Blocks, dan tabel pesanan baru (HPOS).
Unduh doit-woocommerce.zip Sumber terbuka (GPLv2), PHP 7.4+, WooCommerce 7+.
- Pasang. WordPress → Plugin → Tambah Baru → Unggah Plugin → pilih zip → Aktifkan.
- Kunci. WooCommerce → Pengaturan → Pembayaran → doit.id. Isi
API key dan signing secret dari portal doit.id (menu Developer). Mulai dengan
mode test: kunci
pb_test_, tanpa uang sungguhan. - Webhook. Salin Webhook URL yang tampil di halaman pengaturan itu
(bentuknya
https://toko-anda/?wc-api=doit) ke webhook URL aplikasi di portal. Tanpa ini pesanan tidak akan lunas otomatis; plugin juga mengecek status saat pelanggan kembali ke halaman terima kasih, tetapi webhook adalah jalur utamanya. - Mata uang toko harus IDR. Nominal dibulatkan ke rupiah utuh.
- Uji. Buat pesanan di mode test, buka halaman bayar, lalu tekan Simulasikan lunas di portal: pesanan menjadi Processing dan catatan pesanan menyebut id pembayarannya.
- Live. Setelah akun disetujui, isi kunci live dan matikan mode test.
Perilaku yang perlu diketahui. Tagihan berlaku sesuai pengaturan (bawaan 60 menit); lewat itu pesanan ditandai Failed dan pelanggan bisa membayar ulang dari Akun Saya, yang membuat tagihan baru. Tagihan yang masih berlaku dipakai lagi, tidak dibuat ganda. Setiap event webhook diverifikasi dengan signature dan di-dedup per id event. Referensi tagihan di doit.id = nomor pesanan WooCommerce.
Kode error
Semua error berbentuk sama: code stabil untuk mesin, message
menyebut nilai yang salah, doc_url menaut kemari:
{ "error": { "code": "…", "message": "…", "doc_url": "…" } }
| HTTP | Code | Arti & solusi |
|---|---|---|
| 401 | missing_api_key | Header Authorization: Bearer … tidak ada. Tambahkan API key-mu. |
| 401 | invalid_api_key | Key tidak dikenal; cek salin-tempelnya utuh dan aplikasinya masih ada. |
| 403 | app_disabled | Aplikasi pemilik key ini dinonaktifkan. Aktifkan kembali di portal → Developer. |
| 400 | method_unknown | Kombinasi rail/va_bank tidak dikenal; lihat tabel metode. |
| 403 | method_unavailable | Metode ini belum tersedia di platform (mis. kerja sama bank masih diproses). |
| 403 | method_disabled | Metode ini kamu nonaktifkan sendiri; aktifkan lagi di portal → Metode Pembayaran. |
| 403 | rail_not_live | Metode ini belum disetujui untuk live di akunmu (approval berjalan per rel; VA bisa aktif duluan sementara QRIS menunggu). message menyebut rel yang sudah aktif; status lengkap di portal. Sandbox tidak terpengaruh. |
| 400 | live_key_required | Mutasi rekening hanya bisa dibaca dengan API key live; kunci sandbox ditolak. |
| 400 | no_bank_account | Belum ada rekening Mutasi terpasang. Pasang di portal → Mutasi Rekening. |
| 400 | bank_not_found | Parameter bank tidak cocok dengan rekening mana pun pada akunmu. |
| 503 | mutations_unavailable | Layanan pembaca mutasi sedang tidak tersedia; coba lagi nanti. |
| 502 | mutations_failed | Gagal membaca mutasi dari layanan pembaca; coba lagi, pesannya memuat sebabnya. |
| 502 | recheck_failed | /recheck gagal menanyakan penyelenggara atau membaca mutasi; tagihan tidak berubah. |
| 400 | confirm_not_allowed | Konfirmasi manual hanya untuk rail transfer; VA dan QRIS dikonfirmasi penyelenggara. |
| 409 | payment_not_pending | Tagihan sudah lunas, batal, atau sudah diproses; tidak bisa dikonfirmasi lagi. |
| 404 | proof_not_found | Bukti transfer dengan id itu tidak ada pada tagihan tersebut. |
| 403 | platform_disabled | Fitur Platform belum aktif untuk akunmu; aktivasinya per akun, hubungi doit.id. |
| 409 | submerchant_exists | Email ini sudah terdaftar sebagai sub-merchant; id-nya ada di pesan dan header Location. |
| 404 | submerchant_not_found | Sub-merchant tidak ditemukan di bawah akunmu. |
| 403 | submerchant_not_yours | Header for-sub-merchant menunjuk sub-merchant yang bukan milik akunmu. |
| 409 | submerchant_app_missing | Sub-merchant itu belum punya aplikasi di environment tersebut; aplikasi live terbuka setelah KYC-nya disetujui. |
| 409 | bank_locked | Rekening pencairan sudah terdaftar dan terkunci; perubahan lewat doit.id dengan verifikasi ulang. |
| 400 | onboarding_incomplete | Isian atau dokumen wajib sub-merchant belum lengkap; rinciannya di pesan dan di GET .../onboarding. |
| 409 | onboarding_locked | Pendaftaran sub-merchant sudah dikirim dan sedang ditinjau, jadi tidak bisa diubah. |
| 400/409 | consent_required | consent_code belum dikirim, atau belum ada kode yang berlaku; panggil POST .../consent dulu. |
| 409 | consent_stale | Isian atau dokumen berubah setelah kode persetujuan dikirim; minta kode baru. |
| 400 | consent_invalid | Kode persetujuan salah atau kedaluwarsa; lima kali salah, kode hangus. |
| 429 | consent_too_soon | Kode baru saja dikirim; tunggu satu menit sebelum meminta lagi. |
| 400 | document_kind_invalid | Jenis dokumen tidak dikenal; pilihan yang sah ada di pesan dan di documents. |
| 400 | document_type_invalid | Tipe berkas tidak didukung. Juga document_empty (body kosong) dan 413 document_too_large (lebih dari 25 MB). |
| 400 | name_required | Field name wajib diisi. |
| 400 | email_invalid | Format email tidak valid. |
| 400 | type_invalid | type harus "personal" atau "business". |
| 400 | bank_incomplete | bank_code, account_no, dan account_name wajib diisi. |
| 400 | missing_idempotency_key | Semua POST wajib header Idempotency-Key. Pakai ID unik dari sistemmu. |
| 400 | invalid_request | Ada field yang salah; message menyebut field mana dan formatnya yang benar. |
| 404 | payment_not_found | ID tidak ada di akunmu. Key test tidak bisa membaca payment live (dan sebaliknya; cek tenant & env). |
| 409 | payment_not_refundable | Refund hanya untuk status paid. |
| 400 | invalid_refund_amount | Nominal melebihi sisa yang bisa direfund. |
| 502 | provider_refund_failed | Penyedia menolak refund; message membawa alasan aslinya. |
| 429 | rate_limited | Lewati batas 120 req/menit. Tunggu sesuai Retry-After, lalu ulangi request yang sama. |
| 500 | internal_error | Kesalahan di sisi kami, sudah tercatat di log kami. Ulangi dengan Idempotency-Key yang sama; aman. |
| 503 | service_unavailable | Layanan sedang dalam pemulihan (mis. sedang berpindah ke pusat data cadangan). Bersifat sementara: tunggu sesuai Retry-After lalu ulangi request yang sama dengan Idempotency-Key yang sama. Jangan tandai transaksi gagal permanen. |
Sandbox
Akun baru langsung berada di mode sandbox; tidak perlu menunggu apa pun untuk mulai menulis kode. Perilaku sandbox identik dengan produksi (endpoint, format, error), bedanya:
- Pembayaran tidak melibatkan dana riil.
- Pelunasan dipicu manual: buka pembayaran di portal → Simulasikan lunas,
webhook
payment.paidterkirim sungguhan ke server-mu. - Jalan ke produksi dibuka lewat Onboarding: mandiri, tanpa menunggu review manual.
Badan usaha: beberapa PT dalam satu akun
Bukan sub-merchant. Kalau kamu sendiri memiliki lebih dari satu perusahaan, tambahkan masing-masing sebagai badan usaha di akun yang sama (portal, menu Badan usaha). Tiap badan usaha melewati verifikasi (KYC) sendiri dan mendapat rekening pencairan sendiri; integrasi, API key, koneksi pembayaran, dan tim tetap satu.
Tidak ada parameter baru di API. Badan usaha dipilih saat aplikasi dibuat di portal, dan setiap tagihan yang lahir dari aplikasi itu tercap milik badan usaha tersebut: tampil atas namanya di halaman bayar dan diteruskan ke rekeningnya. Jadi pilih API key aplikasi yang tepat, selebihnya sama persis dengan tagihan biasa.
Tahu milik siapa. Respons POST/GET /v1/payments dan payload webhook
tagihan dari aplikasi badan usaha membawa entity ({ id, name }).
Tagihan dari aplikasi badan usaha utama tidak membawa field ini.
POST /v1/payments (API key aplikasi milik badan usaha)
{ "amount": 150000, "reference": "INV-0001" }
→ 201
{ "id": "pay_...", "status": "pending", "amount": 150000, "reference": "INV-0001",
"entity": { "id": "ent_...", "name": "PT Contoh Sistem Terintegrasi" }, ... }
webhook payment.paid
{ "id": "evt_...", "type": "payment.paid",
"entity": { "id": "ent_...", "name": "PT Contoh Sistem Terintegrasi" },
"data": { ... } }
Aturan yang dijaga. Aplikasi live hanya bisa dibuat untuk badan usaha yang sudah disetujui dan rekeningnya terpasang; sandbox bisa lebih dulu. Badan usaha sebuah aplikasi tidak bisa diganti setelah aplikasi punya transaksi: buat aplikasi baru. Biaya pencairan Rp10.000 berlaku per transfer, jadi per badan usaha per periode.
Platform: sub-merchant via API
Akses awal, aktivasi per akun. Untuk SaaS, vendor billing, dan agregator yang
membuka pembayaran bagi pelanggannya sendiri. Prasyarat: akun terverifikasi, perjanjian
platform, lalu fitur diaktifkan doit.id; tanpa itu semua endpoint di bawah menjawab
403 platform_disabled. Selengkapnya di halaman produk.
Daftarkan sub-merchant. Dari portal (menu Sub-merchant) atau lewat API. Idempoten per email: yang sudah terdaftar dijawab
409 beserta id-nya. sandbox.api_key tampil sekali.
POST /v1/submerchants
{ "type": "personal", "name": "Warung Kopi Sudirman",
"email": "owner@warkop.id", "phone": "0812xxxxxx",
"profile": {
"category": "goods", "industry": "food_beverage",
"description": "Kedai kopi, pesan lewat aplikasi kami",
"monthly_transactions": 400, "monthly_amount": 12000000,
"address": { "city": "Jakarta Selatan", "postal_code": "12190" },
"owner": { "name": "Budi Santoso", "id_number": "3174..." } } }
→ 201
{ "id": "ten_a1b2c3", "status": "draft",
"onboarding_url": "https://doit.id/daftar/?ref=...",
"prefilled": ["category", "industry", "address.city", ...],
"sandbox": { "app_id": "app_...", "api_key": "pb_test_..." } }
Isi lebih dulu untuk sub-merchantmu. Semua yang kamu kirim di profile
tampil sudah terisi di formulir verifikasinya; pemilik usaha tinggal mengoreksi dan
mengunggah dokumen. Field yang dikenal:
business_type, category (goods | services | both),
industry (hosting_saas | digital_goods | isp_telco | education | retail |
food_beverage | professional_services | health_beauty | travel | property | donation | other),
entity_type (pt | pt_perorangan | cv | koperasi | yayasan | ud), annual_revenue
(under_500m | 500m_2.5b | 2.5b_10b | over_10b), description,
monthly_transactions, monthly_amount, website,
social_media, other_channels, established_at (YYYY-MM-DD);
address {street, province, city, district, village, postal_code};
owner {name, email, id_number, phone, birth_place, birth_date, gender
(male | female), id_address, tax_id};
commissioner {name, email, id_number, phone};
legal {company_tax_id, nib, deed_number, deed_date, notary, decree_number, decree_date, amendment};
bank {name, account_no, account_name, branch};
serta legal_name, brand, applicant_name, phone.
Field yang tidak dikenal atau nilai pilihan yang salah dijawab 400 profile_invalid
beserta daftarnya.
Dua cara menyelesaikan verifikasi (KYC). Keduanya berujung di antrean
pemeriksaan doit.id yang sama, dan boleh dicampur untuk satu sub-merchant.
(1) Tampilkan onboarding_url: pemilik usaha melengkapi data dan mengunggah
dokumennya sendiri di halaman doit.id; link yang sama kami kirim ke emailnya sebagai
cadangan. (2) Verifikasi di dalam aplikasimu:
sub-merchant tidak perlu membuka doit.id sama sekali. Sandbox bisa dipakai tanpa
menunggu verifikasi; produksi terbuka setelah disetujui.
Verifikasi di dalam aplikasimu (tanpa membuka doit.id)
Kamu yang mengumpulkan data dan dokumen di aplikasimu, lalu meneruskannya lewat API. Dari portal, tombol Lengkapi KYC di menu Sub-merchant melakukan hal yang sama. Satu syarat yang tidak bisa dilewati: persetujuan pemilik usaha. Sebelum dikirim, kami mengirim email ke pemilik berisi ringkasan isian (nama usaha, pemilik, rekening pencairan, daftar dokumen) dan kode enam angka. Pemilik memasukkan kode itu di aplikasimu, kamu meneruskannya ke kami. Tanpa kode, pendaftaran tidak bisa dikirim.
GET /v1/submerchants/{id}/onboarding isian, dokumen, dan apa yang masih kurang
PATCH /v1/submerchants/{id}/onboarding simpan isian (bentuk sama dengan "profile")
PUT /v1/submerchants/{id}/documents/{kind} unggah satu dokumen (isi berkas mentah)
POST /v1/submerchants/{id}/consent kirim kode persetujuan ke email pemilik
POST /v1/submerchants/{id}/submit { "consent_code": "123456" }
1. Baca kebutuhannya. GET .../onboarding mengembalikan
fields (tiap isian: field, label, section,
type, options, required, value), jadi
formulirmu bisa digambar dari jawaban ini dan otomatis mengikuti tipe sub-merchant
(perorangan tidak ditanya akta atau SK). Juga missing_fields,
documents, missing_documents, ready, dan
blocking (alasan belum bisa dikirim; code: fields_missing | documents_missing | bank_name_mismatch | type_mismatch).
{ "kyc_status": "draft", "editable": true, "type": "personal",
"missing_fields": ["owner.id_number", "bank.account_no", ...],
"documents": [ { "kind": "ktp_direktur", "label": "KTP pemilik",
"required": true, "uploaded": false }, ... ],
"missing_documents": ["ktp_direktur", "selfie_ktp", "buku_rekening"],
"ready": false,
"blocking": { "code": "fields_missing", "message": "Isian wajib belum lengkap: ..." },
"consent": { "sent_to": "ow•••@warkop.id", "requested": false, "given_at": null } }
2. Simpan isian. PATCH .../onboarding menerima objek yang sama
dengan profile, boleh sebagian dan boleh berulang. Jawabannya adalah keadaan
terbaru ditambah accepted. Rekening pencairan diisi di bank;
nama pemiliknya wajib sama dengan nama badan usaha (atau nama KTP untuk perorangan),
selain itu blocking.code menjadi bank_name_mismatch.
3. Unggah dokumen. Kirim isi berkas sebagai body mentah (bukan multipart)
dengan Content-Type berkasnya, maksimal 25 MB. Diterima: PDF, JPG, PNG, HEIC,
WEBP, ZIP. Nama berkas lewat header x-filename; bila tidak dikirim, diturunkan
dari tipe konten. Mengunggah kind yang sama menggantikan berkas sebelumnya.
curl -X PUT https://pay.doit.id/v1/submerchants/ten_a1b2c3/documents/ktp_direktur \
-H "Authorization: Bearer pb_live_..." \
-H "Content-Type: image/jpeg" -H "x-filename: ktp.jpg" \
--data-binary @ktp.jpg
4. Minta persetujuan, lalu kirim. Saat ready: true, panggil
POST .../consent; kode berlaku 30 menit, lima kali salah hangus, dan
permintaan ulang dibatasi sekali per menit. Lalu POST .../submit dengan
consent_code. Kode terikat pada isian yang dilihat pemilik: bila isian atau
dokumen berubah setelah kode dikirim, jawabannya 409 consent_stale dan kode
baru harus diminta. Setelah terkirim, kyc_status menjadi masuk
dan isian terkunci (409 onboarding_locked); submit ulang aman,
dijawab already_submitted. Bila tim kami meminta perbaikan, status kembali
perbaikan, review_note berisi catatannya, dan isian terbuka lagi.
Setiap berkas dan pendaftaran yang kamu kirim tercatat atas nama platformmu, dan persetujuan pemilik tersimpan sebagai bukti. Pemeriksaan dokumen oleh doit.id tidak berubah: yang berbeda hanya siapa yang mengetik dan mengunggah.
Yang ditanyakan saat verifikasi (setara formulir sub-merchant penyelenggara pembayaran pada umumnya, jadi sub-merchantmu bisa menyiapkannya di awal): identitas usaha (nama, jenis, kategori, industri, deskripsi singkat), profil transaksi (perkiraan jumlah dan nilai per bulan; menjadi acuan limit), alamat lengkap sampai kelurahan dan kode pos sesuai KTP atau NIB, bukti usaha (website, media sosial, logo), lalu legalitas: perorangan cukup KTP, selfie, dan bukti rekening (NPWP opsional), badan usaha menyertakan akta, SK Kemenkumham, NIB, NPWP perusahaan, serta KTP dan NPWP direktur. Terakhir rekening pencairan atas nama pemilik usaha.
Pantau status. GET /v1/submerchants untuk daftar,
GET /v1/submerchants/{id} untuk detail (kyc_status:
draft → masuk → diproses → disetujui | ditolak, atau perbaikan bila diminta melengkapi). Set
webhook_url aplikasimu dan setiap perubahan status dikirim sebagai event
submerchant.updated berisi { id, name, kyc_status } dengan
signature standar, jadi tidak perlu polling.
Rekening pencairan. POST /v1/submerchants/{id}/bank dengan
bank_code, account_no, account_name. Nama wajib
sama dengan pemilik usaha dan diverifikasi saat aktivasi. Sekali daftar, terkunci;
panggilan kedua dijawab 409 bank_locked. Perubahan rekening lewat doit.id
dengan verifikasi ulang.
Status pencairan. GET /v1/submerchants/{id}/settlements
mengembalikan riwayat payout (pending | processing | sent | failed) untuk
kamu tampilkan di aplikasimu. Settlement berjalan otomatis ke rekening tiap
sub-merchant; tidak ada saldo dan tidak ada alur tarik dana yang perlu dibangun.
Satu API key untuk semua sub-merchant. Kirim header
for-sub-merchant: <id anak> pada endpoint pembayaran mana pun, dan
permintaan itu dijalankan sebagai anak tersebut: tagihan, metode, rute, dan webhook
semuanya miliknya. API key wajib milik induknya, selain itu
403 submerchant_not_yours. Tanpa header ini, API key tiap sub-merchant
tetap bisa dipakai seperti biasa.
POST /v1/payments
Authorization: Bearer <api key INDUK>
for-sub-merchant: ten_abc123
Idempotency-Key: inv-2026-0001
{ "amount": 150000, "reference": "INV-0001" }
Respons dan payload webhook anak membawa sub_merchant
({ id, name }), sehingga kamu boleh mengarahkan callback SELURUH anak ke
satu endpoint dan tetap tahu asalnya tanpa memetakan id sendiri.
Komisi. PATCH /v1/submerchants/{id}/komisi dengan
flat (rupiah) dan/atau percent (maksimal dua desimal, mis.
2.75). Komisi dibekukan saat transaksi lunas, jadi mengubah tarif
hari ini tidak menggeser transaksi kemarin dan tidak bisa dipakai menagih mundur.
Komisi dibebankan ke merchant: ia mengurangi netto sub-merchant, dan tidak pernah menambah yang dibayar pembeli. Netto sub-merchant dijaga tetap positif; bila aturan komisimu melebihi netto sebuah transaksi, yang dibebankan dipangkas dan selisihnya terlihat di rincian. Pada siklus settlement yang sama, sub-merchant menerima nettonya dan kamu menerima jumlah seluruh komisi anakmu ke rekeningmu sendiri. Tetap tidak ada saldo mengendap dan tidak ada alur tarik dana.
Mode kompat Duitku: migrasi tanpa mengubah kode
Sudah punya integrasi Duitku yang berjalan? Kamu tidak perlu menulis ulang apa pun. Aktifkan sendiri lewat portal: Developer → Aplikasi baru → jawab “Migrasi dari payment gateway existing?” dengan Duitku. Kredensialnya langsung tampil sekali, bersebelahan dengan API key. Sudah punya aplikasi dan belum memilihnya saat dibuat? Tetap bisa: tautan yang sama muncul di baris aplikasi setelah diaktifkan, dipakai untuk melihatnya lagi bila lupa disalin. doit.id menyediakan endpoint yang kompatibel dengan API Payment Gateway Duitku v2, cukup ganti tiga nilai di konfigurasi:
| Konfigurasi | Sebelum | Sesudah |
|---|---|---|
| Base URL | https://passport.duitku.com | https://pay.doit.id |
| merchantCode | dari Duitku | tampil saat kamu mengaktifkan mode ini di portal |
| apiKey | dari Duitku | diberikan bersamaan, satu paket |
Yang ditiru persis: POST /webapi/api/merchant/v2/inquiry (signature
MD5(merchantCode + merchantOrderId + paymentAmount + apiKey); respons berisi
reference, paymentUrl, vaNumber, qrString),
POST /webapi/api/merchant/transactionStatus, dan callback ke
callbackUrl-mu dalam format form-urlencoded dengan signature
MD5(merchantCode + amount + merchantOrderId + apiKey), rumus yang sama dengan
yang sudah diverifikasi kodemu hari ini.
Pemetaan paymentMethod: kode VA (BC=BCA, M2=Mandiri, BR=BRI, I1=BNI, B1=CIMB, BT=Permata, VA=Maybank, NC=BNC, DM=Danamon, BV=BSI)
→ Virtual Account bank terkait; kode QRIS/e-wallet (SP, SA, NQ, DQ, OV, DA, LF, LA, dll.)
→ QRIS yang bisa dibayar semua aplikasi. paymentUrl mengarah ke halaman bayar
hosted doit.id.
Field callback yang dipantulkan apa adanya
Seperti Duitku, callback memantulkan kembali nilai yang kamu kirim saat inquiry. Banyak sistem memakainya untuk mengenali tujuan pembayaran, jadi nilainya dikirim persis seperti diterima:
| Field di callback | Asalnya |
|---|---|
additionalParam | Dari additionalParam saat inquiry; umum dipakai membawa ID akun atau tagihan internalmu. |
merchantUserId | Dari merchantUserId saat inquiry. |
productDetail | Dari productDetails saat inquiry. |
merchantOrderId | Order ID milikmu sendiri. |
reference / publisherOrderId | ID payment di doit.id. |
Isi additionalParam dan productDetails juga tampil di portal pada detail
transaksi dan ikut tercari di kolom pencarian, sangat membantu tim keuangan mencocokkan
pembayaran dengan tagihan, karena nomor referensi biasanya berupa angka yang tidak bermakna.
callbackUrl yang ada di request inquiry,
per transaksi, bukan ke webhook URL di menu Developer. Keduanya berbeda: webhook URL native
mengirim JSON dengan header PayBridge-Signature, sedangkan endpoint Duitku-mu
mengharapkan form-urlencoded + MD5. Memasang URL callback Duitku di kolom webhook native membuat
endpoint-mu menerima payload yang tidak bisa ia proses. Selama memakai mode kompat, biarkan kolom
webhook native kosong.Mode kompat Xendit: Invoice API tetap hidup di sini
Xendit sedang memensiunkan Invoice API (legacy) dan meminta penggunanya
menulis ulang ke Payment Session. Kalau sistemmu memakai
POST /v2/invoices, di sini kode itu tetap jalan apa adanya;
cukup ganti dua nilai konfigurasi. Aktifkan sendiri lewat portal:
Developer → Aplikasi baru → jawab “Migrasi dari payment gateway existing?”
dengan Xendit, dan isi URL callback sistem lamamu di kolom yang sama
(wajib untuk Xendit; di dashboard mereka pun disetel sekali, bukan dikirim
per request). Kredensialnya tampil sekali, bersebelahan dengan API key.
| Xendit | doit.id | |
|---|---|---|
| Base URL | https://api.xendit.co | https://pay.doit.id |
| Secret key (Basic auth) | dari dashboard Xendit | tampil saat kamu mengaktifkan mode ini di portal |
Callback token (x-callback-token) | dari dashboard Xendit | diberikan bersamaan, satu paket |
| Callback URL | disetel di dashboard | diisi saat aktivasi di portal (kontak kami untuk mengubahnya) |
Yang ditiru, persis kontrak legacy:
| Endpoint | Perilaku |
|---|---|
POST /v2/invoices | external_id, amount, invoice_duration, payer_email, description, success_redirect_url → respons berisi invoice_url (halaman bayar hosted kami: pembayar memilih VA atau QRIS), status PENDING |
GET /v2/invoices/{id} | status PENDING / PAID / EXPIRED |
GET /v2/invoices?external_id=… | daftar invoice dengan external_id itu (external_id memang tidak unik; tiap POST membuat invoice baru, sama seperti aslinya) |
POST /invoices/{id}/expire | batalkan invoice pending (varian /v2/…/expire juga diterima) |
| Callback saat lunas | JSON ke callback URL-mu dengan header x-callback-token; berisi external_id, status: "PAID", paid_amount, paid_at, payment_method (BANK_TRANSFER/QR_CODE), payment_channel, bank_code. Gagal 2xx → dicoba ulang berkali-kali dengan jeda menaik. |
SETTLED
tidak dipakai; lunas selalu dilaporkan PAID.Onboarding → live
Akses live tidak menunggu email atau rapat sign-off. Halaman Onboarding di portal berisi 8 butir yang tercentang otomatis dari bukti nyata saat integrasimu melakukannya di sandbox:
| Butir | Cara lulus |
|---|---|
| Membuat pembayaran | POST /v1/payments pertama-mu. |
| Idempotency | Kirim ulang request dengan Idempotency-Key yang sama. |
| Membaca status | GET /v1/payments/… (pola jaring pengaman). |
| Webhook terpasang | Set webhook URL di menu Developer. |
Menerima payment.paid | Simulasikan lunas; endpoint-mu membalas 2xx. |
Menerima payment.expired | Buat payment expires_in: 60, biarkan kedaluwarsa. |
| Menolak signature palsu | Tombol Uji webhook mengirim event ber-signature salah; endpoint-mu harus menolaknya. |
| Tahan webhook ganda | Uji webhook mengirim event sama dua kali; keduanya harus dibalas 2xx. |
Delapan centang penuh → akses live terbuka detik itu juga: buat key
pb_live_, ganti prefix key di konfigurasi, selesai.
rail_not_live
di live, dan tetap bisa dipakai penuh di sandbox.Butir-butir ini bukan formalitas; mereka persis daftar penyebab insiden payment paling umum, jadi menyelesaikan onboarding berarti integrasimu sudah tahan di skenario yang biasanya baru ketahuan di produksi.
doit.id · pay.doit.id · Pertanyaan? care@idcloudhost.com