Lewati ke konten
ZePayDokumentasi API
API v2.6.0OpenAPI
Menu dokumentasi
Webhook · API v2.6.0

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#

JavaScript
// 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.

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

NamaDiTipeKeterangan
ZePay-Event-Idwajibheaderstring

Sama dengan id event (evt_…); juga dikirim pada format v1_legacy. Simpan untuk pemrosesan idempoten.

ZePay-Event-Typewajibheaderstring

Nilai: payment.succeeded, payment.expired, payment.failed, payment.cancelled, balance.settled, payout.created, payout.succeeded, payout.failed, payout.cancelled, bank_account.created, bank_account.verified, bank_account.rejected, webhook.test

ZePay-Signaturewajibheaderstring

t=<unix>,v1=<hex> dengan v1 = hex(HMAC_SHA256(webhook_secret, t + "." + body_mentah)). Selama masa transisi rotasi secret ada dua v1 (secret baru lalu secret lama) — terima bila salah satu cocok. t dibuat ulang pada setiap percobaan kirim.

Daftar event#

Semua event memakai amplop yang sama (id, type, created_at, livemode, data); isi data berbeda per jenis.

EVENTpayment.succeeded#

Pembayaran berhasil (dana masuk saldo tertunda)

data.fee = biaya transaksi final; data.net_amount = yang masuk saldo tertunda.

Isi data

FieldTipeKeterangan
data.payment_idwajibstring (uuid)
data.referencewajibstring
data.amountwajibinteger

Rupiah, bilangan bulat.

(min 0)

data.feewajibinteger

Biaya transaksi final.

(min 0)

data.fee_bearerwajibstring
merchantcustomer
data.net_amountwajibinteger

Yang masuk saldo tertunda.

(min 0)

data.methodwajibstring
data.channelwajibstring | null
data.paid_atwajibstring (date-time)
data.metadatawajibobject
Contoh payload payment.succeeded
{
  "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"
    }
  }
}
EVENTpayment.expired#

Tagihan kedaluwarsa tanpa pembayaran

Isi data

FieldTipeKeterangan
data.payment_idwajibstring (uuid)
data.referencewajibstring
data.amountwajibinteger

Rupiah, bilangan bulat.

(min 0)

data.statuswajibstring
EXPIREDFAILEDCANCELLED
data.reasonwajibstring | null
data.methodstring | null

Null untuk payment link yang belum dipilih metodenya.

QRISVAEWALLETRETAIL
data.channelstring | null
data.metadatawajibobject
Contoh payload payment.expired
{
  "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"
    }
  }
}
EVENTpayment.failed#

Pembayaran ditolak penerbit (mis. saldo e-wallet kurang)

Isi data

FieldTipeKeterangan
data.payment_idwajibstring (uuid)
data.referencewajibstring
data.amountwajibinteger

Rupiah, bilangan bulat.

(min 0)

data.statuswajibstring
EXPIREDFAILEDCANCELLED
data.reasonwajibstring | null
data.methodstring | null

Null untuk payment link yang belum dipilih metodenya.

QRISVAEWALLETRETAIL
data.channelstring | null
data.metadatawajibobject
Contoh payload payment.failed
{
  "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"
    }
  }
}
EVENTpayment.cancelled#

Tagihan dibatalkan

Isi data

FieldTipeKeterangan
data.payment_idwajibstring (uuid)
data.referencewajibstring
data.amountwajibinteger

Rupiah, bilangan bulat.

(min 0)

data.statuswajibstring
EXPIREDFAILEDCANCELLED
data.reasonwajibstring | null
data.methodstring | null

Null untuk payment link yang belum dipilih metodenya.

QRISVAEWALLETRETAIL
data.channelstring | null
data.metadatawajibobject
Contoh payload payment.cancelled
{
  "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"
    }
  }
}
EVENTbalance.settled#

Dana pindah dari tertunda ke tersedia (settlement)

Isi data

FieldTipeKeterangan
data.payment_idwajibstring (uuid)
data.referencewajibstring
data.amountwajibinteger

net_amount pembayaran yang pindah ke saldo tersedia.

(min 0)

Contoh payload balance.settled
{
  "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"
    }
  }
}
EVENTpayout.created#

Pencairan dibuat; nominal + biaya transfer ditahan

Isi data

FieldTipeKeterangan
data.payout_idwajibstring (uuid)
data.referencewajibstring
data.amountwajibinteger

