Skip to content
RugWard
☰ Menu

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 webhook
Kotak Webhook: URL endpoint, New signing secret, Send test ping, dan log pengiriman

Pengaturan#

  1. Di dasbor, masukkan endpoint Anda di Webhook lalu klik Save.
  2. Rahasia penandatanganan (whsec_ diikuti 40 karakter) ditampilkan hanya sekali. Simpan di server Anda, bersama kunci API.
  3. Klik Send test ping. Dasbor menampilkan status HTTP yang dikembalikan endpoint Anda.
  4. 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#

EventDikirim saat
invoice.pendingpembayaran terlihat, menunggu konfirmasi atau sisa jumlah
invoice.paidlunas penuh dan terkonfirmasi: kirim barang
invoice.overpaiddibayar lebih dari 1% ekstra: kirim barang
invoice.underpaidpenguncian kurs berakhir dengan jumlah terkonfirmasi yang terlalu sedikit
invoice.latepembayaran tiba setelah kedaluwarsa
invoice.expiredkedaluwarsa tanpa pembayaran
invoice.canceledAnda membatalkannya
pingtombol Send test ping di dasbor

Setiap event dikirim paling banyak sekali per invoice. Invoice uji tidak pernah mengirim webhook.

Permintaan#

Webhook request (what your endpoint receives)
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)
Body: invoice.paid
{
  "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…" }]
    }
  }
}
Body: test ping (dashboard button)
{ "id": "ping:1793324000000", "type": "ping", "created": 1793324000, "data": {} }
  • id berbentuk <invoice id>:<status> dan juga ada di header RugWard-Event-Id: gunakan untuk mengabaikan duplikat.
  • created dalam detik Unix; data.invoice adalah objek yang sama dengan yang dikembalikan GET /api/pay/merchant/invoices/:id.

Memverifikasi tanda tangan#

Header terlihat seperti RugWard-Signature: t=1793324100,v1=5f2c… (64 karakter heksadesimal).

  1. Baca body permintaan mentah persis seperti yang diterima (sebelum parsing JSON apa pun).
  2. Hitung HMAC-SHA256(secret, t + "." + raw_body) dan enkode ke heksadesimal.
  3. Bandingkan dengan v1 dalam waktu konstan.
  4. Tolak permintaan jika t berselisih lebih dari 5 menit dari jam Anda (mencegah replay).
  5. 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.

Node.js (Express): verify, then process
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);
Python (Flask)
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 8
<?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:

Shell (openssl): compute a signature to test your endpoint
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-Id dan 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#

GejalaKemungkinan 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/308server Anda mengalihkan (misalnya http→https, atau garis miring akhir hilang); gunakan URL akhir
ping menunjukkan 405rute tidak menerima POST
tanda tangan tidak pernah cocokbody di-parse atau dienkode ulang sebelum hashing, rahasia yang dipakai salah (setelah New signing secret), atau jam server salah
tidak ada event untuk invoice ujimemang begitu: invoice uji tidak pernah mengirim webhook

Terakhir diperbarui: 30 Oktober 2026

Webhook dan verifikasi tanda tangan · RugWard Dokumentasi | RugWard