Qrisy mengirim HTTP POST ke URL Anda saat event pembayaran terjadi. Enterprise-grade: HMAC-SHA256 signature, 5x retry, DLQ + replay.
paid, cancel, expiredqs_sec_xxx) — reveal-once!Setiap webhook memiliki header X-Qrisy-Signature berisi HMAC-SHA256 hex dari raw body JSON. Verifikasi dengan secret Anda:
import { parseWebhook } from '@qrisy/node';
import express from 'express';
const app = express();
// WAJIB: gunakan express.raw, bukan express.json
app.post('/webhook/qrisy', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
const signature = req.headers['x-qrisy-signature'] as string;
const secret = process.env.QRISY_WEBHOOK_SECRET!;
try {
const payload = parseWebhook(rawBody, signature, secret);
console.log(`Paid: ${payload.transactionId} = ${payload.amount}`);
res.status(200).send('ok');
} catch {
res.status(401).send('invalid signature');
}
});from qrisy import parse_webhook
from flask import Flask, request, abort
app = Flask(__name__)
@app.post('/webhook/qrisy')
def webhook():
raw_body = request.get_data()
signature = request.headers.get('X-Qrisy-Signature', '')
secret = os.environ['QRISY_WEBHOOK_SECRET']
try:
payload = parse_webhook(raw_body, signature, secret)
print(f"Paid: {payload.transaction_id} = {payload.amount}")
return 'ok', 200
except Exception:
abort(401)use Qrisy\Webhook\Webhook;
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_QRISY_SIGNATURE'] ?? '';
$secret = $_ENV['QRISY_WEBHOOK_SECRET'];
try {
$payload = Webhook::parse($rawBody, $signature, $secret);
// handle $payload['transactionId'], $payload['amount']
http_response_code(200);
echo 'ok';
} catch (\Qrisy\Exception\QrisyException $e) {
http_response_code(401);
echo 'invalid signature';
}WAJIB: Gunakan raw body untuk verifikasi. Parsed JSON (via express.json / json_decode) akan mengubah key order & whitespace, menyebabkan signature mismatch.
{
"transactionId": "txn_01ABC...",
"eventType": "paid",
"amount": 15450,
"originalAmount": 15000,
"uniqueNominal": 450,
"totalAmount": 15450,
"packageName": "id.dana",
"appName": "DANA Bisnis",
"storeName": "Toko Budi Jaya",
"status": "paid",
"paidAt": "2026-08-02T10:05:30.000Z"
}| Header | Description |
|---|---|
X-Qrisy-Signature | HMAC-SHA256 hex of raw body |
X-Qrisy-Event-Id | Unique event ID (for idempotency) |
X-Qrisy-Timestamp | Unix timestamp (seconds) |
Qrisy mengirim webhook dengan 5x retry exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
Bila semua 5 attempt gagal (non-2xx response), event masuk Dead Letter Queue (DLQ). Anda bisa replay dari dashboard → Webhook → DLQ tab.
Setiap event memiliki X-Qrisy-Event-Id unik. Simpan ID ini di server Anda untuk mencegah double-processing (idempotency). Window anti-replay: 5 menit.
Dashboard → Webhook → Endpoints → klik Test. Qrisy mengirim dummy payload (amount 10000, DANA). Rate limit: 5 test/hour per endpoint.
Satu store bisa punya multiple webhook endpoints — mis. staging + production. Setiap endpoint punya event filter sendiri (paid only, atau all events).
Server Anda harus return HTTP 2xx dalam 10 detik. Bila timeout atau non-2xx, Qrisy akan retry. Response body disimpan (untuk debugging di dashboard).