Rupiah, bilangan bulat.

(min 0)

data.feewajibinteger

Biaya transfer yang ditahan (hasil pengecekan ke bank tujuan; batas atas).

(min 0)

data.total_debitedwajibinteger

Rupiah, bilangan bulat.

(min 0)

data.statuswajibstring
PROCESSINGPENDING_APPROVAL
data.combinedwajibboolean
Contoh payload payout.created
{
  "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"
    }
  }
}
EVENTpayout.succeeded#

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

FieldTipeKeterangan
data.payout_idwajibstring (uuid)
data.referencewajibstring
data.amountwajibinteger

Rupiah, bilangan bulat.

(min 0)

data.feewajibinteger

Biaya transfer final.

(min 0)

data.total_debitedwajibinteger

Rupiah, bilangan bulat.

(min 0)

Contoh payload payout.succeeded
{
  "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
  }
}
EVENTpayout.failed#

Pencairan gagal; nominal dan biaya kembali ke saldo tersedia

Isi data

FieldTipeKeterangan
data.payout_idwajibstring (uuid)
data.referencewajibstring
data.reasonwajibstring | null
Contoh payload payout.failed
{
  "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"
    }
  }
}
EVENTpayout.cancelled#

Pencairan dibatalkan merchant / ditolak admin; nominal dan biaya kembali

Isi data

FieldTipeKeterangan
data.payout_idwajibstring (uuid)
data.referencewajibstring
data.reasonwajibstring | null
Contoh payload payout.cancelled
{
  "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"
    }
  }
}
EVENTbank_account.created#

Rekening terdaftar (status `verified` atau `pending_review`)

Isi data

FieldTipeKeterangan
data.idwajibstring (uuid)
data.bank_codewajibstring
data.bank_namewajibstring
data.typewajibstring
bankewallet
data.account_number_maskedwajibstring
data.account_namewajibstring

Nama pemilik dari hasil cek rekening

data.labelwajibstring | null
data.statuswajibstring

verified bisa menerima pencairan · pending_review menunggu tinjauan · rejected ditolak · disabled dinonaktifkan

pending_reviewverifiedrejecteddisabled
data.is_defaultwajibboolean
data.review_notewajibstring | null

Alasan bila ditolak admin

data.verified_atwajibstring (date-time) | null
data.created_atwajibstring (date-time)
Contoh payload bank_account.created
{
  "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"
    }
  }
}
EVENTbank_account.verified#

Rekening terverifikasi dan bisa menerima pencairan

Isi data

FieldTipeKeterangan
data.idwajibstring (uuid)
data.bank_codewajibstring
data.bank_namewajibstring
data.typewajibstring
bankewallet
data.account_number_maskedwajibstring
data.account_namewajibstring

Nama pemilik dari hasil cek rekening

data.labelwajibstring | null
data.statuswajibstring

verified bisa menerima pencairan · pending_review menunggu tinjauan · rejected ditolak · disabled dinonaktifkan

pending_reviewverifiedrejecteddisabled
data.is_defaultwajibboolean
data.review_notewajibstring | null

Alasan bila ditolak admin

data.verified_atwajibstring (date-time) | null
data.created_atwajibstring (date-time)
Contoh payload bank_account.verified
{
  "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"
    }
  }
}
EVENTbank_account.rejected#

Rekening ditolak admin (alasan di `review_note`)

Isi data

FieldTipeKeterangan
data.idwajibstring (uuid)
data.bank_codewajibstring
data.bank_namewajibstring
data.typewajibstring
bankewallet
data.account_number_maskedwajibstring
data.account_namewajibstring

Nama pemilik dari hasil cek rekening

data.labelwajibstring | null
data.statuswajibstring

verified bisa menerima pencairan · pending_review menunggu tinjauan · rejected ditolak · disabled dinonaktifkan

pending_reviewverifiedrejecteddisabled
data.is_defaultwajibboolean
data.review_notewajibstring | null

Alasan bila ditolak admin

data.verified_atwajibstring (date-time) | null
data.created_atwajibstring (date-time)
Contoh payload bank_account.rejected
{
  "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"
    }
  }
}
EVENTwebhook.test#

Event uji dari `POST /webhook-endpoints/{id}/test`

Isi data

FieldTipeKeterangan
data.endpoint_idwajibstring (uuid)
data.messagewajibstring
Contoh payload webhook.test
{
  "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"
    }
  }
}