Aplikasi penerima (untuk developer)
Paygate mengirim POST JSON bertanda tangan ke alamat aplikasi tujuan. Aplikasimu cukup menyediakan satu endpoint yang: (1) memverifikasi tanda tangan, (2) mengabaikan event yang sudah diproses, (3) membalas 2xx.
Header
| Header | Isi |
|---|---|
X-Paygate-Signature |
t=<unix>,v1=<hex> dengan v1 = HMAC_SHA256(kunci_rahasia, "<unix>." + body_mentah) |
X-Paygate-Timestamp |
<unix> yang sama |
X-Paygate-Event-Id |
id event (sama dengan id di body) |
X-Paygate-Delivery-Id, X-Paygate-Attempt |
nomor pengiriman dan percobaan ke-berapa |
Gunakan body mentah untuk menghitung HMAC (jangan decode lalu encode ulang). Tolak jika |waktu_sekarang − t| > 300 detik. Bandingkan dengan fungsi constant-time.
Body
{
"id": "0b8e0c3e-…",
"provider": "xendit",
"provider_event_id": "…",
"event_type": "invoice.paid",
"reference_id": "IUR-2024-0001",
"status": "PAID",
"raw_status": "PAID",
"amount": "150000",
"currency": "IDR",
"payment_method": "BANK_TRANSFER",
"metadata": {},
"raw": { "...payload asli dari penyedia..." },
"test": false,
"target": "Iurana",
"matched_by": "rule:12#1",
"received_at": "2026-10-08T13:59:05+00:00"
}
status: PAID, PENDING, FAILED, EXPIRED, REFUNDED, UNKNOWN. Jika test bernilai true, itu tes dari tombol "Uji koneksi"/"Kirim tes": balas 200 dan jangan ubah data.
Aturan emas
- Idempoten — simpan
idevent yang sudah diproses; jika datang lagi, balas 200 tanpa memproses ulang. - Jangan menurunkan tagihan yang sudah lunas karena event
EXPIRED/PENDINGyang datang terlambat. - Cocokkan nominal dengan tagihanmu sebelum melunasi.
- Balas 2xx cepat (batas waktu default 10 detik); pekerjaan berat masukkan ke antrean.
Balasan dan percobaan ulang
| Balasan | Hasil |
|---|---|
| 2xx | Sukses |
| 4xx/5xx, timeout, tidak tersambung | Dicoba lagi: 10 dtk, 30 dtk, 1, 5, 15, 30 mnt, 1, 2, 6 jam; setelah itu ditandai Gagal (bisa dikirim ulang manual) |
| 3xx (redirect) | Dianggap gagal — pakai alamat akhir |
Contoh PHP (Laravel)
public function __invoke(Request $request)
{
$body = $request->getContent();
if (! preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', (string) $request->header('X-Paygate-Signature'), $m)) abort(401);
[, $t, $v1] = $m;
abort_if(abs(time() - (int) $t) > 300, 401);
abort_unless(hash_equals(hash_hmac('sha256', $t . '.' . $body, config('services.paygate.secret')), $v1), 401);
$e = $request->json()->all();
if ($e['test'] ?? false) return response()->json(['ok' => true]);
if (! DB::table('paygate_events')->insertOrIgnore(['event_id' => $e['id'], 'created_at' => now()])) {
return response()->json(['ok' => true]); // duplikat
}
$invoice = Invoice::where('reference', $e['reference_id'])->first();
if ($invoice && $e['status'] === 'PAID' && $invoice->status !== 'paid') {
$invoice->markPaid();
}
return response()->json(['ok' => true]);
}
Jangan taruh route ini di grup yang memakai CSRF atau login sesi. Tabel idempotensi: paygate_events(event_id char(36) primary key, created_at).