Webhook Documentation

Cara menerima notifikasi real-time saat ada pembayaran

Daftar Isi

  1. Overview
  2. Events
  3. Payload Format
  4. Verifikasi Signature
  5. Retry Logic
  6. Uji Coba Webhook
  7. API Endpoints
  8. Contoh: Node.js (Express)
  9. Contoh: Python (Flask)
  10. Contoh: PHP

Overview

Webhook adalah mekanisme push notification. Daripada server kamu harus polling (mengecek berkala), server kami yang akan mengirim data ke URL kamu saat event terjadi.

Cara kerja:

1
Daftarkan Webhook

Masukkan URL endpoint kamu di Dashboard → Webhook

2
Pilih Events

Tentukan event mana yang ingin didengarkan (payment.success, shop.created, dll)

3
Terima Notifikasi

Server kami mengirim POST request ke URL kamu saat event terjadi

4
Verifikasi Signature

Pastikan request benar dari server kami dengan verifikasi HMAC-SHA256

Events

Gunakan event ID di bawah ini saat konfigurasi webhook.

payment.created Pembayaran baru dibuat
payment.success Pembayaran berhasil
payment.cancelled Pembayaran dibatalkan
payment.expired Pembayaran kedaluwarsa
shop.created Pembelian role baru
shop.success Role berhasil diaktifkan
shop.expired Pembelian role kedaluwarsa

Payload Format

Setiap webhook dikirim sebagai POST request dengan body JSON:

{ "event": "payment.success", "timestamp": "2025-07-27T10:30:00.000Z", "data": { "id": "mqd123abc", "amount": 50000, "status": "succeed", "qr_image_url": "https://...", "balance_before": 100000, "balance_after": 150000, "increase": 50000 } }

Field yang dikirim per event

FieldTipeDeskripsi
eventstringNama event (contoh: payment.success)
timestampstringWaktu event dalam ISO 8601
data.idstringID payment atau shop payment
data.amountnumberNominal dalam Rupiah
data.statusstringStatus: pending, succeed, cancelled, expired
data.qr_image_urlstringURL gambar QR (hanya di event *.created)
data.balance_beforenumberSaldo QRIS sebelum transaksi
data.balance_afternumberSaldo QRIS sesudah transaksi (hanya di *.success)
data.increasenumberSelisih saldo (hanya di *.success)
data.rolestringNama role (hanya di event shop.*)
data.role_expires_atstringWaktu role kadaluarsa (hanya di shop.success)

Verifikasi Signature

Setiap request dilengkapi header X-Webhook-Signature berisi HMAC-SHA256 signature. Verifikasi untuk memastikan request benar dari server kami.

Header yang dikirim

HeaderDeskripsi
Content-Typeapplication/json
X-Webhook-SignatureHMAC-SHA256 signature
X-Webhook-EventNama event
User-AgentPaymetGateway-Webhook/1.0

Cara verifikasi

// Signature = HMAC-SHA256(secret_key, request_body) // Bandingkan signature dari header dengan yang kamu hitung sendiri const crypto = require('crypto'); function verifySignature(secret, body, signature) { const expected = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); }
Penting: Selalu gunakan crypto.timingSafeEqual() untuk perbandingan signature. Jangan gunakan === karena rentan timing attack.

Retry Logic

Jika server kamu return error (bukan status 2xx), kami akan retry otomatis:

AttemptDelayKeterangan
10 detikLangsung kirim
230 detikRetry pertama
32 menitRetry kedua
Gagal—Log sebagai failed, stop
Tips: Return 200 OK atau 201 Created untuk menandakan webhook berhasil diterima.

Uji Coba Webhook

Tidak perlu langsung punya server penerima. Ada 3 cara menguji.

1. Receiver Bawaan (Tanpa Server Eksternal)

Halaman Webhook punya receiver bawaan di POST /api/webhook/receive-test.

1
Buka halaman Webhook

Kunjungi https://orkut.nickystore.my.id/webhook

2
Tekan "Kirim Test ke Receiver"

Sistem mengirim webhook ke receiver-nya sendiri

3
Lihat hasil di "Hasil Test Receiver"

Payload, header X-Webhook-Signature, dan badge validasi

Badge validasi:

Bisa juga mengarahkan URL config ke receiver bawaan agar event asli (bukan cuma test) ikut tercatat: https://orkut.nickystore.my.id/api/webhook/receive-test

2. Script Test Lokal (Port 5543)

Script bawaan scripts/webhook-receiver.js menyediakan server penerima lokal:

npm run webhook:test
export WEBHOOK_SECRET=<secret dari halaman webhook> npm run webhook:test

Hasil signature: VALID ✓ (cocok), TIDAK COCOK ✗ (salah), SKIPPED (secret belum diset). Ganti port: node scripts/webhook-receiver.js 9999

Penting: Server live mengirim webhook dari internet. URL lokal hanya berguna kalau bisa diakses dari luar — gunakan ngrok, tunnel, atau URL publik (mis. forwarded port Codespaces / GitHub).

3. URL Publik (Tunnel)

Pakai URL publik seperti https://nama-tunnel.app.github.dev/webhook sebagai URL config di halaman webhook, lalu tekan Simpan dan Kirim Test. Pastikan script receiver (cara 2) berjalan di port yang terbuka.

API Endpoints

GET /api/webhook/configs

Lihat semua konfigurasi webhook milik user.

