Skip to content
RugWard
☰ Меню

RugWard Pay

Вебхуки и проверка подписи

Получайте подписанное HTTPS-уведомление при каждом изменении статуса счёта, проверяйте его в curl, Node.js, Python или PHP и безопасно обрабатывайте повторы и дубликаты.

На этой странице

Как работают вебхуки#

Когда счёт меняет статус, RugWard отправляет POST с JSON-телом на ваш эндпоинт. Каждый запрос подписан вашим секретом подписи (signing secret), поэтому вы можете доказать, что он пришёл от RugWard и не был изменён.

Настройки вебхука
Блок 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 отключает вебхуки.

События#

СобытиеОтправляется, когда
invoice.pendingплатёж замечен, ожидаются подтверждения или остаток суммы
invoice.paidоплачен полностью и подтверждён: отгружайте
invoice.overpaidоплачен с переплатой более 1%: отгружайте
invoice.underpaidфиксация курса закончилась, а подтверждено слишком мало
invoice.lateплатёж пришёл после истечения срока
invoice.expiredсрок истёк без оплаты
invoice.canceledвы его отменили
pingкнопка Send test ping в панели

Каждое событие отправляется не более одного раза на счёт. Тестовые счета никогда не отправляют вебхуки.

Запрос#

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… (64 шестнадцатеричных символа).

  1. Прочитайте сырое тело запроса точно в том виде, в каком оно пришло (до любого разбора JSON).
  2. Вычислите HMAC-SHA256(secret, t + "." + raw_body) и закодируйте в hex.
  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.

Вебхуки — быстрый сигнал, но не единственный. Если ваш сервер какое-то время был недоступен, получите список последних счетов через API и сравните статусы.

Устранение неполадок#

СимптомВероятная причина
"Webhook URL must be https:// with a public host name"IP-адрес, нестандартный порт, http:// или внутреннее имя хоста
пинг показывает HTTP 301/302/308ваш сервер делает редирект (например http→https или нет завершающего слэша); укажите итоговый URL
пинг показывает 405маршрут не принимает POST
подпись никогда не совпадаеттело было разобрано или перекодировано перед хешированием, используется не тот секрет (после New signing secret) или неверно идут часы сервера
нет событий для тестовых счетовтак и задумано: тестовые счета никогда не отправляют вебхуки

Обновлено: 30 октября 2026 г.

Вебхуки и проверка подписи · RugWard Документация | RugWard