Skip to content
RugWard
☰ القائمة

RugWard Pay

إشعارات Webhook والتحقق من التوقيع

احصل على إشعار HTTPS موقّع عند كل تغيير في حالة الفاتورة، وتحقّق منه بلغات curl وNode.js وPython وPHP، وتعامل مع إعادة المحاولة والتكرار بأمان.

في هذه الصفحة

كيف تعمل إشعارات Webhook#

عندما تتغير حالة فاتورة، يرسل RugWard طلب POST بجسم JSON إلى نقطة النهاية الخاصة بك. كل طلب موقّع بسرّ التوقيع الخاص بك، لتتمكن من إثبات أنه جاء من RugWard ولم يُعدَّل.

إعدادات Webhook
مربع Webhook: رابط نقطة النهاية، و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 يستبدل السرّ فورًا؛ انشر السرّ الجديد مباشرةً (تتوقف التواقيع بالسرّ القديم في الحال). وحفظ رابط فارغ يوقف إشعارات Webhook.

الأحداث#

الحدثيُرسَل عندما
invoice.pendingرُصدت دفعة، بانتظار التأكيدات أو بقية المبلغ
invoice.paidدُفعت بالكامل وتأكّدت: اشحن
invoice.overpaidدُفعت بزيادة تفوق 1%: اشحن
invoice.underpaidانتهى تثبيت السعر والمبلغ المؤكد ناقص
invoice.lateوصلت دفعة بعد انتهاء الصلاحية
invoice.expiredانتهت دون دفع
invoice.canceledألغيتها أنت
pingزر Send test ping في اللوحة

يُطلَق كل حدث مرة واحدة على الأكثر لكل فاتورة. الفواتير التجريبية لا ترسل إشعارات 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… (64 محرفًا سداسيًا عشريًا).

  1. اقرأ جسم الطلب الخام كما وصل تمامًا (قبل أي تحليل JSON).
  2. احسب HMAC-SHA256(secret, t + "." + raw_body) وحوّله إلى صيغة سداسية عشرية.
  3. قارنه بقيمة v1 في زمن ثابت (constant time).
  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 ثانية ثم دقيقتين ثم 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، أو شرطة مائلة ناقصة في النهاية)؛ استخدم الرابط النهائي
يُظهر ping الرمز 405المسار لا يقبل POST
التوقيع لا يتطابق أبدًاحُلِّل الجسم أو أُعيد ترميزه قبل الحساب، أو استُخدم سرّ خاطئ (بعد New signing secret)، أو ساعة الخادم غير مضبوطة
لا أحداث للفواتير التجريبيةمتوقَّع: الفواتير التجريبية لا ترسل إشعارات Webhook أبدًا

آخر تحديث: 30 أكتوبر 2026

إشعارات Webhook والتحقق من التوقيع · RugWard التوثيق | RugWard