Webhook

Webhook adalah cara Reader mengabari sistem Anda bahwa tagihan lunas, payout terkirim, atau ada kejadian lain. Reader mengirim POST berisi JSON ke callback_url aplikasi Anda, lalu menunggu balasan ack.

Halaman ini memuat semua aturan pengiriman. Contoh kode lengkap ada di Contoh kode.

Ringkasan dalam 30 detik

  1. Terima POST di callback_url.
  2. Verifikasi X-Reader-Signature dari body mentah, dengan toleransi waktu 5 menit.
  3. Cek event_id. Bila sudah pernah diproses, balas duplicate.
  4. Proses event secepatnya. Pekerjaan berat dipindah ke antrean.
  5. Dalam 10 detik, balas HTTP 2xx dengan:
{ "event_id": "evt_01j8zqa2b3c4d5e6f7g8h9j0km", "status": "accepted" }

Balasan 200 tanpa ack yang benar dianggap gagal dan dikirim ulang.

Menyiapkan endpoint

Atur di dashboard, menu Aplikasi:

Pengaturan Keterangan
Callback URL Wajib HTTPS, bisa diakses publik, dan tidak mengarah ke IP privat atau lokal. Dicek saat disimpan dan saat setiap pengiriman
Webhook secret whsec_…, dipakai untuk memverifikasi tanda tangan. Ditampilkan sekali saat aplikasi dibuat atau secret dirotasi
Wajib ack Menyala secara bawaan. Bila dimatikan, balasan 2xx apa pun dianggap berhasil. Tidak disarankan (lihat kenapa)
Kirim event mutasi tanpa pasangan mutation.unmatched. Mati secara bawaan
Kirim event konfirmasi tertunda payment.confirmation_delayed dan payout.confirmation_delayed. Mati secara bawaan
Kirim event kedaluwarsa payment.expired dan payout.expired. Mati secara bawaan

Satu aplikasi punya satu callback URL. Event dari kunci uji dan kunci live dikirim ke URL yang sama, dibedakan oleh field test.

Katalog event

Event Kapan dikirim Wajib ditangani?
payment.paid Tagihan lunas: normal, telat (late: true), setelah dibatalkan (paid_after_cancel: true), atau ditandai manual (match_type: manual_unverified) Ya
payment.duplicate Uang masuk lagi untuk tagihan yang sudah lunas Ya
payment.verified Tagihan yang ditandai lunas manual terbukti oleh notifikasi bank Opsional
payment.expired Tagihan kedaluwarsa. Belum final selama masa jeda 48 jam. Hanya bila diaktifkan Opsional
payment.confirmation_delayed Rekening tagihan menjadi tidak terpantau. Hanya bila diaktifkan Opsional
payout.sent Payout terkirim, termasuk telat dan hasil cek penerima (recipient_check) Ya, bila memakai payout
payout.duplicate Uang keluar lagi untuk payout yang sudah terkirim Ya, bila memakai payout
payout.reversed Bank mengembalikan transfer payout Ya, bila memakai payout
payout.verified Payout yang ditandai terkirim manual terbukti oleh notifikasi bank Opsional
payout.expired Payout kedaluwarsa. Belum final selama masa jeda. Hanya bila diaktifkan Opsional
payout.confirmation_delayed Rekening sumber payout menjadi tidak terpantau. Hanya bila diaktifkan Opsional
mutation.unmatched Uang masuk tanpa tagihan yang cocok. Hanya bila diaktifkan Opsional
account.terminated Layanan akun diputus admin. Webhook berhenti setelah event ini Ya
ping Tombol Uji endpoint di dashboard Balas ack

Balas accepted untuk jenis event yang tidak Anda kenal, supaya integrasi Anda tetap berjalan bila kelak ada jenis event baru.

Bentuk request

POST <callback_url>
Content-Type: application/json
User-Agent: RAGH-Reader-Webhook/1.0
X-Reader-Event-Id: evt_01j8zqa2b3c4d5e6f7g8h9j0km
X-Reader-Timestamp: 1790566265
X-Reader-Signature: sha256=5f2b9c…
Header Isi
X-Reader-Event-Id Sama dengan event_id di body
X-Reader-Timestamp Waktu kirim, Unix detik. Berbeda di setiap percobaan kirim
X-Reader-Signature sha256= + HMAC-SHA256 heksadesimal huruf kecil. Berbeda di setiap percobaan kirim

