RugWard Pay
वेबहुक और सिग्नेचर सत्यापन
हर इनवॉइस स्थिति बदलने पर साइन की हुई HTTPS सूचना पाएँ, उसे curl, Node.js, Python या PHP में सत्यापित करें, और दोबारा प्रयासों व डुप्लिकेट को सुरक्षित रूप से सँभालें।
इस पेज पर
वेबहुक कैसे काम करते हैं#
जब किसी इनवॉइस की स्थिति बदलती है, RugWard आपके एंडपॉइंट पर JSON बॉडी के साथ POST भेजता है। हर अनुरोध आपके साइनिंग सीक्रेट (signing secret) से साइन होता है, ताकि आप साबित कर सकें कि वह RugWard से आया है और बदला नहीं गया।

सेटअप#
- डैशबोर्ड में Webhook में अपना एंडपॉइंट डालें और Save पर क्लिक करें।
- साइनिंग सीक्रेट (
whsec_और उसके बाद 40 अक्षर) केवल एक बार दिखाया जाता है। इसे अपने सर्वर पर API कुंजी के साथ रखें। - Send test ping पर क्लिक करें। डैशबोर्ड वह HTTP स्थिति दिखाता है जो आपके एंडपॉइंट ने लौटाई।
- बॉक्स के नीचे डिलीवरी लॉग हर इवेंट, उसका परिणाम और प्रयासों की संख्या दिखाता है।
एंडपॉइंट के नियम:
- केवल
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 बटन |
हर इवेंट प्रति इनवॉइस अधिकतम एक बार भेजा जाता है। टेस्ट इनवॉइस कभी वेबहुक नहीं भेजते।
अनुरोध#
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हेडर में भी है: डुप्लिकेट अनदेखा करने के लिए इसका इस्तेमाल करें।createdUnix सेकंड में है;data.invoiceवही ऑब्जेक्ट है जोGET /api/pay/merchant/invoices/:idलौटाता है।
सिग्नेचर सत्यापित करें#
हेडर ऐसा दिखता है: RugWard-Signature: t=1793324100,v1=5f2c… (64 हेक्स अक्षर)।
- अनुरोध की कच्ची (raw) बॉडी ठीक वैसे पढ़ें जैसी मिली (किसी भी 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 सेकंड, 2 मिनट और 8 मिनट बाद। उसके बाद इवेंट डिलीवरी लॉग में विफल चिह्नित होता है। इसलिए आपके हैंडलर को:
- जल्दी 2xx लौटाना चाहिए और धीमा काम बाद में करना चाहिए;
- idempotent होना चाहिए:
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