ข้ามไปยังเนื้อหาหลัก
เรียนรู้คู่มือจัดการการชำระเงินของตัวแทนด้วยเว็บฮุก
คู่มือ

รูปแบบ webhook ที่นักพัฒนาถามถึงมากที่สุด

15 นาที
คำตอบสั้น

ตัวจัดการ webhook ที่เชื่อถือได้ทำสี่สิ่งตามลำดับ: ตรวจสอบลายเซ็นกับเนื้อหาดิบ (webhooks.verify ใน Node, HMAC ที่บันทึกไว้ในที่อื่น), ลบการทำซ้ำตาม ID เหตุการณ์, เพิ่มงานลงในคิวพื้นหลัง, และส่งคืน 200 งานที่ใช้เวลานานจะเกิดขึ้นในผู้ทำงาน โดยมีการลองใหม่และความเป็นอัตถิภาวะที่ชั้นคิว เนื่องจากไม่มีเหตุการณ์ความล้มเหลว การทำความสะอาดตามกำหนดเวลาจะหมดเวลางานที่ติดอยู่รอการชำระเงินและปรับยอด.

ข้อกำหนดเบื้องต้น

ก่อนที่คุณจะเริ่ม.

  • โปรไฟล์เอเจนต์ที่ทำงานได้และความลับในการลงนามจากแดชบอร์ดของคุณ (การตั้งค่า - เว็บฮุก)
  • เฟรมเวิร์กเว็บที่มีการเข้าถึงร่างกายดิบ - Express กับ express.raw, FastAPI, Flask, ฯลฯ. การแยกวิเคราะห์ JSON อัตโนมัติจะทำให้การตรวจสอบลายเซ็นล้มเหลว
  • คิวงาน: BullMQ (Node) หรือ Celery/arq (Python). เว็บฮุกจะส่งกลับ 200 อย่างรวดเร็วและคิวจะทำงานช้า
  • ฐานข้อมูลที่มีการอัปเดตและแทรก (Postgres ใช้งานได้; Redis SET NX ก็ใช้งานได้สำหรับการลบข้อมูลซ้ำที่มีอายุสั้น)
  • จุดสิ้นสุด HTTPS สาธารณะ - ในการพัฒนา, ngrok หรือการแสดงตัวอย่างการปรับใช้. ผู้ส่งจะไม่ส่งไปยัง URL ส่วนตัว
ขั้นตอนที่ 1 จาก 4

ตรวจสอบลายเซ็น.

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.

TypeScript
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");
});
Python
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
ขั้นตอนที่ 2 จาก 4

ทำให้ตัวจัดการเป็นอิดempotent

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.

TypeScript
// 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);
}
Python
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)
ขั้นตอนที่ 3 จาก 4

เพิ่มในคิวและส่งคืน 200 อย่างรวดเร็ว.

จุดสิ้นสุดของ webhook ควรตอบสนองภายในหนึ่งวินาที อะไรก็ตามที่ช้ากว่าจะทำให้เกิดการหมดเวลาและการลองใหม่ รูปแบบคือ: ตรวจสอบ รอคิว ตอบสนอง คิวจะทำการส่งมอบจริงในคนงานที่มีการลองใหม่ การถอยกลับแบบเลขชี้กำลัง และการไม่เปลี่ยนแปลงของตนเอง BullMQ และ Celery รองรับ ID ต่อการทำงาน ซึ่งป้องกันการรอคิวเหตุการณ์เดียวกันโดยไม่ตั้งใจ

TypeScript (BullMQ)
// 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);
});
Python (Celery)
# 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", 200

arq มีรูปแบบเดียวกันในฝั่ง Python - ลงทะเบียนงานด้วย id งานที่กำหนดและให้คิวจัดการการลองใหม่ ข้อจำกัดหลักคือการเพิ่มในคิวเองต้องรวดเร็ว (การเดินทางไปกลับเพียงครั้งเดียวไปยัง Redis); อย่าบล็อก webhook ในการเรียกจากระยะไกล.

ขั้นตอนที่ 4 จาก 4

จัดการการชำระเงินที่ไม่เคยมาถึง.

ไม่มี webhook ล้มเหลว - หากผู้ซื้อทิ้ง ไม่มีเหตุการณ์ใดมาถึง และตัวแทนติดอยู่ใน 'รอการชำระเงิน'. ดังนั้นตรวจจับด้วยตัวคุณเอง: รันการทำความสะอาดตามกำหนดเวลาบนงานที่รอนานเกินไป ปรับยอดกับเครือข่ายด้วย transactions.get ในกรณีที่มันตั้งถิ่นฐานจริง จากนั้นปล่อยทรัพยากรที่ถูกถือไว้ ย้ายงานไปยังสถานะที่ไม่ชำระเงินสุดท้าย และ (ถ้าเหมาะสม) แสดงผลลัพธ์ให้ผู้ใช้.

TypeScript
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 });
  }
}
Python
# 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"])
ข้อผิดพลาดทั่วไป

ห้าข้อผิดพลาดที่ทำให้เหตุการณ์หายไปหรือลอกเลียน