Body selalu berbentuk amplop yang sama:

{
  "event": "payment.paid",
  "event_id": "evt_01j8zqa2b3c4d5e6f7g8h9j0km",
  "api_version": "2026-09-28",
  "created_at": "2026-09-28T10:31:05+07:00",
  "test": false,
  "data": { }
}
Field Keterangan
event Jenis event
event_id ID unik event. Kunci idempotensi Anda
api_version Versi bentuk data
created_at Waktu event dibuat (bukan waktu kirim)
test true untuk data mode uji. Jangan memenuhi pesanan sungguhan dari event uji
data Isi event, lihat di bawah

Body sebuah event sama persis di setiap percobaan kirim. Yang berubah hanya header timestamp dan tanda tangan.

Isi data per event

Event Isi data
payment.paid Objek tagihan + mutation
payment.duplicate Objek tagihan (yang sudah lunas) + duplicate_mutation
payment.verified, payment.expired, payment.confirmation_delayed Objek tagihan
payout.sent Objek payout + mutation
payout.duplicate Objek payout (yang sudah terkirim) + duplicate_mutation
payout.reversed Objek payout (status reversed) + reversal_mutation
payout.verified, payout.expired, payout.confirmation_delayed Objek payout
mutation.unmatched Objek mutasi, seperti di GET /api/v1/mutations
account.terminated merchant_id, reason, at
ping message, app_id

mutation, duplicate_mutation, dan reversal_mutation adalah ringkasan mutasi:

{
  "id": "mut_01j8zqb7r2s3t4v5w6x7y8z9a0",
  "counterparty_name": "BUDI SANTOSO",
  "transaction_at": "2026-09-28T10:31:00+07:00",
  "received_at": "2026-09-28T10:31:04+07:00"
}

Pada payment.paid dan payout.sent yang ditandai manual dari dashboard, mutation bernilai null.

Contoh lengkap payment.paid:

{
  "event": "payment.paid",
  "event_id": "evt_01j8zqa2b3c4d5e6f7g8h9j0km",
  "api_version": "2026-09-28",
  "created_at": "2026-09-28T10:31:05+07:00",
  "test": false,
  "data": {
    "id": "pay_01j8zq5v3m7c2k9x4h6t1r0bna",
    "tx_number": "TKO-260928-0042",
    "reference_id": "ORDER-123",
    "amount": 200000,
    "unique_code": 123,
    "total_amount": 200123,
    "bank": { "code": "BCA", "name": "BCA", "account_number": "1234567890", "account_holder": "PT CONTOH JAYA" },
    "status": "paid",
    "confirmation_delayed": false,
    "late": false,
    "paid_after_cancel": false,
    "match_type": "auto",
    "test": false,
    "description": "Kursus A",
    "customer": { "ref": "CUST-9", "name": "Budi", "contact": "081234567890" },
    "tags": ["produk:kursus-a", "kanal:instagram"],
    "metadata": { "cart_id": "C-77" },
    "expires_at": "2026-09-29T10:15:00+07:00",
    "paid_at": "2026-09-28T10:31:04+07:00",
    "expired_at": null,
    "cancelled_at": null,
    "created_at": "2026-09-28T10:15:00+07:00",
    "payment_url": "https://payment-reader.ragh.co.id/pay/pay_01j8zq5v3m7c2k9x4h6t1r0bna",
    "mutation": {
      "id": "mut_01j8zqb7r2s3t4v5w6x7y8z9a0",
      "counterparty_name": "BUDI SANTOSO",
      "transaction_at": "2026-09-28T10:31:00+07:00",
      "received_at": "2026-09-28T10:31:04+07:00"
    }
  }
}

Contoh account.terminated:

{
  "event": "account.terminated",
  "event_id": "evt_01j8zz9x8w7v6t5s4r3q2p1n0m",
  "api_version": "2026-09-28",
  "created_at": "2026-09-30T09:00:00+07:00",
  "test": false,
  "data": { "merchant_id": "mer_01j8zk3m4n5p6q7r8s9t0v1w2x", "reason": "Pelanggaran syarat layanan", "at": "2026-09-30T09:00:00+07:00" }
}

