Geliştiricilerin en çok sorduğu webhook desenleri.
Güvenilir bir webhook işleyici, sırayla dört şey yapar: ham gövdeye karşı imzayı doğrulamak (Node'daki webhooks.verify, belgelenmiş HMAC başka bir yerde), olay kimliğine göre dedupe etmek, işi arka planda bir kuyruğa eklemek ve 200 döndürmek. Uzun süreli işler işçi içinde gerçekleşir, kuyruk katmanında tekrar denemeler ve idempotans ile. Başarısızlık olayı olmadığı için, planlı bir tarama, ödeme bekleyen takılı işleri zaman aşımına uğratır ve bunları uzlaştırır.
Başlamadan önce.
- Çalışan bir ajan profili ve kontrol panelinizden imza sırrı (Ayarlar - Webhooklar).
- Ham gövde erişimi olan bir web çerçevesi -
express.rawile Express, FastAPI, Flask, vb. Otomatik JSON ayrıştırma ara katmanı imza doğrulamasını bozar. - Bir iş kuyruğu: BullMQ (Node) veya Celery/arq (Python). Webhook hızlı bir şekilde 200 döner ve kuyruk yavaş çalışmayı yapar.
- Bir upsert ilkesine sahip bir veritabanı (Postgres çalışır; Redis SET NX kısa ömürlü dedupe için de çalışır).
- Geliştirme aşamasında bir genel HTTPS uç noktası - ngrok veya bir dağıtım önizlemesi. Gönderen özel URL'lere teslimat yapmayacaktır.
İmza doğrula.
The signature is HMAC-SHA256 over {t}.{rawBody} with your webhook secret, hex-encoded, in the X-Blockchain0x-Signature header (t=<unix>,v1=<hex>), inside a 5-minute replay window. In Node, webhooks.verify from @blockchain0x/node does it and returns a discriminated union; in other languages compute the same HMAC and compare in constant time. Raw-body access matters: if the bytes you sign locally differ from the bytes that arrived, it fails.
import express from "express";
import { webhooks } from "@blockchain0x/node";
const app = express();
// Raw body so the HMAC matches the exact bytes on the wire.
app.use(express.raw({ type: "application/json" }));
app.post("/webhooks/payment", (req, res) => {
const result = webhooks.verify({
headers: req.headers,
rawBody: req.body, // Buffer, raw bytes
secret: process.env.BLOCKCHAIN0X_WEBHOOK_SECRET!,
});
// Discriminated union: branch on ok, no try/catch.
if (!result.ok) return res.status(400).json({ code: result.code });
// result.eventType / result.eventId are now set.
handleEvent(result);
res.status(200).send("ok");
});import hmac, hashlib, os, time
from flask import request
SECRET = os.environ["BLOCKCHAIN0X_WEBHOOK_SECRET"].encode()
# In Node, webhooks.verify does this. In Python, verify by hand against the
# documented algorithm: HMAC-SHA256 over "{t}.{rawBody}", 300s replay window.
def verify_signature(raw_body: bytes) -> bool:
sig = request.headers.get("X-Blockchain0x-Signature", "")
ts = request.headers.get("X-Blockchain0x-Timestamp", "")
parts = dict(p.split("=", 1) for p in sig.split(",") if "=" in p)
t, v1 = parts.get("t", ts), parts.get("v1", sig)
want = hmac.new(SECRET, t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, v1) and abs(time.time() - int(t)) <= 300İşleyiciyi idempotent hale getirin.
Webhooks retry on any non-2xx response, and the same event will arrive multiple times under load even when nothing has gone wrong. Dedupe on the event's id using a database upsert. If the row already exists, skip; if it does not, insert and proceed. Postgres makes this a single statement.
// Pseudocode for a Postgres-backed dedupe table. Replace with your DB of choice.
async function processEventOnce(eventId: string, body: object) {
// INSERT ... ON CONFLICT DO NOTHING returns rowCount === 0 on duplicate.
const inserted = await db.query(
"INSERT INTO webhook_events(id) VALUES ($1) ON CONFLICT DO NOTHING",
[eventId],
);
if (inserted.rowCount === 0) return; // Already processed.
await handleEvent(body);
}async def process_event_once(event_id: str, body: dict):
# INSERT ... ON CONFLICT DO NOTHING returns 0 rows on duplicate.
inserted = await db.execute(
"INSERT INTO webhook_events (id) VALUES ($1) ON CONFLICT DO NOTHING",
event_id,
)
if inserted == "INSERT 0 0": # asyncpg-style status
return # Already processed.
await handle_event(body)Kuyruğa al ve hızlıca 200 döndür.
Webhook uç noktası bir saniye içinde yanıt vermelidir. Daha yavaş olan her şey zaman aşımına ve yeniden denemelere davet eder. Desen şudur: doğrula, sıraya al, yanıt ver. Kuyruk, yeniden denemeler, üssel geri çekilme ve kendi idempotansıyla bir işçi içinde gerçek teslimatı gerçekleştirir. BullMQ ve Celery, aynı olayın yanlışlıkla yeniden sıraya alınmasını önleyen her iş için kimlik destekler.
// Express handler: verify, enqueue, return 200 fast.
import { Queue } from "bullmq";
import { webhooks } from "@blockchain0x/node";
const paymentQueue = new Queue("payments");
app.post(
"/webhooks/payment",
express.raw({ type: "application/json" }),
async (req, res) => {
const result = webhooks.verify({
headers: req.headers,
rawBody: req.body,
secret: process.env.BLOCKCHAIN0X_WEBHOOK_SECRET!,
});
if (!result.ok) return res.status(400).json({ code: result.code });
await paymentQueue.add(result.eventType, { raw: req.body.toString() }, {
jobId: result.eventId, // Idempotency key.
removeOnComplete: true,
attempts: 5,
backoff: { type: "exponential", delay: 1000 },
});
res.status(200).send("ok");
},
);
// Worker file:
import { Worker } from "bullmq";
new Worker("payments", async (job) => {
await handleEvent(job.data);
});# Flask handler enqueues to Celery (or arq) and returns 200 quickly.
from celery import Celery
from flask import request
celery = Celery("payments", broker=os.environ["REDIS_URL"])
@celery.task(bind=True, max_retries=5)
def handle_payment_event(self, event_type, raw):
try:
process_event_once(event_type, raw)
except Exception as exc:
raise self.retry(exc=exc, countdown=2 ** self.request.retries)
@app.post("/webhooks/payment")
def webhook():
raw = request.get_data()
if not verify_signature(raw):
abort(401)
event_id = request.headers.get("X-Blockchain0x-Event-Id", "")
event_type = request.headers.get("X-Blockchain0x-Event-Type", "")
handle_payment_event.apply_async(args=[event_type, raw.decode()], task_id=event_id)
return "ok", 200arq, Python tarafında aynı yapıyı takip eder - görevi belirleyici bir iş kimliği ile kaydedin ve kuyruğun yeniden denemeleri yönetmesine izin verin. Ana kısıtlama, sıraya alma işleminin hızlı olması gerektiğidir (Redis'e tek bir gidiş-dönüş); uzaktan çağrılarda webhook'u asla engellemeyin.
Asla ulaşmayan bir ödemeyi yönetin.
Başarısızlık webhook'u yoktur - eğer bir alıcı terk ederse, hiçbir olay gelmez ve ajans 'ödemeyi bekliyor' durumunda kalır. Bu nedenle, bunu kendiniz tespit edin: çok uzun süre bekleyen işler üzerinde planlı bir tarama çalıştırın, gerçekten yerleştiyse zincirle transactions.get ile uzlaştırın, ardından tutulan kaynakları serbest bırakın, işi terminal ödenmemiş bir duruma taşıyın ve (uygun olduğunda) sonucu kullanıcıya gösterin.
async function sweepStaleAwaitingPayment() {
for (const job of await findJobsAwaitingPaymentOlderThan("1h")) {
// Reconcile against the chain before giving up.
const tx = job.txHash ? await client.transactions.get(job.txHash) : null;
if (tx) { markJobPaid(job.id); continue; } // It actually settled.
// 1. Release any held resources tied to the job.
releaseHeldResources(job.id);
// 2. Move it out of 'awaiting_payment' into a terminal 'unpaid' state.
markJobUnpaid(job.id);
// 3. (Optional) Notify the user, with a fresh payment link.
notifyUser(job.userId, { template: "agent_payment_unpaid", jobId: job.id });
}
}# Run on a schedule - there is no failure webhook to wait for.
def sweep_stale_awaiting_payment():
for job in find_jobs_awaiting_payment_older_than("1h"):
tx = client.transactions.get(job["tx_hash"]) if job.get("tx_hash") else None
if tx:
mark_job_paid(job["id"]) # It actually settled.
continue
release_held_resources(job["id"])
mark_job_unpaid(job["id"])
notify_user(job["user_id"], template="agent_payment_unpaid", job_id=job["id"])Olayları düşüren veya çoğaltan beş hata.
İmza doğrulamadan önce gövdeyi ayrıştırma
HMAC, göndericinin imzaladığı ham baytlar üzerinde hesaplanmalıdır. Eğer çerçeveniz, işleyiciniz çalışmadan önce JSON'u otomatik olarak ayrıştırıyorsa, yerel olarak imzaladığınız baytlar, göndericinin imzaladığı baytlarla eşleşmeyecektir (farklı boşluk, anahtar sırası, kodlama) ve her imza geçersiz görünecektir. Ham gövdeyi almak için rotayı yapılandırın (Express: express.raw, Flask: request.get_data), önce doğrulayın, sonra ayrıştırın.
Gerçek işi webhook işleyicisinde yapmak
Webhook'lar agresif yeniden deneme politikalarına sahiptir. İşleyicinizin işi teslim etmesi 30 saniye sürerse, göndericinin zaman aşımı devreye girer ve webhook yeniden gönderilir - şimdi aynı ödeme için iki teslimatınız var. Her zaman: doğrulayın, kuyruklayın, 2xx döndürün. Gerçek iş, ihtiyaç duyduğu kadar zaman alabilen bir arka plan işçisinde çalışır.
İş mantığını iletmek için HTTP durumunu kullanmak
Eğer işleyiciniz, kullanıcı sisteminizde artık mevcut değilse 4xx dönerse, gönderici bunu 'geçersiz istek' olarak değerlendirir ve yeniden denemeyi durdurur. Eğer aynı koşul için 5xx dönerse, gönderici sonsuza kadar yeniden dener ve kuyruğunuz dolup taşar. Olayı güvenli bir şekilde kalıcı hale getirdiğinizde (veya bir tekrar olarak tanıdığınızda) 200 döndürün; iş kararlarını ifade etmek için kuyruk mantığını kullanın, HTTP durumunu değil.
Olay kimliği yerine bir yük hash'inde idempotans
Aynı ajana ilişkin iki farklı olay (bir payment.received ve daha sonra bir payment.sent) farklı gövdelere sahiptir ve meşru olarak ayrı işleme gerektirir. Eğer dedupe bir gövde hash'ine dayanıyorsa, bunlardan birini bırakabilirsiniz. X-Blockchain0x-Event-Id üzerinde dedupe yapın (her teslimat için benzersiz) ve olay türünün işleyicinizin ne yapacağını belirlemesine izin verin.
Ayrı bir onay olayı bekleniyor
Gönderilen olaylar payment.received, payment.sent, wallet.deployed ve webhook.test'tir - ayrı bir onay olayı yoktur. payment.received, transfer bir blokta olduğunda tetiklenir, bu da çoğu iş için sinyalinizdir. Pahalı veya geri alınamaz bir şey için, transactions.get'i sorgulayın ve kendi onay eşiğinizi uygulayın; var olmayan bir olayı beklemeyin.
Webhook'lar güvenli hale geldiğinde.
Webhooks zor kısımdır. Yukarıdaki dört desen yerinde olduğunda, kalan iş çoğunlukla operasyoneldir: hata yollarını test eden bir test ortamı, yukarı akıştaki bir ajanın işleyicinizi aşırı yüklememesi için harcama kontrolleri ve son bir güvenlik incelemesi.
Gerçek para olmadan ajan ödemelerini test et
İstem enjeksiyonuna dayanacak şekilde ajanın harcama kontrollerini ayarlayın
Canlıya geçmeden önce ajanın cüzdanını güvence altına alın
Tam referans docs.blockchain0x.com'da. Webhook sözlüğü: ödeme yetkisi. Ürün yüzeyi: Ödeme API'si.