RugWard Pay
Webhook'lar ve imza doğrulama
Her fatura durum değişikliğinde imzalı bir HTTPS bildirimi alın, bunu curl, Node.js, Python veya PHP ile doğrulayın ve yeniden denemeleri ve kopyaları güvenle yönetin.
Bu sayfada
Webhook'lar nasıl çalışır#
Bir faturanın durumu değiştiğinde RugWard, uç noktanıza JSON gövdeli bir POST gönderir. Her istek imzalama sırrınızla (signing secret) imzalanır; böylece isteğin RugWard'dan geldiğini ve değiştirilmediğini kanıtlayabilirsiniz.

Kurulum#
- Panelde uç noktanızı Webhook alanına girin ve Save'e tıklayın.
- İmzalama sırrı (
whsec_ve ardından 40 karakter) yalnızca bir kez gösterilir. Sunucunuzda, API anahtarının yanında saklayın. - Send test ping'e tıklayın. Panel, uç noktanızın döndürdüğü HTTP durumunu gösterir.
- Kutunun altındaki teslimat günlüğü her olayı, sonucunu ve deneme sayısını gösterir.
Uç nokta kuralları:
- yalnızca
https://, standart portta (:8443yok), en fazla 500 karakter; - herkese açık bir ana makine adı: IP adresleri,
localhostve özel ağlar reddedilir; - 10 saniye içinde herhangi bir 2xx durumuyla yanıt verin. Yönlendirmeler izlenmez: 3xx yanıtı başarısızlık sayılır.
New signing secret sırrı hemen değiştirir; yenisini hemen devreye alın (eski sırla yapılan imzalar anında geçersiz olur). Boş bir URL kaydetmek webhook'ları kapatır.
Olaylar#
| Olay | Gönderildiği durum |
|---|---|
invoice.pending | bir ödeme görüldü, onaylar veya kalan tutar bekleniyor |
invoice.paid | tamamen ödendi ve onaylandı: gönderin |
invoice.overpaid | %1'den fazla fazlasıyla ödendi: gönderin |
invoice.underpaid | kur kilidi, onaylanan tutar yetersizken sona erdi |
invoice.late | süre dolduktan sonra bir ödeme geldi |
invoice.expired | ödeme olmadan süresi doldu |
invoice.canceled | siz iptal ettiniz |
ping | paneldeki Send test ping düğmesi |
Her olay fatura başına en fazla bir kez tetiklenir. Test faturaları asla webhook göndermez.
İstek#
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>biçimindedir veRugWard-Event-Idbaşlığında da bulunur: kopyaları yok saymak için kullanın.createdUnix saniyesi cinsindendir;data.invoice,GET /api/pay/merchant/invoices/:idçağrısının döndürdüğü nesnenin aynısıdır.
İmzayı doğrulayın#
Başlık şöyle görünür: RugWard-Signature: t=1793324100,v1=5f2c… (64 onaltılık karakter).
- İstek gövdesini, alındığı haliyle ham olarak okuyun (herhangi bir JSON ayrıştırmasından önce).
HMAC-SHA256(secret, t + "." + raw_body)değerini hesaplayın ve onaltılık olarak kodlayın.v1ile sabit zamanlı karşılaştırın.tsaatinizden 5 dakikadan fazla farklıysa isteği reddedin (tekrar saldırılarını önler).- Ancak bundan sonra JSON'u ayrıştırın.
JSON'u önce ayrıştırıp yeniden serileştirmek baytları değiştirir ve imzayı bozar. İmzayı her zaman ham gövde üzerinde kontrol edin.
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);Uç noktanızı elle test etmek için bir gövdeyi kendiniz imzalayın:
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"Yeniden denemeler ve kopyalar#
Uç noktanız zaman aşımına uğrarsa veya 2xx ile yanıt vermezse RugWard yeniden dener: toplamda en fazla 4 deneme, yaklaşık 30 saniye, 2 dakika ve 8 dakika sonra. Bundan sonra olay, teslimat günlüğünde başarısız olarak işaretlenir. Bu nedenle işleyiciniz:
- hızla 2xx ile yanıt vermeli, yavaş işleri sonra yapmalı;
- idempotent olmalı:
RugWard-Event-Iddeğerini saklayın ve zaten işlediğiniz olayları atlayın; - sıraya bağlı olmamalı: şüphe durumunda faturayı API ile yeniden okuyun.
Webhook'lar hızlı bir sinyaldir, tek sinyal değildir. Sunucunuz bir süre kapalı kaldıysa son faturaları API ile listeleyin ve durumları karşılaştırın.
Sorun giderme#
| Belirti | Olası neden |
|---|---|
| "Webhook URL must be https:// with a public host name" | bir IP adresi, özel bir port, http:// veya dahili bir ana makine adı |
| ping HTTP 301/302/308 gösteriyor | sunucunuz yönlendiriyor (örneğin http→https veya eksik sondaki eğik çizgi); son URL'yi kullanın |
| ping 405 gösteriyor | rota POST kabul etmiyor |
| imza hiç eşleşmiyor | gövde hash'lemeden önce ayrıştırıldı veya yeniden kodlandı, yanlış sır kullanılıyor (New signing secret sonrası) veya sunucu saati yanlış |
| test faturaları için olay yok | beklenen durum: test faturaları asla webhook göndermez |
Son güncelleme: 30 Ekim 2026