Setelah account.terminated, semua kunci API dicabut dan tagihan pending dibatalkan. Hentikan pembuatan tagihan dan jangan arahkan pembeli ke halaman bayar lagi.

Verifikasi tanda tangan

Tanda tangan membuktikan request benar-benar dari Reader dan isinya tidak diubah.

X-Reader-Signature = "sha256=" + hex( HMAC_SHA256( webhook_secret, timestamp + "." + body ) )

Langkah verifikasi:

  1. Ambil body mentah persis seperti diterima, sebelum di-parse. Jangan mem-parse lalu meng-encode ulang JSON: urutan field, spasi, dan escape bisa berubah, sehingga tanda tangan tidak cocok.
  2. Ambil header X-Reader-Timestamp. Tolak bila bukan angka, atau selisihnya dengan jam server Anda lebih dari 300 detik. Ini mencegah request lama diputar ulang.
  3. Hitung HMAC-SHA256 dengan kunci = webhook secret (seluruh string whsec_…, apa adanya) dan pesan = timestamp + "." + body.
  4. Bentuk "sha256=" + hex huruf kecil, lalu bandingkan dengan header X-Reader-Signature memakai perbandingan waktu-konstan (hash_equals, crypto.timingSafeEqual, hmac.Equal, hmac.compare_digest).
  5. Bila tidak cocok, balas 401. Reader akan mengirim ulang sesuai jadwal.

Pastikan jam server Anda tersinkron (NTP). Jam yang meleset lebih dari 5 menit membuat semua webhook ditolak.

Kontrak ack

Setiap webhook wajib dibalas dalam 10 detik dengan HTTP 2xx dan body JSON:

{ "event_id": "evt_01j8zqa2b3c4d5e6f7g8h9j0km", "status": "accepted" }
status Arti Tindakan Reader
accepted Diterima dan (akan) diproses Selesai. Event delivered
duplicate Event ini sudah pernah Anda proses Selesai. Event delivered
rejected Anda menolak dengan sadar, misalnya "order tidak ditemukan". Sertakan reason Tidak dikirim ulang. Event rejected, dan kejadiannya masuk daftar perlu tindakan beserta alasannya. Uangnya tetap tercatat
retry Minta dikirim ulang nanti Dikirim ulang sesuai jadwal

Contoh menolak:

{ "event_id": "evt_01j8zqa2b3c4d5e6f7g8h9j0km", "status": "rejected", "reason": "order ORDER-123 tidak ditemukan" }

Pakai rejected hanya bila event memang tidak bisa diproses, dan perlu dilihat manusia. Untuk kejadian yang bisa Anda tangani sendiri (misalnya pesanan sudah dipenuhi), balas accepted atau duplicate.

Kenapa ack wajib

Banyak server membalas 200 untuk URL yang salah: halaman beranda, halaman login, atau rute cadangan aplikasi SPA. Tanpa ack, webhook yang tidak pernah diproses akan tampak "sukses". Dengan ack, Reader tahu pasti bahwa kode webhook Anda yang membalas.

Anda bisa mematikan kewajiban ack per aplikasi di dashboard. Setelah itu, balasan 2xx apa pun dianggap berhasil, termasuk dari URL yang salah.

Pemetaan respons

Setiap percobaan kirim dicatat dengan kode di dashboard. Kode yang sama muncul di last_error_code dan last_delivery.error_code pada GET /api/v1/webhook-events.

