Webhook Mutasi QRIS Realtime
Terima notifikasi otomatis (HTTP POST Callback) secara instan setiap kali pelanggan berhasil melakukan pembayaran QRIS di toko atau merchant Anda.
Informasi Layanan & Ketentuan Webhook
- Kecepatan Realtime: Webhook dikirimkan dalam hitungan detik setelah transaksi terkonfirmasi oleh jaringan perbankan QRIS / Hub.
- Biaya Langganan Terjangkau: Hanya Rp 2.000 / slot / bulan (30 hari). Anda dapat membeli lebih dari 1 slot webhook.
- Routing Khusus per QRIS Toko: Setiap slot webhook dapat diarahkan khusus ke QRIS toko tertentu atau menerima seluruh mutasi QRIS Anda.
- Bebas Batas Transaksi (Unlimited): Tidak ada batasan jumlah pengiriman webhook selama masa aktif langganan slot berjalan.
- Keamanan HMAC SHA-256: Setiap request dilengkapi tanda tangan digital kriptografi yang dapat diverifikasi menggunakan Secret Token akun Anda.
Alur Pengiriman Webhook
Pelanggan memindai QRIS merchant Anda melalui aplikasi m-banking atau e-wallet (BCA, Mandiri, BRI, Dana, GoPay, dll).
Sistem background worker Cutiezy mendeteksi transaksi masuk secara realtime (< 1 detik).
Sistem mengirimkan HTTP POST JSON ke URL Webhook Anda membawa signature kriptografi HMAC SHA-256.
Server Anda memverifikasi signature, mengupdate status order pembeli, dan mengembalikan respon HTTP 200 OK.
Spesifikasi Request Header
Setiap payload webhook yang dikirimkan oleh Cutiezy selalu menyertakan header berikut:
| Nama Header | Contoh Nilai | Keterangan |
|---|---|---|
| Content-Type | application/json | Format data yang dikirim adalah JSON terstruktur. |
| X-Cutiezy-Signature | a3f5c78b9... (64 hex characters) | Tanda tangan HMAC SHA-256 dari raw body request menggunakan Secret Token Anda. |
| X-Cutiezy-Event | qris.payment | Jenis event: qris.payment untuk transaksi baru, atau test.ping untuk uji coba. |
| X-Cutiezy-Timestamp | 2026-10-08T23:30:00+07:00 | Waktu saat webhook dipancarkan (format ISO-8601). |
| User-Agent | Cutiezy-Webhook/1.0 (+https://api.cutiezy.id) | Identitas crawler pengirim webhook resmi Cutiezy. |
Format Payload JSON
Struktur body request yang dikirimkan ke webhook server Anda.
{
"event": "qris.payment",
"timestamp": "2026-10-08T23:30:00+07:00",
"slot_no": 1,
"target_qris": "QRIS Toko Utama",
"data": {
"id": "202610080019283719",
"provider": "bri",
"reference": "202610080019283719",
"partner_reference": "00000001",
"amount": 25000,
"currency": "IDR",
"status": "SUCCESS",
"customer": "Cutiezy PASURUAN ID",
"issuer": "QRIS - DANA",
"channel": "QRIS",
"occurred_at": "2026-10-08 23:29:55",
"received_at": "2026-10-08T23:30:00+07:00"
}
}
Deskripsi Field Payload
| Field | Tipe | Keterangan |
|---|---|---|
| event | string | Tipe notifikasi, misal qris.payment atau test.ping. |
| slot_no | integer | Nomor slot webhook penerima (misal: 1, 2). |
| target_qris | string | Nama QRIS target yang dikonfigurasi pada slot (contoh: QRIS Toko Utama atau Semua QRIS (Global)). |
| data.reference | string | Nomor referensi unik transaksi dari perbankan/RRN (contoh: 202610080019283719). Gunakan untuk mencegah duplikasi (Idempotency). |
| data.amount | integer | Nominal pembayaran dalam Rupiah bulat (contoh: 25000). |
| data.status | string | Status mutasi, biasanya bernilai SUCCESS. |
| data.issuer | string | Aplikasi / Bank yang dipakai pembeli untuk scan QRIS (contoh: QRIS - DANA, BCA Mobile, GOPAY). |
| data.occurred_at | string | Waktu transaksi terjadi menurut bank. |
Verifikasi Signature HMAC SHA-256
Untuk keamanan maksimal, selalu verifikasi keaslian pengirim sebelum memproses transaksi di server Anda.
sha256 dengan kunci secret_key yang ada di menu pengaturan Webhook akun Anda.
<?php
// 1. Ambil Secret Key Anda dari menu /qris
$secretKey = "YOUR_WEBHOOK_SECRET_KEY";
// 2. Baca RAW payload body dan header signature
$rawBody = file_get_contents('php://input');
$receivedSignature = $_SERVER['HTTP_X_CUTIEZY_SIGNATURE'] ?? '';
// 3. Hitung HMAC SHA-256
$expectedSignature = hash_hmac('sha256', $rawBody, $secretKey);
// 4. Verifikasi keaslian request dengan hash_equals
if (!hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// 5. Decode JSON dan proses transaksi
$payload = json_decode($rawBody, true);
$event = $payload['event'] ?? '';
$data = $payload['data'] ?? [];
if ($event === 'qris.payment') {
$ref = $data['reference'];
$amount = $data['amount'];
// TODO: Update status invoice order Anda di database menjadi PAID
// Contoh: Orders::markAsPaid($ref, $amount);
}
// 6. Selalu kembalikan HTTP 200 OK
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);
const express = require('express');
const crypto = require('crypto');
const app = express();
const SECRET_KEY = 'YOUR_WEBHOOK_SECRET_KEY';
// Penting: Gunakan express.raw() atau tangkap rawBody untuk verifikasi signature
app.post('/api/webhook/qris', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
const signature = req.headers['x-cutiezy-signature'] || '';
const expectedSignature = crypto
.createHmac('sha256', SECRET_KEY)
.update(rawBody)
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(rawBody);
console.log('Notifikasi Transaksi Diterima:', payload);
if (payload.event === 'qris.payment') {
const { reference, amount, customer } = payload.data;
// TODO: Proses pembayaran invoice Anda
}
res.status(200).json({ received: true });
});
app.listen(3000, () => console.log('Webhook server running on port 3000'));
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import json
app = FastAPI()
SECRET_KEY = b"YOUR_WEBHOOK_SECRET_KEY"
@app.post("/api/webhook/qris")
async def handle_qris_webhook(request: Request):
raw_body = await request.body()
signature = request.headers.get("x-cutiezy-signature", "")
expected_signature = hmac.new(SECRET_KEY, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected_signature, signature):
raise HTTPException(status_code=401, detail="Invalid signature")
payload = json.loads(raw_body.decode("utf-8"))
if payload.get("event") == "qris.payment":
data = payload.get("data", {})
print(f"Pembayaran Masuk: Rp {data.get('amount')} (Ref: {data.get('reference')})")
# TODO: Jalankan proses fulfillment order
return {"received": True}
Respon Server & Best Practices
Server Anda wajib merespon dengan status code 200 OK dengan body {"received": true} dalam batas waktu maksimal 5 detik.
Gunakan nilai data.reference sebagai identifier unik. Simpan referensi transaksi ke database Anda agar order tidak diproses ganda jika terdapat retry jaringan.
Jika Anda perlu mengirimkan notifikasi WhatsApp, email PDF, atau interaksi API pihak ketiga yang membutuhkan waktu > 2 detik, masukkan ke antrean (queue) dan segera kembalikan response 200 OK ke Cutiezy.