Ir para o conteúdo principal
AprenderGuiasGerencie pagamentos de agentes com webhooks
GUIA

Os padrões de webhook que os desenvolvedores mais perguntam.

15 minutos
RESPOSTA CURTA

Um manipulador de webhook confiável faz quatro coisas em ordem: verifica a assinatura contra o corpo raw (webhooks.verify no Node, o HMAC documentado em outro lugar), deduplica por id de evento, coloca o trabalho em uma fila de fundo e retorna 200. Trabalhos de longa duração acontecem no trabalhador, com tentativas e idempotência na camada da fila. Como não há um evento de falha, uma varredura programada expira trabalhos presos aguardando pagamento e os reconcilia.

PRÉ-REQUISITOS

Antes de você começar.

  • Um perfil de agente funcionando e o segredo de assinatura do seu painel (Configurações - Webhooks).
  • Um framework web com acesso a corpo bruto - Express com express.raw, FastAPI, Flask, etc. Middleware de análise automática de JSON quebra a verificação de assinatura.
  • Uma fila de trabalho: BullMQ (Node) ou Celery/arq (Python). O webhook retorna 200 rapidamente e a fila faz o trabalho lento.
  • Um banco de dados com uma primitiva de upsert (Postgres funciona; Redis SET NX também funciona para deduplicação de curta duração).
  • Um endpoint HTTPS público - em desenvolvimento, ngrok ou uma prévia de implantação. O remetente não entregará em URLs privadas.
PASSO 1 DE 4

Verificar a assinatura.

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
PASSO 2 DE 4

Torne o manipulador idempotente.

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)
PASSO 3 DE 4

Coloque na fila e retorne 200 rapidamente.

O endpoint do webhook deve responder dentro de um segundo. Qualquer coisa mais lenta convida a timeouts e tentativas. O padrão é: verificar, enfileirar, responder. A fila executa a entrega real em um trabalhador com tentativas, retrocesso exponencial e sua própria idempotência. BullMQ e Celery ambos suportam ID por trabalho, o que impede a re-enfileiração acidental do mesmo evento.

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 segue a mesma forma no lado Python - registre a tarefa com um id de trabalho determinístico e deixe a fila lidar com as tentativas. A principal restrição é que a enfileiração em si deve ser rápida (uma única viagem de ida e volta ao Redis); nunca bloqueie o webhook em chamadas remotas.

PASSO 4 DE 4

Gerencie um pagamento que nunca chega.

Não há webhook de falha - se um comprador abandona, nenhum evento chega e o agente fica preso em 'aguardando_pagamento'. Portanto, detecte isso você mesmo: execute uma varredura programada sobre trabalhos que esperaram tempo demais, reconcile contra a cadeia com transactions.get caso tenha realmente sido resolvido, então libere recursos retidos, mova o trabalho para um estado terminal não pago e (se apropriado) apresente o resultado ao usuário.

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"])
ARMADILHAS COMUNS

Cinco erros que descartam ou duplicam eventos.

Analisando o corpo antes de verificar a assinatura

HMAC deve ser calculado sobre os bytes brutos que o remetente assinou. Se seu framework analisa automaticamente o JSON antes que seu manipulador seja executado, os bytes que você assina localmente não corresponderão aos bytes que o remetente assinou (espaçamento em branco diferente, ordem das chaves, codificação) e cada assinatura parecerá inválida. Configure a rota para receber o corpo bruto (Express: express.raw, Flask: request.get_data), verifique primeiro e depois analise.

Fazendo o trabalho real dentro do manipulador de webhook

Webhooks têm políticas de nova tentativa agressivas. Se seu manipulador levar 30 segundos para entregar o trabalho, o tempo limite do remetente é acionado e o webhook é reenviado - agora você tem duas entregas em andamento para o mesmo pagamento. Sempre: verifique, coloque na fila, retorne 2xx. O trabalho real é executado em um trabalhador em segundo plano que pode levar o tempo que precisar.

Usando status HTTP para comunicar lógica de negócios

Se seu manipulador retornar um 4xx quando o usuário não existir mais em seu sistema, o remetente tratará isso como 'solicitação inválida' e parará de tentar novamente. Se retornar 5xx para a mesma condição, o remetente tentará para sempre e sua fila se encherá. Retorne 200 assim que você tiver persistido com segurança o evento (ou reconhecido como duplicado); use a lógica de fila, não o status HTTP, para expressar decisões de negócios.

Idempotência em um hash de carga útil em vez do ID do evento

Dois eventos diferentes sobre o mesmo agente (um payment.received e um payment.sent posterior) têm corpos diferentes e precisam legitimamente de processamento separado. Se sua dedupe estiver em um hash de corpo, você pode descartar um deles. Dedupe no X-Blockchain0x-Event-Id (único por entrega) e deixe o tipo de evento direcionar o que seu manipulador faz.

Esperando um evento de confirmação separado

Os eventos enviados são payment.received, payment.sent, wallet.deployed e webhook.test - não há um evento de confirmação separado. payment.received é acionado quando a transferência está em um bloco, que é seu sinal para a maioria dos trabalhos. Para algo caro ou irreversível, consulte transactions.get e aplique seu próprio limite de confirmação; não espere por um evento que não existe.

PRÓXIMOS PASSOS

Uma vez que os webhooks estejam à prova de falhas.

Webhooks são a parte difícil. Com os quatro padrões acima em vigor, o trabalho restante é principalmente operacional: um ambiente de teste que exercita os caminhos de falha, controles de gastos para que um agente upstream não sobrecarregue seu manipulador, e uma revisão final de segurança.

Referência completa em docs.blockchain0x.com. Glossário de webhook: mandato de pagamento. Superfície de produto: API de Pagamento.

Última revisão: 2026-05-15. Publicado sob CC BY 4.0.

Webhooks em que você pode confiar sob carga.

Assinado, refeito, idempotente. Grátis para começar.