Respons endpoint Anda Kode Tindakan Reader Petunjuk
2xx + ack accepted / duplicate DELIVERED Selesai —
2xx + ack rejected REJECTED Tidak diulang. Masuk perlu tindakan Aplikasi Anda menolak event ini dengan sadar
2xx + ack retry RETRY_REQUESTED Diulang sesuai jadwal Aplikasi Anda meminta dikirim ulang
2xx tanpa ack yang valid INVALID_ACK Diulang Endpoint membalas 2xx tanpa ack yang benar. Kemungkinan URL-nya salah, atau kode belum membalas {"event_id": "...", "status": "accepted"}
3xx REDIRECT Redirect tidak diikuti. Diulang Perbarui callback URL ke alamat tujuan redirect. Sering terjadi karena http→https atau garis miring di akhir URL
401 / 403 SIGNATURE_REJECTED Diulang Tanda tangan ditolak. Periksa webhook secret yang dipakai aplikasi
404 NOT_FOUND Diulang Rute webhook tidak ada
408 / 429 BUSY Diulang, mengikuti Retry-After Server sibuk
410 GONE Endpoint dinonaktifkan. Semua pengiriman ke aplikasi ini dihentikan, dan Anda dikabari Aktifkan lagi dari dashboard setelah endpoint siap
4xx lain (400, 422, …) HTTP_4XX Diulang Payload ditolak. Periksa versi API dan validasi di aplikasi Anda
5xx HTTP_5XX Diulang Error di server aplikasi Anda
Tidak ada balasan dalam 10 detik TIMEOUT Diulang Balas dulu, lalu proses belakangan di antrean
Nama domain tidak ditemukan DNS_ERROR Diulang Periksa DNS domain callback
Masalah sertifikat TLS_ERROR Diulang Periksa sertifikat TLS endpoint (kedaluwarsa, rantai tidak lengkap, nama tidak cocok)
Koneksi gagal CONNECTION_ERROR Diulang Server mati, port tertutup, atau firewall menolak
Callback URL kosong NO_CALLBACK_URL Diulang Isi callback URL di pengaturan aplikasi
URL ditolak BLOCKED_URL Diulang Callback URL wajib HTTPS dan tidak boleh mengarah ke IP privat

Ack dianggap valid bila: status HTTP 2xx, body berupa objek JSON, event_id sama persis, dan status salah satu dari accepted, duplicate, rejected, retry. Header Content-Type balasan tidak diperiksa.

Yang disimpan untuk setiap percobaan: waktu, kode HTTP, 2 KB pertama body balasan, header Content-Type, Retry-After, dan Location, durasi, kode error, dan pemicunya. Detail percobaan disimpan 90 hari.

Jadwal kirim ulang

percobaan 1 : seketika
percobaan 2 : 1 menit setelah percobaan 1 gagal
percobaan 3 : 5 menit setelah percobaan 2 gagal
berikutnya  : tiap 15 menit
berhenti    : 72 jam sejak percobaan pertama → status failed

Selama gagal terus, pemilik dan admin akun dikabari lewat email (dan WhatsApp, bila nomor HP terisi di profil) setelah 1 jam, lalu setiap 24 jam selama belum pulih.

Pemutus sirkuit

Bila endpoint Anda gagal 3 kali berturut-turut, Reader membuka "sirkuit" untuk endpoint itu, supaya server Anda tidak dibanjiri request yang pasti gagal:

  1. Event lain untuk endpoint itu tidak dicoba satu per satu.
  2. Setiap 15 menit, hanya satu event tertua yang dikirim sebagai penguji.
  3. Begitu penguji berhasil, sirkuit ditutup, lalu semua event yang tertunda dikirim berurutan sesuai waktu dibuat.

Balasan ack apa pun, termasuk rejected dan retry, membuktikan endpoint bisa dihubungi, sehingga tidak dihitung sebagai kegagalan. Kirim ulang manual yang berhasil juga langsung menutup sirkuit.

Batas 72 jam tetap dihitung per event selama sirkuit terbuka.

Kirim ulang manual

Setelah memperbaiki server, Anda tidak perlu menunggu jadwal otomatis.

Cara Cakupan Aturan
Kirim ulang sekarang (per event), di dashboard atau lewat POST /api/v1/webhook-events/{id}/resend Satu event, berapa pun umurnya, termasuk yang failed atau rejected Dikirim seketika, melewati jadwal, pemutus sirkuit, dan jeda. Bila berhasil, event menjadi delivered dan sirkuit endpoint ditutup, sehingga antrean lain ikut mengalir. Bila gagal, dicatat sebagai percobaan manual dan jadwal otomatis tetap berjalan; batas 72 jam tidak diperpanjang. Paling banyak 1 kali per 60 detik per event
Kirim ulang semua yang gagal, di dashboard Hanya event yang gagal atau sedang menunggu jadwal ulang, yang dibuat dalam 30 menit terakhir, untuk aplikasi yang sedang dilihat Dashboard menampilkan jumlahnya lebih dulu. Event dikirim berurutan sesuai waktu dibuat, paling banyak 5 per detik. Tombol bisa dipakai lagi 5 menit setelah pemakaian terakhir

