Các mẫu webhook mà các nhà phát triển thường hỏi nhất.
Một bộ xử lý webhook đáng tin cậy thực hiện bốn việc theo thứ tự: xác minh chữ ký với thân thô (webhooks.verify trong Node, HMAC đã được tài liệu ở nơi khác), loại bỏ trùng lặp theo ID sự kiện, đưa công việc vào hàng đợi nền, và trả về 200. Công việc kéo dài diễn ra trong công nhân, với các lần thử lại và tính không đổi ở lớp hàng đợi. Bởi vì không có sự kiện thất bại, một cuộc quét theo lịch sẽ hết thời gian các công việc bị kẹt chờ thanh toán và hòa giải chúng.
Trước khi bạn bắt đầu.
- Một hồ sơ tác nhân hoạt động và bí mật ký từ bảng điều khiển của bạn (Cài đặt - Webhooks).
- Một khung web với quyền truy cập raw-body - Express với
express.raw, FastAPI, Flask, v.v. Middleware tự động phân tích JSON làm hỏng việc xác minh chữ ký. - Một hàng đợi công việc: BullMQ (Node) hoặc Celery/arq (Python). Webhook trả về 200 nhanh và hàng đợi thực hiện công việc chậm.
- Một cơ sở dữ liệu với một phép chèn hoặc cập nhật (Postgres hoạt động; Redis SET NX cũng hoạt động cho việc loại bỏ trùng lặp ngắn hạn).
- Một điểm cuối HTTPS công khai - trong quá trình phát triển, ngrok hoặc một bản xem trước triển khai. Người gửi sẽ không giao hàng đến các URL riêng tư.
Xác minh chữ ký.
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)) <= 300Làm cho trình xử lý trở nên không thay đổi.
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)Xếp hàng và trả về 200 nhanh.
Điểm cuối webhook nên phản hồi trong vòng một giây. Bất kỳ điều gì chậm hơn đều mời gọi thời gian chờ và thử lại. Mẫu là: xác minh, xếp hàng, phản hồi. Hàng đợi thực hiện việc giao hàng thực tế trong một công nhân với các lần thử lại, lùi lại theo cấp số nhân, và tính chất không đổi của riêng nó. BullMQ và Celery đều hỗ trợ ID theo công việc, điều này ngăn chặn việc xếp hàng lại vô tình cùng một sự kiện.
// 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 theo cùng một hình dạng ở phía Python - đăng ký nhiệm vụ với một ID công việc xác định và để hàng đợi xử lý các lần thử lại. Ràng buộc chính là việc xếp hàng phải nhanh (một vòng đi vòng về đến Redis); không bao giờ chặn webhook trên các cuộc gọi từ xa.
Xử lý một khoản thanh toán không bao giờ đến.
Không có webhook thất bại - nếu một người mua bỏ cuộc, không có sự kiện nào đến, và tác nhân bị kẹt trong trạng thái 'đang chờ thanh toán'. Vì vậy, hãy phát hiện nó tự mình: thực hiện một cuộc quét theo lịch trên các công việc đã chờ quá lâu, hòa giải với chuỗi bằng transactions.get trong trường hợp nó thực sự đã được giải quyết, sau đó giải phóng tài nguyên bị giữ, di chuyển công việc đến trạng thái chưa thanh toán cuối cùng, và (nếu phù hợp) hiển thị kết quả cho người dùng.
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"])Năm sai lầm làm mất hoặc trùng lặp sự kiện.
Phân tích nội dung trước khi xác minh chữ ký
HMAC phải được tính toán trên các byte thô mà người gửi đã ký. Nếu framework của bạn tự động phân tích JSON trước khi trình xử lý của bạn chạy, các byte bạn ký cục bộ sẽ không khớp với các byte mà người gửi đã ký (khoảng trắng khác nhau, thứ tự khóa, mã hóa) và mọi chữ ký sẽ trông không hợp lệ. Cấu hình tuyến đường để nhận thân thô (Express: express.raw, Flask: request.get_data), xác minh trước, sau đó phân tích.
Thực hiện công việc thực sự bên trong bộ xử lý webhook
Webhooks có chính sách thử lại mạnh mẽ. Nếu bộ xử lý của bạn mất 30 giây để giao công việc, thời gian chờ của người gửi sẽ kích hoạt và webhook sẽ được gửi lại - bây giờ bạn có hai lần giao hàng đang diễn ra cho cùng một khoản thanh toán. Luôn luôn: xác minh, xếp hàng, trả về 2xx. Công việc thực tế chạy trong một worker nền có thể mất thời gian cần thiết.
Sử dụng trạng thái HTTP để giao tiếp logic kinh doanh
Nếu trình xử lý của bạn trả về 4xx khi người dùng không còn tồn tại trong hệ thống của bạn, người gửi coi đó là 'yêu cầu không hợp lệ' và ngừng thử lại. Nếu nó trả về 5xx cho cùng một điều kiện, người gửi sẽ thử lại mãi mãi và hàng đợi của bạn sẽ đầy. Trả về 200 khi bạn đã lưu trữ sự kiện một cách an toàn (hoặc nhận ra nó là một bản sao); sử dụng logic hàng đợi, không phải trạng thái HTTP, để thể hiện các quyết định kinh doanh.
Idempotency trên một băm tải thay vì ID sự kiện
Hai sự kiện khác nhau về cùng một đại lý (một payment.received và một payment.sent sau đó) có các thân khác nhau và hợp pháp cần xử lý riêng biệt. Nếu việc loại bỏ trùng lặp của bạn dựa trên một băm thân, bạn có thể bỏ một trong số chúng. Loại bỏ trùng lặp trên X-Blockchain0x-Event-Id (duy nhất cho mỗi lần giao hàng), và để loại sự kiện điều khiển những gì trình xử lý của bạn thực hiện.
Mong đợi một sự kiện xác nhận riêng biệt
Các sự kiện đã được gửi là payment.received, payment.sent, wallet.deployed và webhook.test - không có sự kiện xác nhận riêng biệt. payment.received kích hoạt khi chuyển khoản nằm trong một khối, đó là tín hiệu của bạn cho hầu hết công việc. Đối với một thứ gì đó đắt đỏ hoặc không thể đảo ngược, hãy truy vấn transactions.get và áp dụng ngưỡng xác nhận của riêng bạn; đừng chờ một sự kiện không tồn tại.
Khi webhook trở nên an toàn.
Webhooks là phần khó khăn. Với bốn mẫu ở trên, công việc còn lại chủ yếu là hoạt động: một môi trường thử nghiệm kiểm tra các đường dẫn thất bại, kiểm soát chi tiêu để một đại lý upstream không làm ngập trình xử lý của bạn, và một đánh giá bảo mật cuối cùng.
Thử nghiệm thanh toán đại lý mà không cần tiền thật
Thiết lập các kiểm soát chi tiêu của agent mà vẫn tồn tại sau khi bị tiêm nhắc
Bảo mật ví agent của bạn trước khi ra mắt
Tài liệu đầy đủ tại docs.blockchain0x.com. Từ điển webhook: ủy quyền thanh toán. Bề mặt sản phẩm: API Thanh toán.