Skip to content
RugWard
☰ Menu

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.

Paramètres du webhook
Le bloc Webhook : URL de l'endpoint, New signing secret, Send test ping et le journal des livraisons

Configuration#

  1. Dans le tableau de bord, saisissez votre endpoint dans Webhook et cliquez sur Save.
  2. 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.
  3. Cliquez sur Send test ping. Le tableau de bord affiche le statut HTTP renvoyé par votre endpoint.
  4. 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, localhost et 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énementEnvoyé quand
invoice.pendingun paiement a été vu, en attente de confirmations ou du reste du montant
invoice.paidentièrement payée et confirmée : expédiez
invoice.overpaidpayée avec plus de 1 % en trop : expédiez
invoice.underpaidle verrouillage du taux a pris fin avec trop peu de montant confirmé
invoice.lateun paiement est arrivé après l'expiration
invoice.expiredexpirée sans paiement
invoice.canceledvous l'avez annulée
pingle 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#

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 vaut <invoice id>:<status> et figure aussi dans l'en-tête RugWard-Event-Id : utilisez-le pour ignorer les doublons.
  • created est en secondes Unix ; data.invoice est le même objet que celui renvoyé par GET /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).

  1. Lisez le corps brut de la requête exactement tel que reçu (avant tout parsing JSON).
  2. Calculez HMAC-SHA256(secret, t + "." + raw_body) et encodez-le en hexadécimal.
  3. Comparez-le à v1 en temps constant.
  4. Rejetez la requête si t s'écarte de plus de 5 minutes de votre horloge (bloque les rejeux).
  5. 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.

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

Pour tester votre endpoint à la main, signez vous-même un corps :

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"

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-Id et 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ômeCause 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/308votre serveur redirige (par exemple http→https, ou une barre oblique finale manquante) ; utilisez l'URL finale
le ping affiche 405la route n'accepte pas POST
la signature ne correspond jamaisle 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 testnormal : les factures de test n'envoient jamais de webhooks

Dernière mise à jour: 30 octobre 2026

Webhooks et vérification de signature · RugWard Documentation | RugWard