주 콘텐츠로 건너뛰기
학습가이드웹훅으로 에이전트 결제를 처리하세요.
가이드

개발자들이 가장 많이 묻는 웹후크 패턴입니다.

15분
짧은 답변

신뢰할 수 있는 웹훅 핸들러는 네 가지 작업을 순서대로 수행합니다: 원본 본문에 대해 서명을 검증합니다 (Node의 webhooks.verify, 다른 곳의 문서화된 HMAC), 이벤트 ID로 중복 제거, 작업을 백그라운드 큐에 추가, 그리고 200을 반환합니다. 장기 실행 작업은 작업자에서 발생하며, 큐 계층에서 재시도 및 멱등성이 적용됩니다. 실패 이벤트가 없기 때문에, 예약된 스윕은 결제를 기다리는 작업이 정체되는 시간을 초과하고 이를 조정합니다.

전제 조건

시작하기 전에.

  • 작동하는 에이전트 프로필과 대시보드에서의 서명 비밀(설정 - 웹후크).
  • 원시 본문 접근이 가능한 웹 프레임워크 - express.raw가 포함된 Express, FastAPI, Flask 등. 자동 JSON 파싱 미들웨어는 서명 검증을 깨뜨립니다.
  • 작업 큐: BullMQ (Node) 또는 Celery/arq (Python). 웹후크는 빠르게 200을 반환하고 큐는 느린 작업을 수행합니다.
  • 업서트 원시 기능이 있는 데이터베이스(Postgres 작동; Redis SET NX는 단기 중복 제거에도 작동).
  • 공용 HTTPS 엔드포인트 - 개발 중, ngrok 또는 배포 미리보기. 발신자는 개인 URL로 배달하지 않습니다.
4단계 중 1단계

서명을 확인하세요.

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
4단계 중 2단계

핸들러를 멱등성 있게 만드세요.

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)
4단계 중 3단계

대기열에 추가하고 200을 빠르게 반환하세요.

웹후크 엔드포인트는 1초 이내에 응답해야 합니다. 더 느리면 타임아웃과 재시도를 초래합니다. 패턴은: 확인, 큐에 추가, 응답입니다. 큐는 재시도, 지수 백오프 및 자체 아이템포턴시를 가진 작업자에서 실제 배달을 실행합니다. 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에 대한 단일 왕복); 원격 호출에서 웹후크를 차단하지 마십시오.

4단계 중 4단계

결제가 도착하지 않는 경우를 처리합니다.

실패 웹훅이 없습니다 - 구매자가 포기하면 이벤트가 도착하지 않고 에이전트는 '결제 대기 중' 상태에 갇히게 됩니다. 그러므로 스스로 감지하십시오: 너무 오랫동안 기다린 작업에 대해 예약된 스윕을 실행하고, 실제로 정산되었는지 확인하기 위해 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), 먼저 검증한 다음 구문 분석합니다.

웹훅 핸들러 내에서 실제 작업 수행

웹후크는 공격적인 재시도 정책을 가지고 있습니다. 만약 여러분의 핸들러가 작업을 전달하는 데 30초가 걸린다면, 발신자의 타임아웃이 작동하고 웹후크가 다시 전송됩니다 - 이제 동일한 결제에 대해 두 번의 전송이 진행 중입니다. 항상: 확인하고, 큐에 추가하고, 2xx를 반환하세요. 실제 작업은 필요한 만큼 오래 걸릴 수 있는 백그라운드 작업자에서 실행됩니다.

비즈니스 로직을 전달하기 위해 HTTP 상태 사용

사용자가 시스템에 더 이상 존재하지 않을 때 핸들러가 4xx를 반환하면 발신자는 이를 '잘못된 요청'으로 간주하고 재시도를 중단합니다. 동일한 조건에 대해 5xx를 반환하면 발신자는 영원히 재시도하고 대기열이 가득 차게 됩니다. 이벤트를 안전하게 지속했거나 중복으로 인식한 후 200을 반환하세요; 비즈니스 결정을 표현하기 위해 HTTP 상태가 아닌 대기열 논리를 사용하세요.

이벤트 ID 대신 페이로드 해시에 대한 아이템포턴시

같은 에이전트에 대한 두 가지 다른 이벤트(결제 수신 및 이후 결제 전송)는 서로 다른 본체를 가지며 정당하게 별도의 처리가 필요합니다. 본체 해시에 대한 중복 제거가 있다면 그 중 하나를 삭제할 수 있습니다. X-Blockchain0x-Event-Id(전달당 고유)에서 중복 제거하고 이벤트 유형이 핸들러가 수행하는 작업을 결정하도록 합니다.

별도의 확인 이벤트를 기대합니다

전송된 이벤트는 payment.received, payment.sent, wallet.deployed 및 webhook.test입니다 - 별도의 확인 이벤트는 없습니다. payment.received는 전송이 블록에 있을 때 발생하며, 이는 대부분의 작업에 대한 신호입니다. 비싸거나 되돌릴 수 없는 작업의 경우, transactions.get을 폴링하고 자신의 확인 임계값을 적용하십시오; 존재하지 않는 이벤트를 기다리지 마십시오.

다음 단계

웹훅이 완벽해지면.

웹훅은 어려운 부분입니다. 위의 네 가지 패턴이 마련되면 나머지 작업은 주로 운영적입니다: 실패 경로를 테스트하는 테스트 환경, 업스트림 에이전트가 핸들러를 과부하하지 않도록 하는 지출 제어, 그리고 최종 보안 검토입니다.

전체 참조는 docs.blockchain0x.com에 있습니다. 웹후크 용어집: 결제 위임. 제품 표면: 결제 API.

마지막 검토: 2026-05-15. CC BY 4.0에 따라 게시됨.

부하 하에서도 신뢰할 수 있는 웹훅입니다.

서명됨, 재시도됨, 멱등성. 무료로 시작하세요.