Os padrões de webhook que os desenvolvedores mais perguntam.
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.
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.
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.
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)) <= 300Torne 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.
// 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)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.
// 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 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.
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.
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"])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.
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.
Teste pagamentos de agentes sem dinheiro real
Configure controles de gastos do agente que sobrevivem à injeção de prompt
Proteja sua carteira de agente antes de ir ao vivo
Referência completa em docs.blockchain0x.com. Glossário de webhook: mandato de pagamento. Superfície de produto: API de Pagamento.