Skip to content
RugWard
☰ Menü

RugWard Pay

Webhooks und Signaturprüfung

Erhalte bei jeder Statusänderung einer Rechnung eine signierte HTTPS-Benachrichtigung, prüfe sie in curl, Node.js, Python oder PHP und behandle Wiederholungen und Duplikate sicher.

Auf dieser Seite

So funktionieren Webhooks#

Wenn sich der Status einer Rechnung ändert, sendet RugWard einen POST mit JSON-Body an deinen Endpoint. Jede Anfrage ist mit deinem Signaturgeheimnis (signing secret) signiert, sodass du nachweisen kannst, dass sie von RugWard stammt und nicht verändert wurde.

Webhook-Einstellungen
Der Webhook-Bereich: Endpoint-URL, New signing secret, Send test ping und das Zustellprotokoll

Einrichtung#

  1. Gib im Dashboard deinen Endpoint unter Webhook ein und klicke auf Save.
  2. Das Signaturgeheimnis (whsec_ gefolgt von 40 Zeichen) wird nur einmal angezeigt. Bewahre es auf deinem Server neben dem API-Schlüssel auf.
  3. Klicke auf Send test ping. Das Dashboard zeigt den HTTP-Status, den dein Endpoint zurückgegeben hat.
  4. Das Zustellprotokoll unter dem Bereich zeigt jedes Ereignis, sein Ergebnis und die Anzahl der Versuche.

Regeln für den Endpoint:

  • nur https://, auf dem Standardport (kein :8443), maximal 500 Zeichen;
  • ein öffentlicher Hostname: IP-Adressen, localhost und private Netze werden abgelehnt;
  • innerhalb von 10 Sekunden mit einem beliebigen 2xx-Status antworten. Weiterleitungen werden nicht verfolgt: eine 3xx-Antwort gilt als Fehler.

New signing secret ersetzt das Geheimnis sofort; setze das neue umgehend ein (Signaturen mit dem alten Geheimnis enden sofort). Das Speichern einer leeren URL schaltet Webhooks ab.

Ereignisse#

EreignisGesendet, wenn
invoice.pendingeine Zahlung gesehen wurde, wartet auf Bestätigungen oder den Restbetrag
invoice.paidvollständig bezahlt und bestätigt: versenden
invoice.overpaidmit mehr als 1 % zu viel bezahlt: versenden
invoice.underpaiddie Kurssperre endete mit zu wenig bestätigtem Betrag
invoice.lateeine Zahlung kam nach Ablauf an
invoice.expiredohne Zahlung abgelaufen
invoice.canceleddu hast sie storniert
pingdie Schaltfläche Send test ping im Dashboard

Jedes Ereignis wird pro Rechnung höchstens einmal ausgelöst. Testrechnungen senden nie Webhooks.

Die Anfrage#

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 ist <invoice id>:<status> und steht auch im Header RugWard-Event-Id: nutze sie, um Duplikate zu ignorieren.
  • created ist in Unix-Sekunden; data.invoice ist dasselbe Objekt, das GET /api/pay/merchant/invoices/:id liefert.

Signatur prüfen#

Der Header sieht so aus: RugWard-Signature: t=1793324100,v1=5f2c… (64 Hex-Zeichen).

  1. Lies den rohen Request-Body genau so, wie er ankam (vor jedem JSON-Parsing).
  2. Berechne HMAC-SHA256(secret, t + "." + raw_body) und kodiere ihn hexadezimal.
  3. Vergleiche ihn mit v1 in konstanter Zeit.
  4. Lehne die Anfrage ab, wenn t mehr als 5 Minuten von deiner Uhr abweicht (verhindert Replays).
  5. Erst dann das JSON parsen.

Wird das JSON zuerst geparst und neu serialisiert, ändern sich die Bytes und die Signatur bricht. Prüfe die Signatur immer am rohen Body.

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

Um deinen Endpoint manuell zu testen, signiere selbst einen Body:

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"

Wiederholungen und Duplikate#

Wenn dein Endpoint in ein Timeout läuft oder nicht mit 2xx antwortet, versucht RugWard es erneut: insgesamt bis zu 4 Versuche, nach etwa 30 Sekunden, 2 Minuten und 8 Minuten. Danach wird das Ereignis im Zustellprotokoll als fehlgeschlagen markiert. Dein Handler sollte daher:

  • schnell mit 2xx antworten und langsame Arbeit danach erledigen;
  • idempotent sein: RugWard-Event-Id speichern und bereits verarbeitete Ereignisse überspringen;
  • nicht von der Reihenfolge abhängen: im Zweifel die Rechnung per API neu lesen.

Webhooks sind ein schnelles Signal, aber nicht das einzige. War dein Server eine Weile nicht erreichbar, liste die letzten Rechnungen per API auf und vergleiche die Status.

Fehlerbehebung#

SymptomWahrscheinliche Ursache
"Webhook URL must be https:// with a public host name"eine IP-Adresse, ein eigener Port, http:// oder ein interner Hostname
Ping zeigt HTTP 301/302/308dein Server leitet weiter (z. B. http→https oder fehlender abschließender Schrägstrich); nutze die endgültige URL
Ping zeigt 405die Route akzeptiert kein POST
Signatur stimmt nieder Body wurde vor dem Hashen geparst oder neu kodiert, das falsche Geheimnis wird verwendet (nach New signing secret) oder die Serveruhr geht falsch
keine Ereignisse für Testrechnungenerwartet: Testrechnungen senden nie Webhooks

Zuletzt aktualisiert: 30. Oktober 2026

Webhooks und Signaturprüfung · RugWard Dokumentation | RugWard