RugWard Pay
Webhooks et vérification de signature
Recevez une notification HTTPS signée à chaque changement de statut d'une facture, vérifiez-la en curl, Node.js, Python ou PHP et gérez en toute sécurité les nouvelles tentatives et les doublons.
Sur cette page
Fonctionnement des webhooks#
Quand une facture change de statut, RugWard envoie un POST avec un corps JSON à votre endpoint. Chaque requête est signée avec votre secret de signature (signing secret) : vous pouvez ainsi prouver qu'elle vient de RugWard et n'a pas été modifiée.

Configuration#
- Dans le tableau de bord, saisissez votre endpoint dans Webhook et cliquez sur Save.
- Le secret de signature (
whsec_suivi de 40 caractères) n'est affiché qu'une fois. Conservez-le sur votre serveur, à côté de la clé API. - Cliquez sur Send test ping. Le tableau de bord affiche le statut HTTP renvoyé par votre endpoint.
- Le journal des livraisons sous le bloc affiche chaque événement, son résultat et le nombre de tentatives.
Règles de l'endpoint :
https://uniquement, sur le port standard (pas de:8443), 500 caractères maximum ;- un nom d'hôte public : les adresses IP,
localhostet les réseaux privés sont refusés ; - répondre en moins de 10 secondes avec n'importe quel statut 2xx. Les redirections ne sont pas suivies : une réponse 3xx compte comme un échec.
New signing secret remplace le secret immédiatement ; déployez le nouveau sans attendre (les signatures faites avec l'ancien secret cessent aussitôt). Enregistrer une URL vide désactive les webhooks.
Événements#
| Événement | Envoyé quand |
|---|---|
invoice.pending | un paiement a été vu, en attente de confirmations ou du reste du montant |
invoice.paid | entièrement payée et confirmée : expédiez |
invoice.overpaid | payée avec plus de 1 % en trop : expédiez |
invoice.underpaid | le verrouillage du taux a pris fin avec trop peu de montant confirmé |
invoice.late | un paiement est arrivé après l'expiration |
invoice.expired | expirée sans paiement |
invoice.canceled | vous l'avez annulée |
ping | le bouton Send test ping du tableau de bord |
Chaque événement est émis au plus une fois par facture. Les factures de test n'envoient jamais de webhooks.
La requête#
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": {} }idvaut<invoice id>:<status>et figure aussi dans l'en-têteRugWard-Event-Id: utilisez-le pour ignorer les doublons.createdest en secondes Unix ;data.invoiceest le même objet que celui renvoyé parGET /api/pay/merchant/invoices/:id.
Vérifier la signature#
L'en-tête ressemble à RugWard-Signature: t=1793324100,v1=5f2c… (64 caractères hexadécimaux).
- Lisez le corps brut de la requête exactement tel que reçu (avant tout parsing JSON).
- Calculez
HMAC-SHA256(secret, t + "." + raw_body)et encodez-le en hexadécimal. - Comparez-le à
v1en temps constant. - Rejetez la requête si
ts'écarte de plus de 5 minutes de votre horloge (bloque les rejeux). - Seulement ensuite, parsez le JSON.
Parser d'abord le JSON puis le resérialiser modifie les octets et casse la signature. Vérifiez toujours la signature sur le corps brut.
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);Pour tester votre endpoint à la main, signez vous-même un corps :
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"Nouvelles tentatives et doublons#
Si votre endpoint dépasse le délai ou ne répond pas 2xx, RugWard réessaie : jusqu'à 4 tentatives au total, après environ 30 secondes, 2 minutes et 8 minutes. Ensuite, l'événement est marqué en échec dans le journal des livraisons. Votre gestionnaire doit donc :
- répondre 2xx rapidement et faire le travail lent ensuite ;
- être idempotent : stockez
RugWard-Event-Idet ignorez les événements déjà traités ; - ne pas dépendre de l'ordre : en cas de doute, relisez la facture via l'API.
Les webhooks sont un signal rapide, pas le seul. Si votre serveur a été indisponible un moment, listez les factures récentes via l'API et comparez les statuts.
Dépannage#
| Symptôme | Cause probable |
|---|---|
| "Webhook URL must be https:// with a public host name" | une adresse IP, un port personnalisé, http:// ou un nom d'hôte interne |
| le ping affiche HTTP 301/302/308 | votre serveur redirige (par exemple http→https, ou une barre oblique finale manquante) ; utilisez l'URL finale |
| le ping affiche 405 | la route n'accepte pas POST |
| la signature ne correspond jamais | le corps a été parsé ou réencodé avant le hachage, le mauvais secret est utilisé (après New signing secret) ou l'horloge du serveur est fausse |
| aucun événement pour les factures de test | normal : les factures de test n'envoient jamais de webhooks |
Dernière mise à jour: 30 octobre 2026