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 dan klik Simulasikan lunas. 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:

KeyPerilaku
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

Tidak butuh API untuk sekadar membuat link bayar. Di portal → menu Payments ada tombol + Buat tagihan: isi nominal dan nama pelanggan, link bayar langsung jadi beserta template pesan penagihan siap kirim ke WhatsApp. Cocok untuk staf non-teknis; API di bawah ini untuk integrasi sistem.
FieldTipeKeterangan
amountinteger, wajibRupiah utuh: 150000 = Rp150.000. Tanpa desimal.
railstring"qris", "va", atau "any" (default bila dikosongkan). Dengan "any", pelanggan memilih metodenya sendiri di halaman bayar hosted, lengkap dengan biayanya per metode.
referencestring, wajibID tagihan di sistemmu (≤128 karakter). Dikembalikan di webhook.
va_bankstringWajib bila rail=va; lihat tabel kode bank di bawah.
expires_inintegerMasa berlaku tagihan (detik), ≥60. Default: QRIS 3600, VA 86400. Catatan: gambar QR-nya sendiri berumur lebih pendek, lihat kotak di bawah.
customerobjectOpsional: {"name","phone"}; nama tampil di VA.
return_urlstringOpsional, URL http(s) ≤512 karakter; menambah tombol “kembali ke merchant” di halaman bayar hosted.
metadataobjectBebas; 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,
  "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"
}
QR dinamis berumur pendek (±10 menit), terpisah dari masa berlaku tagihan. Bila QR kedaluwarsa sementara tagihannya masih berlaku, pembayaran tetap berstatus 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.
Kamu tidak memilih penyedia pembayaran: routing, failover, dan seluruh kerumitan di baliknya ditangani doit.id. Kalau satu jalur gangguan, transaksi baru otomatis lewat jalur sehat.

Metode pembayaran & biaya layanan

MetodeKode va_bankBiaya layanan default
QRIS (semua bank & e-wallet)0,70%, selalu ditanggung merchant (ketentuan BI)
Mandiri Virtual AccountbmriRp4.000
BSI Virtual AccountbsyiRp4.000
BNI Virtual AccountbniaRp3.000
BRI Virtual Account (BRIVA)brinRp3.000
CIMB Niaga Virtual AccountcimbRp3.000
Permata Virtual AccountpermataRp3.000
Maybank Virtual AccountmaybankRp3.000
Danamon Virtual AccountdanamonRp3.000
BNC Virtual Account (Bank Neo Commerce)neobelum tersedia
BCA Virtual Accountbcabelum tersedia (method_unavailable)

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.

Khusus QRIS: sesuai regulasi Bank Indonesia, MDR QRIS (0,70%) tidak boleh dibebankan ke pembeli; pilihan pembebanannya terkunci ke Merchant dan tidak bisa diubah.

Halaman bayar hosted: tanpa frontend sama sekali

Setiap payment membawa hosted_url: halaman bayar siap pakai yang bisa kamu kirim langsung ke pelanggan (WhatsApp, email, SMS). Halaman itu menampilkan nama usahamu, nominal, 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.

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.