Kenapa tombol massal dibatasi 30 menit: ratusan event lama yang dikirim sekaligus bisa menjatuhkan server Anda yang baru pulih, dan lebih mungkin diproses dua kali oleh kode yang belum idempoten. Event yang lebih tua tetap diurus jadwal otomatis, atau bisa dikirim ulang satu per satu.

Jeda pengiriman

Saat maintenance, Anda bisa menjeda pengiriman dari dashboard, menu Webhook.

Endpoint dinonaktifkan (410)

Membalas 410 Gone berarti "endpoint ini sudah tidak ada". Reader langsung:

Event yang belum terkirim tetap disimpan. Setelah endpoint siap, tekan Aktifkan lagi di dashboard. Event yang menunggu dikirim di jadwal berikutnya, selama belum lewat 72 jam sejak percobaan pertamanya. Jangan pernah membalas 410 untuk kesalahan sementara.

Rotasi webhook secret

Merotasi secret dilakukan dari dashboard dan meminta OTP email. Secret baru langsung dipakai untuk semua percobaan kirim berikutnya, termasuk kirim ulang event lama.

Supaya tidak ada webhook yang ditolak saat pergantian:

  1. Ubah kode Anda agar sementara menerima tanda tangan dari secret lama atau secret baru.
  2. Rotasi secret di dashboard, lalu pasang secret baru di server.
  3. Setelah beberapa menit, hapus secret lama dari kode.

Webhook yang sempat ditolak (SIGNATURE_REJECTED) akan dikirim ulang sesuai jadwal.

Urutan dan idempotensi

Menangani telat dan dobel

Event dan tanda Artinya Yang disarankan
payment.paid, late: false Lunas tepat waktu Penuhi pesanan
payment.paid, late: true Pembeli membayar setelah batas waktu, dalam masa jeda 48 jam Penuhi pesanan. Pembeli sudah membayar. Bila stok habis, refund dan tetap balas accepted
payment.paid, paid_after_cancel: true Tagihan sudah dibatalkan, tetapi uang tetap masuk Aktifkan kembali dan penuhi pesanan, atau refund. Kejadian ini juga ada di daftar perlu tindakan
payment.paid, match_type: manual_unverified Ditandai lunas manual dari dashboard, notifikasi bank belum datang Proses seperti lunas biasa. payment.verified menyusul bila notifikasinya datang
payment.duplicate Uang masuk lagi untuk tagihan yang sudah lunas Jangan penuhi pesanan dua kali. Catat untuk refund
payment.expired Kedaluwarsa, tetapi belum final selama 48 jam Boleh tampilkan "menunggu pembayaran kedaluwarsa". Jangan hapus pesanan, dan jangan batalkan pesanan yang sudah dipenuhi
payout.sent, recipient_check: "mismatch" Uang sudah keluar, tetapi penerima di notifikasi bank berbeda Tandai pencairan selesai, lalu minta admin memeriksanya
payout.duplicate Penerima dibayar dua kali Minta dana kembali atau anggap pembayaran berikutnya
payout.reversed Bank mengembalikan transfer Tandai pencairan belum selesai. Untuk transfer ulang, buat payout baru dengan reference_id yang sama

Uji endpoint

Tombol Uji endpoint di halaman aplikasi mengirim event ping:

{
  "event": "ping",
  "event_id": "evt_01j8zv4w5x6y7z8a9b0c1d2e3f",
  "api_version": "2026-09-28",
  "created_at": "2026-09-28T09:00:00+07:00",
  "test": false,
  "data": { "message": "Uji webhook dari RAGH Reader", "app_id": "app_01j8zk7p8q9r0s1t2v3w4x5y6z" }
}

Balas dengan ack seperti biasa. Hasilnya langsung tampil di dashboard, lengkap dengan kode respons dan body balasan Anda.

Untuk menguji alur lengkap tagihan, pakai kunci uji dan POST /api/v1/test/simulate.

Daftar IP pengirim

Daftar IP pengirim webhook belum dipublikasikan. Halaman ini akan memuatnya setelah tersedia. Sementara itu, jangan membatasi endpoint hanya dengan allowlist IP. Verifikasi tanda tangan adalah perlindungan utamanya.

Checklist sebelum go-live