API
Langganan
Tagihan berulang, ditagih lewat QRIS tiap siklus.
Tersedia untuk semua akun. Mode test bisa dipakai siapa saja sejak hari pertama; langganan live butuh akun yang sudah terverifikasi, seperti dijelaskan di bawah. Referensi lengkapnya ada di /v1/openapi.json.
Tiap siklus menerbitkan satu invoice, dan pelanggan menyetujui pembayaran QRIS untuk invoice itu. Tidak ada kartu tersimpan dan tidak ada yang ditagih otomatis — perpanjangan terjadi karena ada orang yang scan.
Langganan live hanya untuk akun yang sudah terverifikasi. Sebelum verifikasi selesai, POST /v1/subscriptions dengan live key dijawab 403 verification_required dan tidak menulis apa pun; sandbox key tetap jalan, jadi integrasinya bisa disiapkan dulu.
Lebih suka menyimpan jadwal di sistem sendiri? Langganan juga bisa ditagih dengan menerbitkan satu pembayaran biasa per periode. Alurnya ada di panduan menagih langganan bulanan tanpa autodebet, dan di sana pun pelanggan tetap menyetujui tiap siklusnya.
Bentuknya
Customer adalah orang yang Anda tagih. Plan adalah harga dan siklusnya. Subscription menggabungkan keduanya. Tiap siklus terbit satu invoice, dan tiap invoice punya payment request yang bisa Anda arahkan ke pelanggan.
1. Siapkan plan dan customer
curl __BASE__/v1/subscription_plans \
-H "Authorization: Bearer kp_test_..." \
-H "Content-Type: application/json" \
-d '{"code":"basic","name":"Bulanan","amount":150000,"interval_unit":"month"}'
curl __BASE__/v1/subscription_customers \
-H "Authorization: Bearer kp_test_..." \
-H "Content-Type: application/json" \
-d '{"name":"Budi","phone":"+628123456789","external_id":"crm-42"}'Kirim external_id kalau Anda punya id sendiri. Mengirim id yang sama lagi akan memperbarui customer itu, bukan membuat yang kedua — ini yang membuat import aman diulang.
Untuk mengubah customer, pakai PATCH /v1/subscription_customers/{id}. Pakai ini — bukan kirim ulang POST — terutama kalau yang diperbaiki justru external_id-nya: endpoint create mencocokkan field itu, jadi mengubahnya di sana malah membuat customer kedua dan langganannya tetap nempel di yang pertama.
amount tetap wajib walaupun isinya 0. Plan gratis itu plan beneran, jadi field yang kosong tidak bisa diartikan sebagai gratis.
Mengubah atau menghentikan plan
Untuk mengubah harga atau siklus plan, kirim ulang plan itu dengan code yang sama. Itu menulis versi berikutnya dan mengarsipkan versi yang digantikan, dan karena itulah code wajib diisi: field ini yang mengikat versi-versinya. Pelanggan yang sudah berlangganan tidak ikut berubah harganya. Tiap langganan tetap di versi saat dibuka; untuk memindahkannya ke versi baru, pakai POST /v1/subscriptions/{id}/change di bawah.
Untuk berhenti menawarkan plan tanpa menggantinya, pakai POST /v1/subscription_plans/{id}/archive. Plan itu keluar dari GET /v1/subscription_plans, jadi tidak bisa dipilih lagi untuk langganan baru maupun import, sementara semua langganan yang sudah memakainya tetap ditagih seperti biasa. Mengarsipkan berarti menarik produk, bukan membatalkan orang yang sudah membelinya. Id yang sudah diarsipkan, atau bukan milik Anda, dijawab 404.
2. Mulai langganan
curl __BASE__/v1/subscriptions \
-H "Authorization: Bearer kp_test_..." \
-H "Content-Type: application/json" \
-d '{"customer_id":"cust_...","plan_id":"plan_..."}'Siklus pertama langsung ditagih, jadi invoice pertama sudah ada saat response ini kembali. Kalau plan-nya punya trial, belum ada invoice sampai trial-nya habis — trial yang tidak lanjut tidak pernah muncul di invoice sama sekali.
3. Menagih
Baca invoice-nya, ambil payment_request_id, lalu ambil itu dari GET /v1/transactions/{id} untuk dapat QRIS-nya. Invoice hidup lebih lama dari percobaan bayarnya: kode QRIS mati dalam sejam, invoice jatuh temponya beberapa hari, dan percobaan baru menggantikan yang sudah mati.
4. Beri akses lewat paid_through, bukan status
status itu posisi langganan dalam siklus hidupnya. paid_through itu boleh-tidaknya pelanggan memakai layanan Anda sekarang. Dua pertanyaan yang berbeda.
Pelanggan past_due yang masih di dalam periode yang sudah dia bayar tetap punya akses — memang untuk itu masa tenggangnya ada. Kalau Anda cek status saja, pelanggan yang membayar akan terputus begitu satu invoice telat.
const ok = sub.paid_through && new Date() < new Date(sub.paid_through);5. Kalau telat bayar
Invoice yang belum dibayar lewat masa tenggang memindahkan langganan ke past_due dan memicu invoice.overdue. Akses tetap jalan sampai paid_through habis sendiri. Bayar telat tetap melunasi invoice-nya dan memperpanjang akses dari situ — tidak ada yang hangus.
6. Ubah dan berhenti
POST /v1/subscriptions/{id}/change berlaku di perpanjangan berikutnya, dan hanya di situ. Tidak ada opsi langsung: proration belum ada di rilis ini, dan menerapkan upgrade sebelum dibayar sama saja memberi layanan yang belum disetujui siapa pun. Nilai yang dijadwalkan muncul sebagai pending_plan_id dan pending_quantity sampai perpanjangan menerapkannya.
Berhenti dengan at_period_end: true tetap menghormati layanan yang sudah dibayar. Defaultnya menghentikan akses sekarang. Keduanya tidak mengembalikan dana.
Berhenti itu final. Pembayaran yang datang setelahnya tetap melunasi invoice-nya — utangnya memang nyata — tapi tidak memberi akses dan tidak menghidupkan penagihan lagi. Cancel kedua kali dijawab 409 already_canceled, bukan karena request Anda salah.
7. Impor pelanggan langganan yang sudah ada
Pelanggan yang sudah berlangganan di tempat lain bisa dipindahkan sekaligus dalam satu file, dengan tanggal tagihan yang sama seperti sebelumnya, tanpa dibuat satu per satu. Di dashboard, buka Langganan → Impor CSV: unduh contoh CSV lalu isi, atau blok barisnya di Excel atau Google Sheets dan tempel langsung. Lalu klik Cek filenya. Pemeriksaan ini tidak membuat apa pun dan tidak mengirim pesan ke siapa pun; hasilnya menunjukkan baris yang siap, yang dilewati, dan yang ditolak beserta alasannya.
Baris yang ditolak diperbaiki langsung di halaman impor: nama, kontak, plan, dan tanggal. Plan yang disebut file tapi belum ada bisa dipetakan ke plan Anda yang lain atau dibuat dari situ. Kalau baris yang ditolak terlalu banyak untuk diperbaiki satu per satu, unduh baris yang ditolak (alasannya ada di satu kolom), perbaiki di spreadsheet, lalu unggah lagi. Baris yang ditolak tetap membawa nilai whatsapp dan no_email, jadi file yang sudah diperbaiki kembali dengan pilihan yang sama.
Pratinjau punya kotak centang WhatsApp dan Jangan email di setiap baris, plus satu tombol di atas masing-masing kolom untuk seluruh file. Baris tanpa nomor HP tidak bisa dicentang untuk WhatsApp, dan baris tanpa alamat email tidak punya email untuk dihentikan; kedua kotak itu nonaktif di sana beserta alasannya. Tombol untuk seluruh file hanya mengubah baris yang bisa membawa pilihan itu, dan tampil campuran kalau isi filenya campuran. Sebelum tombol impor, halaman menampilkan kredit WhatsApp yang bisa terpakai pada siklus pertama, dihitung hanya dari baris yang ikut WhatsApp dan punya nomor HP.
| Kolom | Isinya |
|---|---|
name | Wajib. |
email, phone | Minimal salah satu, supaya pelanggan bisa diingatkan. Email harus alamat yang valid dan nomor telepon 10 sampai 15 digit. Baris yang salah satunya keliru ditolak, bukan diimpor tanpa kontak itu. |
plan | Wajib. Nama, kode, atau id plan live yang sudah ada. Plan dengan masa trial ditolak: pelanggan yang diimpor bukan pelanggan baru. |
next_bill_date | Wajib. Tagihan berikutnya, bukan yang terakhir: 2026-10-01, 01/10/2026, atau 01-10-2026, dibaca tanggal lebih dulu. Tanggal yang sudah lewat ditolak. |
quantity | Opsional, bilangan bulat, 1 kalau kosong. |
external_id | Opsional, dan inilah yang membuat impor aman dijalankan ulang. |
whatsapp | Isi ya supaya pengingat dikirim lewat WhatsApp. Nomor HP saja bukan persetujuan. |
no_email | Isi ya supaya pelanggan ini tidak menerima email tagihan sama sekali: tanpa pengingat dan tanpa tanda terima. Kosong berarti email tetap dikirim kalau alamatnya ada. Alamat email saja tidak menentukan apa-apa. |
Kedua kolom pilihan membaca ya, yes, true, 1, atau y sebagai ya, dan isi lain sebagai tidak. Artinya mengikuti kolomnya: whatsapp=ya meminta WhatsApp, no_email=ya meminta tanpa email. Urutan kolom bebas, dan kolom di luar daftar ini diabaikan. Satu file paling banyak 500 baris dan 1 MB. Contohnya:
name,email,phone,plan,next_bill_date,quantity,external_id,whatsapp,no_email
Budi Santoso,budi@contoh.id,081234567890,Paket Bulanan,2026-11-01,1,PLG-001,ya,
Siti Rahayu,siti@contoh.id,,Paket Bulanan,2026-11-05,1,PLG-002,,ya
Warung Makan Sederhana,,081234567892,Paket Tahunan,2026-12-12,2,PLG-003,ya,Budi diingatkan lewat WhatsApp dan email. Siti tidak punya nomor HP dan memilih tanpa email, jadi invoicenya tetap terbit tanpa pesan ke siapa pun. Warung hanya diingatkan lewat WhatsApp. Dua langkah yang sama lewat API, dengan file sebagai body request:
curl __BASE__/v1/subscription_imports/sample -H "Authorization: Bearer kp_test_..." -o langganan.csv
curl "__BASE__/v1/subscription_imports?dry_run=true" -H "Authorization: Bearer kp_test_..." -H "Content-Type: text/csv" --data-binary @langganan.csv
curl __BASE__/v1/subscription_imports -H "Authorization: Bearer kp_test_..." -H "Content-Type: text/csv" --data-binary @langganan.csvKeduanya menjawab 200 dengan laporan: jumlah ready, created, skipped, dan failed, plus satu entri per baris berisi line sesuai nomor baris di file Anda, status, message, dan reason yang menyebut sel mana yang perlu diperbaiki. Masalah di satu baris tidak pernah menggagalkan request; 400 hanya untuk file yang tidak bisa dibaca, baris header tanpa name, plan, atau next_bill_date, atau file di atas 500 baris.
Menjalankan ulang aman. Baris yang external_id-nya sudah punya langganan dilaporkan skipped, bukan dibuka dua kali, dan pelanggan yang sudah tersimpan dipakai ulang lewat external_id, email, atau nomor HP, tidak diduplikasi. Jadi impor yang berhenti di tengah diselesaikan dengan mengirim file yang sama lagi. Tanpa external_id, impor ulang tidak bisa membedakan pengulangan dari langganan kedua, dan karena itu contoh CSV-nya sudah mengisi kolom ini.
Langganan hasil impor sama seperti langganan biasa: invoice pertamanya jatuh tempo pada next_bill_date, dan pengingatnya mengikuti tanggal itu, jadi baris yang diimpor sebulan lebih awal tidak mengirim pesan apa pun hari ini. Impor live butuh akun terverifikasi, dicek per baris; key sandbox bisa dipakai kapan saja. Endpoint impor belum tercantum di /v1/openapi.json; bagian inilah kontraknya.
Webhook
invoice.issued, invoice.paid, invoice.overdue, invoice.voided, invoice.uncollectible, dan event siklus hidup subscription.*. Isi body-nya sama persis dengan objek yang dikembalikan API ini, jadi yang Anda simpan dari webhook dan yang Anda ambil lewat GET itu barang yang sama.
Invoice yang tidak pernah dibayar dilepas 14 hari setelah jatuh tempo: statusnya jadi uncollectible dan langganannya ikut berhenti, jadi pelanggan yang berhenti membayar benar-benar berhenti, bukan menggantung di past due selamanya.
Pengiriman bersifat at-least-once. Dedup pakai Kasera-Event-Id dan anggap urutannya bisa tertukar — event itu penanda untuk membaca objeknya, bukan pengganti membacanya.
Test mode
Key kp_test_ membuat langganan sandbox. Payment request-nya sandbox dan tidak ada pengingat yang dikirim ke siapa pun, jadi nomor asli tidak akan pernah menerima pesan dari langganan yang hanya Anda coba-coba.
Simulasi perpanjangan
Menunggu sebulan untuk melihat invoice kedua bukanlah tes. Majukan saja jam langganan sandbox-nya:
curl __BASE__/v1/subscriptions/sub_.../advance \
-H "Authorization: Bearer kp_test_..." \
-H "Content-Type: application/json" \
-d '{"to":"2026-05-01T00:00:00+07:00"}'Ini sendiri tidak menagih apa pun. Yang dimajukan hanya jam langganannya; sweep perpanjangan lalu melakukan persis yang dia lakukan di produksi — menerbitkan invoice tiap siklus yang terlewat, membuat QRIS-nya, dan memperpanjang akses saat dibayar. Tunggu sebentar, lalu baca lagi langganannya.
Bisa dipanggil berkali-kali dan efeknya menumpuk, jadi Anda bisa menyusuri setahun perpanjangan satu per satu. Field test_clock menunjukkan posisi jamnya sekarang. Hanya bisa maju — memundurkan akan menaruh langganan di belakang siklus yang sudah ditagih.
Sandbox saja. Langganan live ditolak 422, dan database-nya juga menolak.
Yang belum ada
Siklus lebih dari setahun ditolak. Proration, kode kupon, dan pembayaran sebagian belum ada di rilis ini, dan auto-debit memang tidak ada — tiap siklus disetujui pelanggan.