Webhook & event
ZePay mengirim POST JSON bertanda tangan ke endpoint webhook Anda setiap kali ada perubahan pada pembayaran, saldo, pencairan, atau rekening — 13 jenis event. Endpoint diatur sendiri lewat API (Webhook & event).
Atur endpoint lewat API#
// tambah endpoint (maks. 3 per mode); secret hanya muncul sekali di respons ini
const hook = await zepay('POST', '/api/v2/webhook-endpoints',
{ url: 'https://tokobatik.id/hooks/zepay', events: ['payment.*', 'payout.*', 'balance.*', 'bank_account.*'] }, 'webhook-setup-tokobatik-1');
// hook.secret → "whsec_…" (simpan sebagai ZEPAY_WEBHOOK_SECRET)
await zepay('POST', `/api/v2/webhook-endpoints/${hook.id}/test`); // kirim event webhook.test
await zepay('POST', `/api/v2/webhook-endpoints/${hook.id}/rotate-secret`, // secret lama tetap ikut
{ previous_secret_valid_hours: 24 }, 'rotate-' + Date.now()); // ditandatangani 24 jam
// kejar webhook yang terlewat (mis. server Anda sempat mati)
const missed = await zepay('GET', '/api/v2/events?delivery_status=failed&limit=100');
for (const ev of missed.data) await zepay('POST', `/api/v2/events/${ev.id}/resend`);Tanpa events, endpoint berlangganan payment.*, payout.*, balance.*, dan bank_account.*. URL wajib https:// dan mengarah ke alamat publik.
Pengiriman & verifikasi tanda tangan#
Setiap event dikirim sebagai POST JSON { id, type, created, livemode, data } dengan header ZePay-Event-Id (= id), ZePay-Event-Type, ZePay-Signature, dan User-Agent: ZePay-Webhook/2.0.
Header ZePay-Signature: t=<unix>,v1=<hex>; v1 = HMAC_SHA256(webhook_secret, t + "." + body_mentah). Hitung dari body mentah (byte persis yang diterima), bukan dari JSON yang di-parse lalu di-stringify ulang. Selama masa transisi rotasi ada dua v1 (secret baru & lama) — terima bila salah satu cocok. Tolak bila selisih waktu > 300 detik (t dibuat ulang di setiap percobaan kirim). Simpan id event agar idempoten; balas 2xx dalam 10 detik — redirect tidak diikuti. Gagal → dicoba ulang 1 mnt, 5 mnt, 15 mnt, 30 mnt, 1 jam, 2 jam, 4 jam, 8 jam, dan 8 jam kemudian (10 percobaan, ±24 jam), lalu berhenti (dead); event 30 hari terakhir ada di GET /events dan bisa dikirim ulang.
app.post('/hooks/zepay', express.raw({ type: 'application/json' }), (req, res) => {
const pairs = req.get('ZePay-Signature').split(',').map(p => p.split('='));
const t = pairs.find(([k]) => k === 't')?.[1];
const sigs = pairs.filter(([k]) => k === 'v1').map(([, v]) => v); // 2 nilai saat rotasi secret
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', process.env.ZEPAY_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest('hex');
const ok = sigs.some(v => v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected)));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body);
// simpan event.id; proses event.type
res.sendStatus(200);
});| Event | Kapan | data |
|---|---|---|
payment.succeeded |
Pembayaran berhasil, dana masuk saldo tertunda | payment_id, reference, amount, fee (biaya transaksi final), fee_bearer, net_amount, method, channel, paid_at, metadata |
payment.expired / payment.failed / payment.cancelled |
Tagihan kedaluwarsa / ditolak penerbit atau gagal diterbitkan / dibatalkan | payment_id, reference, amount, status, reason (mis. Tagihan kedaluwarsa, Ditolak penerbit, Gagal diterbitkan kanal pembayaran (kode …)), metadata |
balance.settled |
Dana pindah ke saldo tersedia | payment_id, reference, amount (= net_amount) |
payout.created |
Pencairan dibuat; nominal + biaya transfer ditahan | payout_id, reference, amount, fee (ditahan), total_debited, status, combined |
payout.succeeded |
Diterima rekening tujuan | payout_id, reference, amount, fee (final), total_debited |
payout.failed |
Gagal; nominal + biaya kembali | payout_id, reference, reason |
payout.cancelled |
Dibatalkan / ditolak; nominal + biaya kembali | payout_id, reference, reason |
bank_account.created |
Rekening terdaftar (verified / pending_review) |
objek rekening |
bank_account.verified |
Rekening bisa menerima pencairan | objek rekening |
bank_account.rejected |
Rekening ditolak (alasan di review_note) |
objek rekening |
webhook.test |
Event uji dari POST /webhook-endpoints/{id}/test |
endpoint_id, message |
Waktu di dalam data (mis. paid_at, verified_at) ditulis server dalam ISO 8601 dengan offset lain (mis. +00:00); selalu parse sebagai ISO 8601 lengkap. Field waktu lain di API memakai +07:00.
Header pengiriman#
Setiap pengiriman membawa header berikut. Verifikasi ZePay-Signature sebelum memproses isi event.
| Nama | Di | Tipe | Keterangan |
|---|---|---|---|
ZePay-Event-Idwajib | header | string | Sama dengan |
ZePay-Event-Typewajib | header | string | Nilai: |
ZePay-Signaturewajib | header | string |
|
Daftar event#
Semua event memakai amplop yang sama (id, type, created_at, livemode, data); isi data berbeda per jenis.
Pembayaran berhasil (dana masuk saldo tertunda)
data.fee = biaya transaksi final; data.net_amount = yang masuk saldo tertunda.
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.payment_idwajib | string (uuid) | |
data.referencewajib | string | |
data.amountwajib | integer | Rupiah, bilangan bulat. (min 0) |
data.feewajib | integer | Biaya transaksi final. (min 0) |
data.fee_bearerwajib | string | merchantcustomer |
data.net_amountwajib | integer | Yang masuk saldo tertunda. (min 0) |
data.methodwajib | string | |
data.channelwajib | string | null | |
data.paid_atwajib | string (date-time) | |
data.metadatawajib | object |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Tagihan kedaluwarsa tanpa pembayaran
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.payment_idwajib | string (uuid) | |
data.referencewajib | string | |
data.amountwajib | integer | Rupiah, bilangan bulat. (min 0) |
data.statuswajib | string | EXPIREDFAILEDCANCELLED |
data.reasonwajib | string | null | |
data.method | string | null | Null untuk payment link yang belum dipilih metodenya. QRISVAEWALLETRETAIL |
data.channel | string | null | |
data.metadatawajib | object |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Pembayaran ditolak penerbit (mis. saldo e-wallet kurang)
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.payment_idwajib | string (uuid) | |
data.referencewajib | string | |
data.amountwajib | integer | Rupiah, bilangan bulat. (min 0) |
data.statuswajib | string | EXPIREDFAILEDCANCELLED |
data.reasonwajib | string | null | |
data.method | string | null | Null untuk payment link yang belum dipilih metodenya. QRISVAEWALLETRETAIL |
data.channel | string | null | |
data.metadatawajib | object |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Tagihan dibatalkan
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.payment_idwajib | string (uuid) | |
data.referencewajib | string | |
data.amountwajib | integer | Rupiah, bilangan bulat. (min 0) |
data.statuswajib | string | EXPIREDFAILEDCANCELLED |
data.reasonwajib | string | null | |
data.method | string | null | Null untuk payment link yang belum dipilih metodenya. QRISVAEWALLETRETAIL |
data.channel | string | null | |
data.metadatawajib | object |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Dana pindah dari tertunda ke tersedia (settlement)
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.payment_idwajib | string (uuid) | |
data.referencewajib | string | |
data.amountwajib | integer |
(min 0) |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Pencairan dibuat; nominal + biaya transfer ditahan
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.payout_idwajib | string (uuid) | |
data.referencewajib | string | |
data.amountwajib | integer | Rupiah, bilangan bulat. (min 0) |
data.feewajib | integer | Biaya transfer yang ditahan (hasil pengecekan ke bank tujuan; batas atas). (min 0) |
data.total_debitedwajib | integer | Rupiah, bilangan bulat. (min 0) |
data.statuswajib | string | PROCESSINGPENDING_APPROVAL |
data.combinedwajib | boolean |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Pencairan diterima rekening tujuan; biaya transfer final terpotong
data.fee = biaya transfer final. Bila lebih kecil dari yang ditahan, selisihnya sudah kembali ke saldo tersedia.
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.payout_idwajib | string (uuid) | |
data.referencewajib | string | |
data.amountwajib | integer | Rupiah, bilangan bulat. (min 0) |
data.feewajib | integer | Biaya transfer final. (min 0) |
data.total_debitedwajib | integer | Rupiah, bilangan bulat. (min 0) |
{
"id": "evt_0c2e4a6b8d1f4c3e9a5b7d9f1e3a5c70",
"type": "payout.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payout_id": "9d0f3c1a-2b7e-4f55-8a19-6c3e2d1b0a77",
"reference": "WD-01JBQ9S2ZK7MX4D8E2R5T0WQ3N",
"amount": 100000,
"fee": 2000,
"total_debited": 102000
}
}Pencairan gagal; nominal dan biaya kembali ke saldo tersedia
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.payout_idwajib | string (uuid) | |
data.referencewajib | string | |
data.reasonwajib | string | null |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Pencairan dibatalkan merchant / ditolak admin; nominal dan biaya kembali
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.payout_idwajib | string (uuid) | |
data.referencewajib | string | |
data.reasonwajib | string | null |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Rekening terdaftar (status `verified` atau `pending_review`)
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.idwajib | string (uuid) | |
data.bank_codewajib | string | |
data.bank_namewajib | string | |
data.typewajib | string | bankewallet |
data.account_number_maskedwajib | string | |
data.account_namewajib | string | Nama pemilik dari hasil cek rekening |
data.labelwajib | string | null | |
data.statuswajib | string |
pending_reviewverifiedrejecteddisabled |
data.is_defaultwajib | boolean | |
data.review_notewajib | string | null | Alasan bila ditolak admin |
data.verified_atwajib | string (date-time) | null | |
data.created_atwajib | string (date-time) |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Rekening terverifikasi dan bisa menerima pencairan
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.idwajib | string (uuid) | |
data.bank_codewajib | string | |
data.bank_namewajib | string | |
data.typewajib | string | bankewallet |
data.account_number_maskedwajib | string | |
data.account_namewajib | string | Nama pemilik dari hasil cek rekening |
data.labelwajib | string | null | |
data.statuswajib | string |
pending_reviewverifiedrejecteddisabled |
data.is_defaultwajib | boolean | |
data.review_notewajib | string | null | Alasan bila ditolak admin |
data.verified_atwajib | string (date-time) | null | |
data.created_atwajib | string (date-time) |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Rekening ditolak admin (alasan di `review_note`)
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.idwajib | string (uuid) | |
data.bank_codewajib | string | |
data.bank_namewajib | string | |
data.typewajib | string | bankewallet |
data.account_number_maskedwajib | string | |
data.account_namewajib | string | Nama pemilik dari hasil cek rekening |
data.labelwajib | string | null | |
data.statuswajib | string |
pending_reviewverifiedrejecteddisabled |
data.is_defaultwajib | boolean | |
data.review_notewajib | string | null | Alasan bila ditolak admin |
data.verified_atwajib | string (date-time) | null | |
data.created_atwajib | string (date-time) |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}Event uji dari `POST /webhook-endpoints/{id}/test`
Isi data
| Field | Tipe | Keterangan |
|---|---|---|
data.endpoint_idwajib | string (uuid) | |
data.messagewajib | string |
{
"id": "evt_4f2a9c1e7b3d4e8a9c0b1d2e3f405162",
"type": "payment.succeeded",
"created": "2026-10-31T10:44:12+07:00",
"livemode": true,
"data": {
"payment_id": "2c6e1f0a-7d3b-4a9e-8f21-5b0c9d4e7a13",
"reference": "SKR-01JBQ7X4M2RKD8T5W9C3HZPN6E",
"amount": 250000,
"fee": 1750,
"fee_bearer": "merchant",
"net_amount": 248250,
"method": "QRIS",
"channel": "QRIS",
"paid_at": "2026-10-31T03:44:11.721+00:00",
"metadata": {
"order_id": "1042"
}
}
}