RugWard Pay
إشعارات Webhook والتحقق من التوقيع
احصل على إشعار HTTPS موقّع عند كل تغيير في حالة الفاتورة، وتحقّق منه بلغات curl وNode.js وPython وPHP، وتعامل مع إعادة المحاولة والتكرار بأمان.
في هذه الصفحة
كيف تعمل إشعارات Webhook#
عندما تتغير حالة فاتورة، يرسل RugWard طلب POST بجسم JSON إلى نقطة النهاية الخاصة بك. كل طلب موقّع بسرّ التوقيع الخاص بك، لتتمكن من إثبات أنه جاء من RugWard ولم يُعدَّل.

الإعداد#
- في اللوحة أدخل نقطة النهاية في Webhook وانقر Save.
- يظهر سرّ التوقيع (
whsec_متبوعًا بـ 40 محرفًا) مرة واحدة. خزّنه على خادمك بجوار مفتاح API. - انقر Send test ping. تعرض اللوحة رمز HTTP الذي أعادته نقطة النهاية.
- يعرض سجل التسليم أسفل المربع كل حدث ونتيجته وعدد المحاولات.
قواعد نقطة النهاية:
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 أبدًا.
الطلب#
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": {} }idهو<invoice id>:<status>ويوجد أيضًا في الترويسةRugWard-Event-Id: استخدمه لتجاهل التكرارات.createdبالثواني بتوقيت Unix؛ وdata.invoiceهو الكائن نفسه الذي يعيدهGET /api/pay/merchant/invoices/:id.
التحقق من التوقيع#
تبدو الترويسة هكذا: RugWard-Signature: t=1793324100,v1=5f2c… (64 محرفًا سداسيًا عشريًا).
- اقرأ جسم الطلب الخام كما وصل تمامًا (قبل أي تحليل JSON).
- احسب
HMAC-SHA256(secret, t + "." + raw_body)وحوّله إلى صيغة سداسية عشرية. - قارنه بقيمة
v1في زمن ثابت (constant time). - ارفض الطلب إن ابتعد
tعن ساعتك بأكثر من 5 دقائق (لمنع إعادة الإرسال). - بعد ذلك فقط حلّل JSON.
تحليل JSON أولًا ثم إعادة تسلسله يغيّر البايتات ويُفسد التوقيع. تحقّق دائمًا من التوقيع على الجسم الخام.
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);لاختبار نقطة النهاية يدويًا، وقّع جسمًا بنفسك:
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