Webhook

Qrisy mengirim HTTP POST ke URL Anda saat event pembayaran terjadi. Enterprise-grade: HMAC-SHA256 signature, 5x retry, DLQ + replay.

Setup

  1. Dashboard → Webhook → Endpoints → Tambah Endpoint
  2. Masukkan URL server Anda (HTTPS recommended)
  3. Pilih events: paid, cancel, expired
  4. Simpan secret (qs_sec_xxx) — reveal-once!

Verify Signature

Setiap webhook memiliki header X-Qrisy-Signature berisi HMAC-SHA256 hex dari raw body JSON. Verifikasi dengan secret Anda:

Node.js (Express)

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');
  }
});

Python (Flask)

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)

PHP (native)

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.

Payload Structure

{
  "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"
}

Headers

HeaderDescription
X-Qrisy-SignatureHMAC-SHA256 hex of raw body
X-Qrisy-Event-IdUnique event ID (for idempotency)
X-Qrisy-TimestampUnix timestamp (seconds)

Retry & DLQ

Qrisy mengirim webhook dengan 5x retry exponential backoff:

AttemptDelay
1immediate
230 seconds
32 minutes
410 minutes
51 hour

Bila semua 5 attempt gagal (non-2xx response), event masuk Dead Letter Queue (DLQ). Anda bisa replay dari dashboard → Webhook → DLQ tab.

Anti-Replay

Setiap event memiliki X-Qrisy-Event-Id unik. Simpan ID ini di server Anda untuk mencegah double-processing (idempotency). Window anti-replay: 5 menit.

Test Webhook

Dashboard → Webhook → Endpoints → klik Test. Qrisy mengirim dummy payload (amount 10000, DANA). Rate limit: 5 test/hour per endpoint.

Multiple Endpoints

Satu store bisa punya multiple webhook endpoints — mis. staging + production. Setiap endpoint punya event filter sendiri (paid only, atau all events).

Response Requirements

Server Anda harus return HTTP 2xx dalam 10 detik. Bila timeout atau non-2xx, Qrisy akan retry. Response body disimpan (untuk debugging di dashboard).