Skip to content
RugWard
☰ メニュー

RugWard Pay

Webhook と署名の検証

請求書のステータスが変わるたびに署名付き HTTPS 通知を受け取り、curl・Node.js・Python・PHP で検証し、再試行と重複を安全に処理します。

このページの内容

Webhook の仕組み#

請求書のステータスが変わると、RugWard はあなたのエンドポイントに JSON 本文付きの POST を送信します。各リクエストは署名シークレット(signing secret)で署名されるため、RugWard から送られ改ざんされていないことを証明できます。

Webhook の設定
Webhook ボックス:エンドポイント URL、New signing secret、Send test ping、配信ログ

設定#

  1. ダッシュボードの Webhook にエンドポイントを入力し、Save をクリックします。
  2. 署名シークレット(whsec_ に続く 40 文字)は一度だけ表示されます。API キーと一緒にサーバーに保存してください。
  3. Send test ping をクリックします。ダッシュボードにエンドポイントが返した HTTP ステータスが表示されます。
  4. ボックスの下の配信ログに、各イベント、その結果、試行回数が表示されます。

エンドポイントのルール:

  • https:// のみ、標準ポート(:8443 は不可)、最大 500 文字;
  • 公開ホスト名であること:IP アドレス、localhost、プライベートネットワークは拒否されます;
  • 10 秒以内に任意の 2xx ステータスで応答すること。リダイレクトは追跡されません:3xx 応答は失敗として扱われます。

New signing secret はシークレットを即座に置き換えます。新しいものをすぐに反映してください(古いシークレットによる署名は直ちに無効になります)。空の URL を保存すると Webhook がオフになります。

イベント#

イベント送信されるタイミング
invoice.pending支払いを検出し、承認または残額を待っている
invoice.paid全額支払われ承認済み:発送してください
invoice.overpaid1% を超えて多く支払われた:発送してください
invoice.underpaidレートロック終了時に承認済み金額が不足していた
invoice.late期限切れ後に支払いが届いた
invoice.expired支払いなしで期限切れ
invoice.canceledあなたがキャンセルした
pingダッシュボードの Send test ping ボタン

各イベントは請求書ごとに最大 1 回だけ送信されます。テスト請求書は Webhook を送信しません。

リクエスト#

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 は <invoice id>:<status> 形式で、RugWard-Event-Id ヘッダーにも含まれます。重複を無視するために使ってください。
  • created は Unix 秒です。data.invoice は GET /api/pay/merchant/invoices/:id が返すものと同じオブジェクトです。

署名を検証する#

ヘッダーは RugWard-Signature: t=1793324100,v1=5f2c…(16 進数 64 文字)のような形式です。

  1. リクエストの生の本文を受信したそのままの形で読み取ります(JSON を解析する前に)。
  2. HMAC-SHA256(secret, t + "." + raw_body) を計算し、16 進数でエンコードします。
  3. v1 と定数時間で比較します。
  4. t があなたの時計から 5 分以上ずれていればリクエストを拒否します(リプレイ防止)。
  5. その後で初めて JSON を解析します。

先に JSON を解析して再シリアライズするとバイトが変わり、署名が一致しなくなります。必ず生の本文で署名を確認してください。

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

エンドポイントを手動でテストするには、自分で本文に署名します:

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"

再試行と重複#

エンドポイントがタイムアウトするか 2xx を返さない場合、RugWard は再試行します。合計で最大 4 回、約 30 秒、2 分、8 分後です。その後、イベントは配信ログで失敗としてマークされます。したがってハンドラーは次のようにしてください:

  • すばやく 2xx を返し、時間のかかる処理は後で行う;
  • 冪等にする:RugWard-Event-Id を保存し、処理済みのイベントはスキップする;
  • 順序に依存しない:迷ったら API で請求書を読み直す。

Webhook は速いシグナルですが、唯一のシグナルではありません。サーバーがしばらく停止していた場合は、API で最近の請求書を一覧取得してステータスを照合してください。

トラブルシューティング#

症状考えられる原因
"Webhook URL must be https:// with a public host name"IP アドレス、独自ポート、http://、または内部ホスト名
ping が HTTP 301/302/308 を示すサーバーがリダイレクトしている(例:http→https、末尾スラッシュの欠落)。最終的な URL を使ってください
ping が 405 を示すルートが POST を受け付けていない
署名が一致しないハッシュ前に本文が解析または再エンコードされた、誤ったシークレットを使用している(New signing secret 後)、またはサーバーの時計がずれている
テスト請求書でイベントが来ない想定どおり:テスト請求書は Webhook を送信しません

最終更新: 2026年10月30日

Webhook と署名の検証 · RugWard ドキュメント | RugWard