doit.id tidak mengirim WhatsApp/email ke pelanggan Anda. Model ini link-based: tiap invoice menghasilkan halaman bayar (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

FieldTipeKeterangan
namestring, wajibNama paket (≤100 karakter), mis. "Internet 20 Mbps".
amountinteger, wajibNominal rupiah utuh per periode.
intervalobject, wajib{"unit","count"}. unit: day/week/month; count 1–366. Contoh bulanan: {"unit":"month","count":1}.
railstringany (default), qris, atau va. Dgn any pelanggan memilih di halaman bayar.
va_bankstringWajib bila rail=va; lihat tabel kode bank.
expires_inintegerMasa berlaku tiap invoice (detik, ≥3600). Default 604800 (7 hari).
kirim_emailbooleanDefault 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.

FieldTipeKeterangan
plan_idstring, wajibID paket.
customer_namestring, wajibNama pelanggan (tampil di VA & halaman bayar).
customer_phonestringOpsional; Anda pakai untuk mengirim tagihan sendiri.
customer_emailstringOpsional; ikut di payload webhook.
referencestringID pelanggan di sistem Anda; jadi prefix reference tiap invoice.
start_atstringISO-8601. Kosong/kini → invoice pertama terbit sekarang. Masa depan → mulai nanti (mis. akhir trial).
metadataobjectBebas; 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.

Event 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:

EventKapan
payment.paidPembayaran masuk. Payload lengkap, cukup untuk menandai invoice lunas & membuka layanan tanpa API call tambahan.
payment.expiredLewat masa berlaku.
invoice.createdInvoice recurring baru terbit. Payload = payment (dgn hosted_url) + field subscription. Lihat Recurring Billing.
refund.succeeded / refund.failedHasil refund.
webhook.testDikirim 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:

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.

Callback di browser bukan sumber kebenaran. 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:

AlatUntuk 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.

Kunci API adalah rahasia. Simpan di env konfigurasi MCP, jangan tempel di prompt. Untuk agent yang hanya perlu membaca, buat aplikasi terpisah di portal supaya kuncinya mudah dicabut.

Kode error

Semua error berbentuk sama: code stabil untuk mesin, message menyebut nilai yang salah, doc_url menaut kemari:

{ "error": { "code": "…", "message": "…", "doc_url": "…" } }
HTTPCodeArti & solusi
401missing_api_keyHeader Authorization: Bearer … tidak ada. Tambahkan API key-mu.
401invalid_api_keyKey tidak dikenal; cek salin-tempelnya utuh dan aplikasinya masih ada.
403app_disabledAplikasi pemilik key ini dinonaktifkan. Aktifkan kembali di portal → Developer.
400method_unknownKombinasi rail/va_bank tidak dikenal; lihat tabel metode.
403method_unavailableMetode ini belum tersedia di platform (mis. kerja sama bank masih diproses).
403method_disabledMetode ini kamu nonaktifkan sendiri; aktifkan lagi di portal → Metode Pembayaran.
403rail_not_liveMetode 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.
400missing_idempotency_keySemua POST wajib header Idempotency-Key. Pakai ID unik dari sistemmu.
400invalid_requestAda field yang salah; message menyebut field mana dan formatnya yang benar.
404payment_not_foundID tidak ada di akunmu. Key test tidak bisa membaca payment live (dan sebaliknya; cek tenant & env).
409payment_not_refundableRefund hanya untuk status paid.
400invalid_refund_amountNominal melebihi sisa yang bisa direfund.
502provider_refund_failedPenyedia menolak refund; message membawa alasan aslinya.
429rate_limitedLewati batas 120 req/menit. Tunggu sesuai Retry-After, lalu ulangi request yang sama.
500internal_errorKesalahan di sisi kami, sudah tercatat di log kami. Ulangi dengan Idempotency-Key yang sama; aman.
503service_unavailableLayanan 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:

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:

KonfigurasiSebelumSesudah
Base URLhttps://passport.duitku.comhttps://pay.doit.id
merchantCodedari Duitkutampil saat kamu mengaktifkan mode ini di portal
apiKeydari Duitkudiberikan 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 callbackAsalnya
additionalParamDari additionalParam saat inquiry; umum dipakai membawa ID akun atau tagihan internalmu.
merchantUserIdDari merchantUserId saat inquiry.
productDetailDari productDetails saat inquiry.
merchantOrderIdOrder ID milikmu sendiri.
reference / publisherOrderIdID 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.

Callback dikirim ke 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 adalah jembatan migrasi; integrasi baru sebaiknya langsung memakai API doit.id yang lebih sederhana. Keduanya menulis ke ledger yang sama, jadi kamu bisa migrasi bertahap: sistem lama lewat endpoint kompat, fitur baru lewat API utama, satu dashboard.

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.

Xenditdoit.id
Base URLhttps://api.xendit.cohttps://pay.doit.id
Secret key (Basic auth)dari dashboard Xendittampil saat kamu mengaktifkan mode ini di portal
Callback token (x-callback-token)dari dashboard Xenditdiberikan bersamaan, satu paket
Callback URLdisetel di dashboarddiisi saat aktivasi di portal (kontak kami untuk mengubahnya)

Yang ditiru, persis kontrak legacy:

EndpointPerilaku
POST /v2/invoicesexternal_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}/expirebatalkan invoice pending (varian /v2/…/expire juga diterima)
Callback saat lunasJSON 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.
Batasnya, supaya tidak ada kejutan: yang ditiru hanya Invoice API (checkout hosted, VA + QRIS). API khusus Fixed VA, QR Code API, e-wallet direct, kartu, dan payout/disbursement tidak ditiru; payout tidak akan pernah, karena kami tidak memegang dana. Status 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:

ButirCara lulus
Membuat pembayaranPOST /v1/payments pertama-mu.
IdempotencyKirim ulang request dengan Idempotency-Key yang sama.
Membaca statusGET /v1/payments/… (pola jaring pengaman).
Webhook terpasangSet webhook URL di menu Developer.
Menerima payment.paidSimulasikan lunas; endpoint-mu membalas 2xx.
Menerima payment.expiredBuat payment expires_in: 60, biarkan kedaluwarsa.
Menolak signature palsuTombol Uji webhook mengirim event ber-signature salah; endpoint-mu harus menolaknya.
Tahan webhook gandaUji 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.

Aktivasi metode pembayaran di live berjalan per rel mengikuti approval penyedia: VA bisa disetujui lebih dulu sementara QRIS masih diproses. Statusnya terlihat di portal; rel yang belum disetujui mengembalikan error 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