RugWard Pay
Webhook dan verifikasi tanda tangan
Terima notifikasi HTTPS bertanda tangan pada setiap perubahan status invoice, verifikasi dengan curl, Node.js, Python, atau PHP, dan tangani percobaan ulang serta duplikat dengan aman.
Di halaman ini
Cara kerja webhook#
Saat status invoice berubah, RugWard mengirim POST dengan body JSON ke endpoint Anda. Setiap permintaan ditandatangani dengan rahasia penandatanganan (signing secret) Anda, sehingga Anda dapat membuktikan bahwa permintaan berasal dari RugWard dan tidak diubah.

Pengaturan#
- Di dasbor, masukkan endpoint Anda di Webhook lalu klik Save.
- Rahasia penandatanganan (
whsec_diikuti 40 karakter) ditampilkan hanya sekali. Simpan di server Anda, bersama kunci API. - Klik Send test ping. Dasbor menampilkan status HTTP yang dikembalikan endpoint Anda.
- Log pengiriman di bawah kotak menampilkan setiap event, hasilnya, dan jumlah percobaan.
Aturan endpoint:
- hanya
https://, pada port standar (tanpa:8443), maksimal 500 karakter; - nama host publik: alamat IP,
localhost, dan jaringan privat ditolak; - jawab dalam 10 detik dengan status 2xx apa pun. Pengalihan tidak diikuti: jawaban 3xx dihitung sebagai kegagalan.
New signing secret langsung mengganti rahasia; segera terapkan yang baru (tanda tangan dengan rahasia lama langsung berhenti berlaku). Menyimpan URL kosong mematikan webhook.
Event#
| Event | Dikirim saat |
|---|---|
invoice.pending | pembayaran terlihat, menunggu konfirmasi atau sisa jumlah |
invoice.paid | lunas penuh dan terkonfirmasi: kirim barang |
invoice.overpaid | dibayar lebih dari 1% ekstra: kirim barang |
invoice.underpaid | penguncian kurs berakhir dengan jumlah terkonfirmasi yang terlalu sedikit |
invoice.late | pembayaran tiba setelah kedaluwarsa |
invoice.expired | kedaluwarsa tanpa pembayaran |
invoice.canceled | Anda membatalkannya |
ping | tombol Send test ping di dasbor |
Setiap event dikirim paling banyak sekali per invoice. Invoice uji tidak pernah mengirim webhook.
Permintaan#
POST /rugward-webhook HTTP/1.1
Host: shop.example
Content-Type: application/json
User-Agent: RugWard-Pay-Webhook/1.0
RugWard-Event-Id: Zx3kQ8pL2mN7vR1tY5wB9c:paid
RugWard-Event: invoice.paid
RugWard-Signature: t=1793324105,v1=5f0c3d0e8b1a…(64 hex characters){
"id": "Zx3kQ8pL2mN7vR1tY5wB9c:paid",
"type": "invoice.paid",
"created": 1793324100,
"data": {
"invoice": {
"id": "Zx3kQ8pL2mN7vR1tY5wB9c",
"status": "paid",
"amountUsd": "25.00",
"orderId": "1234",
"description": "1 x T-shirt, size M",
"redirectUrl": "https://shop.example/thanks?order=1234",
"createdAt": 1793323282069,
"expiresAt": 1793326882069,
"paidAt": 1793324100512,
"assets": ["BTC", "ETH-8453", "USDC-8453"],
"test": false,
"quotes": [{ "id": 4711, "asset": "USDC-8453", "amount": "25.000629", "rateUsd": "1", "address": "0x…", "expiresAt": 1793325082069 }],
"payments": [{ "txid": "0x…", "asset": "USDC-8453", "amount": "24.875626", "confirmations": 30, "required": 30, "confirmed": true, "seenAt": 1793324012000, "explorer": "https://basescan.org/tx/0x…" }]
}
}
}{ "id": "ping:1793324000000", "type": "ping", "created": 1793324000, "data": {} }idberbentuk<invoice id>:<status>dan juga ada di headerRugWard-Event-Id: gunakan untuk mengabaikan duplikat.createddalam detik Unix;data.invoiceadalah objek yang sama dengan yang dikembalikanGET /api/pay/merchant/invoices/:id.
Memverifikasi tanda tangan#
Header terlihat seperti RugWard-Signature: t=1793324100,v1=5f2c… (64 karakter heksadesimal).
- Baca body permintaan mentah persis seperti yang diterima (sebelum parsing JSON apa pun).
- Hitung
HMAC-SHA256(secret, t + "." + raw_body)dan enkode ke heksadesimal. - Bandingkan dengan
v1dalam waktu konstan. - Tolak permintaan jika
tberselisih lebih dari 5 menit dari jam Anda (mencegah replay). - Baru setelah itu parse JSON-nya.
Mem-parse JSON terlebih dulu lalu menyerialkannya ulang mengubah byte dan merusak tanda tangan. Selalu periksa tanda tangan pada body mentah.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.RUGWARD_WEBHOOK_SECRET; // whsec_…
const app = express();
// Use the RAW body: the signature covers the exact bytes RugWard sent.
app.post("/rugward-webhook", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(req.get("RugWard-Signature") || "");
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return res.sendStatus(400);
const want = crypto.createHmac("sha256", SECRET).update(`${m[1]}.${raw}`).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]))) return res.sendStatus(400);
const event = JSON.parse(raw);
// De-duplicate: the same event id can arrive more than once.
// if (alreadyProcessed(event.id)) return res.sendStatus(200);
if (event.type === "invoice.paid" || event.type === "invoice.overpaid") {
const inv = event.data.invoice;
// markOrderPaid(inv.orderId, inv.id); // ship only now
}
res.sendStatus(200); // any 2xx = delivered
});
app.listen(3000);import hashlib, hmac, json, os, re, time
from flask import Flask, abort, request
SECRET = os.environ["RUGWARD_WEBHOOK_SECRET"].encode() # whsec_…
app = Flask(__name__)
@app.post("/rugward-webhook")
def rugward_webhook():
raw = request.get_data() # raw bytes, exactly as sent
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", request.headers.get("RugWard-Signature", ""))
if not m or abs(time.time() - int(m.group(1))) > 300:
abort(400)
want = hmac.new(SECRET, m.group(1).encode() + b"." + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(want, m.group(2)):
abort(400)
event = json.loads(raw)
if event["type"] in ("invoice.paid", "invoice.overpaid"):
inv = event["data"]["invoice"]
# mark_order_paid(inv["orderId"], inv["id"]) # de-duplicate on event["id"]
return "", 200<?php
$secret = getenv("RUGWARD_WEBHOOK_SECRET"); // whsec_…
$raw = file_get_contents("php://input");
$sig = $_SERVER["HTTP_RUGWARD_SIGNATURE"] ?? "";
if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $sig, $m) || abs(time() - (int)$m[1]) > 300) {
http_response_code(400); exit;
}
$want = hash_hmac("sha256", $m[1] . "." . $raw, $secret);
if (!hash_equals($want, $m[2])) { http_response_code(400); exit; }
$event = json_decode($raw, true);
if (in_array($event["type"], ["invoice.paid", "invoice.overpaid"], true)) {
$inv = $event["data"]["invoice"];
// mark_order_paid($inv["orderId"], $inv["id"]); // de-duplicate on $event["id"]
}
http_response_code(200);Untuk menguji endpoint secara manual, tandatangani body sendiri:
SECRET='whsec_…' # your signing secret
BODY='{"id":"ping:1","type":"ping","created":1793324000,"data":{}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST https://shop.example/rugward-webhook \
-H "Content-Type: application/json" \
-H "RugWard-Event: ping" \
-H "RugWard-Signature: t=$T,v1=$SIG" \
--data "$BODY"Percobaan ulang dan duplikat#
Jika endpoint Anda kehabisan waktu atau tidak menjawab 2xx, RugWard mencoba lagi: total hingga 4 percobaan, setelah sekitar 30 detik, 2 menit, dan 8 menit. Setelah itu event ditandai gagal di log pengiriman. Karena itu handler Anda harus:
- menjawab 2xx dengan cepat dan mengerjakan tugas lambat sesudahnya;
- idempoten: simpan
RugWard-Event-Iddan lewati event yang sudah diproses; - tidak bergantung pada urutan: jika ragu, baca ulang invoice dengan API.
Webhook adalah sinyal cepat, bukan satu-satunya. Jika server Anda sempat mati, daftar invoice terbaru dengan API dan bandingkan statusnya.
Pemecahan masalah#
| Gejala | Kemungkinan penyebab |
|---|---|
| "Webhook URL must be https:// with a public host name" | alamat IP, port kustom, http://, atau nama host internal |
| ping menunjukkan HTTP 301/302/308 | server Anda mengalihkan (misalnya http→https, atau garis miring akhir hilang); gunakan URL akhir |
| ping menunjukkan 405 | rute tidak menerima POST |
| tanda tangan tidak pernah cocok | body di-parse atau dienkode ulang sebelum hashing, rahasia yang dipakai salah (setelah New signing secret), atau jam server salah |
| tidak ada event untuk invoice uji | memang begitu: invoice uji tidak pernah mengirim webhook |
Terakhir diperbarui: 30 Oktober 2026