การวิเคราะห์เนื้อหาก่อนการตรวจสอบลายเซ็น

HMAC ต้องคำนวณจากไบต์ดิบที่ผู้ส่งลงนาม หากเฟรมเวิร์กของคุณทำการแยก JSON โดยอัตโนมัติก่อนที่ตัวจัดการของคุณจะทำงาน ไบต์ที่คุณลงนามในเครื่องจะไม่ตรงกับไบต์ที่ผู้ส่งลงนาม (ช่องว่างต่างกัน, ลำดับคีย์, การเข้ารหัส) และลายเซ็นทุกอันจะดูไม่ถูกต้อง กำหนดเส้นทางเพื่อรับข้อมูลดิบ (Express: express.raw, Flask: request.get_data), ตรวจสอบก่อนแล้วจึงแยก

ทำงานจริงภายในตัวจัดการ webhook

Webhooks มีนโยบายการลองใหม่ที่รุนแรง หาก handler ของคุณใช้เวลา 30 วินาทีในการส่งมอบงาน เวลาเอาต์ไทม์ของผู้ส่งจะทำงานและ webhook จะถูกส่งใหม่ - ตอนนี้คุณมีการจัดส่งสองรายการในอากาศสำหรับการชำระเงินเดียวกัน เสมอ: ตรวจสอบ, ใส่คิว, คืน 2xx งานจริงจะทำงานใน background worker ที่สามารถใช้เวลานานเท่าที่ต้องการ

การใช้สถานะ HTTP เพื่อสื่อสารตรรกะทางธุรกิจ

หากตัวจัดการของคุณส่งคืน 4xx เมื่อผู้ใช้อีกต่อไปไม่มีอยู่ในระบบของคุณ ผู้ส่งจะถือว่านั้นเป็น 'คำขอที่ไม่ถูกต้อง' และหยุดการลองใหม่ หากส่งคืน 5xx สำหรับเงื่อนไขเดียวกัน ผู้ส่งจะลองใหม่ตลอดไปและคิวของคุณจะเต็ม ส่งคืน 200 เมื่อคุณได้บันทึกเหตุการณ์อย่างปลอดภัย (หรือตระหนักว่าเป็นการซ้ำ); ใช้ตรรกะคิว ไม่ใช่สถานะ HTTP เพื่อแสดงการตัดสินใจทางธุรกิจ

Idempotency บนแฮชของข้อมูลแทนที่จะเป็น ID ของเหตุการณ์

สองเหตุการณ์ที่แตกต่างกันเกี่ยวกับตัวแทนเดียวกัน (การชำระเงินที่ได้รับและการชำระเงินที่ส่งในภายหลัง) มีเนื้อหาที่แตกต่างกันและต้องการการประมวลผลแยกต่างหาก หากการลบข้อมูลซ้ำของคุณอยู่ที่แฮชเนื้อหา คุณสามารถละทิ้งหนึ่งในนั้นได้ ลบข้อมูลซ้ำตาม X-Blockchain0x-Event-Id (ไม่ซ้ำกันต่อการจัดส่ง) และให้ประเภทของเหตุการณ์กำหนดสิ่งที่ตัวจัดการของคุณทำ

คาดหวังเหตุการณ์การยืนยันแยกต่างหาก

เหตุการณ์ที่จัดส่งคือ payment.received, payment.sent, wallet.deployed และ webhook.test - ไม่มีเหตุการณ์ยืนยันแยกต่างหาก payment.received จะเกิดขึ้นเมื่อการโอนอยู่ในบล็อก ซึ่งเป็นสัญญาณของคุณสำหรับงานส่วนใหญ่ สำหรับสิ่งที่มีค่าใช้จ่ายสูงหรือไม่สามารถย้อนกลับได้ ให้ตรวจสอบ transactions.get และใช้เกณฑ์การยืนยันของคุณเอง; อย่ารอเหตุการณ์ที่ไม่มีอยู่.

ขั้นตอนถัดไป

เมื่อเว็บฮุกมีความปลอดภัยอย่างสมบูรณ์

Webhooks เป็นส่วนที่ยาก ด้วยรูปแบบทั้งสี่ที่กล่าวถึงข้างต้น งานที่เหลือส่วนใหญ่จะเป็นการดำเนินงาน: สภาพแวดล้อมการทดสอบที่ทดสอบเส้นทางการล้มเหลว การควบคุมการใช้จ่ายเพื่อไม่ให้ตัวแทนจากต้นน้ำทำให้ตัวจัดการของคุณล้น และการตรวจสอบความปลอดภัยขั้นสุดท้าย

เอกสารอ้างอิงเต็มรูปแบบที่ docs.blockchain0x.com. พจนานุกรมของ webhook: คำสั่งชำระเงิน. พื้นผิวผลิตภัณฑ์: Payment API.

ตรวจสอบล่าสุด: 2026-05-15. เผยแพร่ภายใต้ CC BY 4.0.

Webhooks ที่คุณสามารถไว้วางใจได้ภายใต้ภาระ

ลงนาม, ลองใหม่, ไม่เปลี่ยนแปลง เริ่มต้นฟรี.