GET /api/webhook/configs
{ "success": true, "configs": [ { "_id": "...", "userId": 123456, "url": "https://example.com/webhook", "secret": "abc123...", "events": ["payment.success", "shop.success"], "active": true } ] }

POST /api/webhook/config

Buat atau update konfigurasi webhook.

POST /api/webhook/config
FieldTipeDeskripsi
urlstringURL endpoint kamu (wajib, harus valid URL)
eventsarrayDaftar event ID (minimal 1)
Request: { "url": "https://example.com/webhook", "events": ["payment.success", "payment.created"] } Response: { "success": true, "secret": "a1b2c3d4e5f6..." }
Simpan secret! Secret hanya ditampilkan sekali saat pertama kali membuat config. Gunakan untuk verifikasi signature.

DELETE /api/webhook/config/:id

Hapus konfigurasi webhook.

DELETE /api/webhook/config/:id

POST /api/webhook/config/toggle

Aktifkan atau nonaktifkan webhook.

POST /api/webhook/config/toggle
{ "id": "...", "active": true }

POST /api/webhook/test/:id

Test kirim webhook test ke URL kamu.

POST /api/webhook/test/:id
Response: { "success": true, "result": { "status": 200, "body": "OK", "ok": true } }

GET /api/webhook/logs

Lihat riwayat pengiriman webhook (50 terakhir).

GET /api/webhook/logs

POST /api/webhook/test-local

Test kirim webhook ke receiver bawaan (receive-test) tanpa keluar server.

POST /api/webhook/test-local
Response: { "success": true, "receiptId": "webhook_receipt_id", "signature_valid": true, "config_found": true }

POST /api/webhook/receive-test

Receiver bawaan publik. Cocokkan X-Webhook-Signature (HMAC-SHA256) dengan secret config pemilik, lalu simpan ke webhook_receipts.

POST /api/webhook/receive-test

Bisa dipakai sebagai URL config agar event asli ikut tercatat.

Response: { "success": true, "receiptId": "webhook_receipt_id", "owner": "NICKY", "signature_valid": true }

GET /api/webhook/receipts

Lihat bukti webhook yang masuk ke receiver bawaan (test maupun event asli).

GET /api/webhook/receipts

Contoh: Node.js (Express)

const express = require('express'); const crypto = require('crypto'); const app = express(); const WEBHOOK_SECRET = 'secret-dari-dashboard'; app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { // 1. Verifikasi signature const signature = req.headers['x-webhook-signature']; const expected = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(req.body) .digest('hex'); if (signature !== expected) { return res.status(401).json({ error: 'Invalid signature' }); } // 2. Parse payload const event = JSON.parse(req.body); // 3. Proses berdasarkan event switch (event.event) { case 'payment.created': console.log('Payment baru:', event.data.id); console.log('Nominal:', event.data.amount); break; case 'payment.success': console.log('Payment berhasil:', event.data.id); console.log('Saldo naik:', event.data.increase); // Kirim notifikasi ke customer, update database, dll break; case 'payment.cancelled': console.log('Payment dibatalkan:', event.data.id); break; case 'shop.success': console.log('Role diaktifkan:', event.data.role); break; } res.json({ received: true }); } ); app.listen(3000);

Contoh: Python (Flask)

from flask import Flask, request, jsonify import hmac import hashlib app = Flask(__name__) WEBHOOK_SECRET = 'secret-dari-dashboard' @app.route('/webhook', methods=['POST']) def webhook(): # 1. Verifikasi signature signature = request.headers.get('X-Webhook-Signature', '') expected = hmac.new( WEBHOOK_SECRET.encode(), request.data, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(signature, expected): return jsonify({'error': 'Invalid signature'}), 401 # 2. Parse payload event = request.json # 3. Proses berdasarkan event if event['event'] == 'payment.created': print(f"Payment baru: {event['data']['id']}") print(f"Nominal: {event['data']['amount']}") elif event['event'] == 'payment.success': print(f"Payment berhasil: {event['data']['id']}") print(f"Saldo naik: {event['data']['increase']}") elif event['event'] == 'payment.cancelled': print(f"Payment dibatalkan: {event['data']['id']}") elif event['event'] == 'shop.success': print(f"Role diaktifkan: {event['data']['role']}") return jsonify({'received': True}) if __name__ == '__main__': app.run(port=3000)

Contoh: PHP

<?php $WEBHOOK_SECRET = 'secret-dari-dashboard'; $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? ''; // 1. Verifikasi signature $expected = hash_hmac('sha256', $payload, $WEBHOOK_SECRET); if (!hash_equals($expected, $signature)) { http_response_code(401); echo json_encode(['error' => 'Invalid signature']); exit; } // 2. Parse payload $event = json_decode($payload, true); // 3. Proses berdasarkan event switch ($event['event']) { case 'payment.created': error_log("Payment baru: {$event['data']['id']}"); error_log("Nominal: {$event['data']['amount']}"); break; case 'payment.success': error_log("Payment berhasil: {$event['data']['id']}"); error_log("Saldo naik: {$event['data']['increase']}"); break; case 'payment.cancelled': error_log("Payment dibatalkan: {$event['data']['id']}"); break; case 'shop.success': error_log("Role diaktifkan: {$event['data']['role']}"); break; } // 4. Return 200 OK header('Content-Type: application/json'); echo json_encode(['received' => true]); ?>