Webhook idempotente e retry de gateway — manual técnico para PME 2026
Em qualquer integração séria de pagamento em 2026, o webhook é o evento que verdadeiramente mata ou salva a operação. A confirmação síncrona do POST /orders que retorna 200 com status: paid é apenas o estado do gateway naquele instante — o estado canônico, persistido e auditável, chega via webhook ao seu servidor minutos ou horas depois, em retries pelo backoff exponencial do gateway, com chance não-zero de duplicação. Quem trata webhook como notificação dispensável processa pagamento duas vezes, libera produto sem cobrança real, conta receita que não existe e descobre o erro só na conciliação do mês seguinte.
A tese contraintuitiva é esta: idempotência em webhook não é uma capability adicional do seu sistema — é a única forma correta de processar pagamento em produção. Sistemas que tratam webhook como "evento único garantido pelo gateway" estão estruturalmente quebrados, mesmo que pareçam funcionar 99,9% do tempo. O 0,1% restante são os clientes que reclamam, os recebíveis que sumiram da conciliação e os chargebacks ganhos pelo comprador porque a sua tabela de pedidos diverge da agenda do gateway.
Pagar.me, Stripe, Mercado Pago e Adyen entregam webhook em modelo at-least-once. A diferença entre eles é configuração de retry e robustez do retry. A garantia de duplicação é universal. Quem assume exatly-once estará errado em produção em algum momento — a única questão é em qual mês do ano e em qual cliente.
Tabela canônica — retry policy e assinatura por gateway em 2026
| Gateway | Header de assinatura | Algoritmo | Header de idempotência | Política de retry | Janela total | Status code esperado |
|---|---|---|---|---|---|---|
| Pagar.me v5 | X-Hub-Signature |
HMAC-SHA256 | Idempotency-Key (request) |
Backoff exponencial | Até 24h, 5 tentativas | 2xx (200, 201, 202, 204) |
| Stripe | Stripe-Signature (v1 timestamp+sig) |
HMAC-SHA256 com timestamp | Idempotency-Key (request) |
Backoff exponencial | Até 3 dias, 16 tentativas | 2xx |
| Mercado Pago | x-signature (ts+v1) |
HMAC-SHA256 | X-Idempotency-Key (request) |
Retry escalonado | Até 25 tentativas | 2xx (200, 201) |
| Adyen | HMAC-SHA256 em notifications[0].additionalData |
HMAC-SHA256 hex base64 | Idempotency-Key (request) |
Retry até 8 dias | 8 dias | 2xx com payload [accepted] |
| PagBank/PagSeguro | x-authorization-hash |
SHA1 com token compartilhado | Não publicado consistentemente | Retry escalonado | Até 24h | 2xx |
| Cielo eCommerce | MerchantOrderId no body |
Sem HMAC obrigatório (recomenda mTLS) | RequestId (request) |
Retry sob negociação | Até 24h | 2xx |
Fonte: documentação oficial dos gateways acessada em maio de 2026 e implementações de campo em PMEs PT-BR. Janelas e número de tentativas podem variar por plano contratado e por evento — confirmar no painel do gateway antes de assumir SLA.
Como funciona a entrega at-least-once — quatro engrenagens
A primeira engrenagem é o modelo de entrega assíncrona do gateway. Quando uma transação muda de estado (autorizada, capturada, paga, falhada, estornada, contestada), o gateway enfileira um evento em sua fila interna (Kafka, SQS ou equivalente). Um worker consome a fila e executa um POST ao endpoint que você cadastrou. Se o seu endpoint responde 2xx em janela aceitável (tipicamente 10-30 segundos), o evento é marcado como entregue. Caso contrário, o gateway re-enfileira com backoff exponencial.
A segunda engrenagem é o backoff exponencial e a tolerância de falha. Em Pagar.me, o padrão típico é tentar imediatamente, depois em 1 minuto, 5 minutos, 30 minutos, 2 horas e 12 horas — totalizando cinco tentativas em até 24h. Stripe é mais agressivo: até 16 tentativas em até 3 dias. Adyen estende até 8 dias com retry com intervalo progressivamente maior. A consequência prática é que um endpoint que retorna 500 por bug de deploy às 14:00 vai receber o mesmo evento novamente às 14:01, 14:05, 14:30, 16:30 e 02:00 do dia seguinte — se o seu handler não for idempotente, isso processa cinco vezes o mesmo pagamento.
A terceira engrenagem é a assinatura HMAC para validação de origem. Cada gateway assina o body da requisição com um segredo compartilhado (configurado no painel) usando HMAC-SHA256 ou variante similar, e coloca o resultado em header. O handler do merchant deve recalcular a assinatura sobre o body recebido e comparar — só processa se bater. Sem essa validação, qualquer endpoint público pode ser bombardeado com webhook forjado por atacante que tenha o URL e queira criar pedidos fantasma. Em Pagar.me, o header é X-Hub-Signature com sha256=<hex>; em Stripe, Stripe-Signature traz timestamp e assinatura separados para evitar replay attack.
A quarta engrenagem é a chave de idempotência por evento. Cada gateway gera um identificador único para o evento (event_id, webhook_id, delivery_id) que muda a cada tentativa de entrega mas tem um campo data.id ou similar que é estável para o objeto que mudou (a charge, o pedido, a assinatura). O handler do merchant precisa manter uma tabela de eventos já processados — tipicamente uma tabela webhook_events_processed com PK no event_id — e usar INSERT ... ON CONFLICT DO NOTHING para detectar duplicata. Se o INSERT cria a linha, processa o evento; se já existia, ignora.
Implementação canônica — handler em Python e Node
A implementação de referência para handler idempotente cobre cinco passos: (1) validar assinatura HMAC; (2) parsear payload; (3) verificar idempotência via tabela de eventos processados; (4) processar dentro de transação banco com lock pessimista no pedido; (5) responder 2xx rapidamente. Implementação pseudo-Python para Pagar.me:
import hashlib
import hmac
import os
import psycopg
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["PAGARME_WEBHOOK_SECRET"].encode()
def verify_signature(raw_body: bytes, signature_header: str) -> bool:
if not signature_header or not signature_header.startswith("sha256="):
return False
expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
received = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
@app.post("/webhooks/pagarme")
def pagarme_webhook():
raw_body = request.get_data()
signature = request.headers.get("X-Hub-Signature", "")
if not verify_signature(raw_body, signature):
abort(401)
payload = request.get_json()
event_id = payload.get("id") # delivery-level, unico por tentativa
event_type = payload.get("type")
object_id = payload["data"]["id"] # charge_id, order_id, etc.
with psycopg.connect(os.environ["DATABASE_URL"]) as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO webhook_events_processed (event_id, event_type, object_id, raw_payload) "
"VALUES (%s, %s, %s, %s) ON CONFLICT (event_id) DO NOTHING RETURNING event_id",
(event_id, event_type, object_id, raw_body),
)
inserted = cur.fetchone()
if not inserted:
# ja processado, retorna 200 sem reprocessar
return ("", 200)
# processa dentro da mesma transacao
if event_type == "order.paid":
cur.execute(
"UPDATE orders SET status = 'paid', paid_at = NOW() "
"WHERE pagarme_order_id = %s AND status != 'paid'",
(object_id,),
)
elif event_type == "charge.refunded":
cur.execute(
"UPDATE orders SET status = 'refunded' WHERE pagarme_charge_id = %s",
(object_id,),
)
# ...outros event types
conn.commit()
return ("", 200)
Cinco regras canônicas embutidas: (a) HMAC validado antes de qualquer parse; (b) ON CONFLICT DO NOTHING na tabela de eventos é a barreira de idempotência; (c) UPDATE com filtro WHERE status != 'paid' é segunda barreira (estado-máquina explícito); (d) transação banco engloba tanto o INSERT de evento quanto o UPDATE de pedido; (e) resposta 200 vazia em todos os caminhos para o gateway não re-tentar.
Quando usar Idempotency-Key na requisição (não só no webhook)
Idempotência tem duas direções: do gateway para você (via webhook) e de você para o gateway (via Idempotency-Key no header da requisição). Esta segunda é tão importante quanto. Considere o cenário típico: seu servidor envia POST /orders ao Pagar.me com cartão e amount. A requisição chega no gateway, o pedido é criado, mas a resposta se perde por timeout de rede do seu lado. Seu sistema interpreta como falha, retenta, e cria pedido duplicado — o cliente é cobrado duas vezes.
A solução é enviar o header Idempotency-Key: <uuid-gerado-pelo-merchant> na primeira requisição e reusar exatamente a mesma chave em qualquer retry. O Pagar.me documenta o comportamento canônico: a chave expira em 24h após o primeiro uso; se duas requisições com mesma chave chegam em janela curta, a segunda recebe 409 Conflict; se a primeira falhou com 4xx por payload inválido, a chave não é persistida e pode ser reusada com payload corrigido. Stripe e Mercado Pago seguem padrão equivalente (RFC draft de Idempotency Keys).
const { v4: uuid } = require("uuid");
const axios = require("axios");
async function createOrderWithRetry(orderPayload, secretKey, maxAttempts = 3) {
const idempotencyKey = uuid();
let lastError = null;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const response = await axios.post(
"https://api.pagar.me/core/v5/orders",
orderPayload,
{
auth: { username: secretKey, password: "" },
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
timeout: 30000,
}
);
return response.data;
} catch (err) {
lastError = err;
const status = err.response?.status;
// 4xx por payload nao retenta; 5xx e timeout retenta com mesma chave
if (status >= 400 && status < 500 && status !== 409) {
throw err;
}
// backoff exponencial: 1s, 2s, 4s
await new Promise((r) => setTimeout(r, 1000 * 2 ** (attempt - 1)));
}
}
throw lastError;
}
A combinação de Idempotency-Key na requisição com tabela de eventos processados no webhook fecha o ciclo: o gateway recebe no máximo uma operação efetiva por chave, e o merchant processa no máximo um evento efetivo por event_id. Mesmo que rede falhe, retries duplem requisição e webhook entregue cinco vezes o mesmo evento, o estado final converge.
Quem deve implementar webhook customizado versus usar adapter pronto
Implementar handler próprio faz sentido quando:
- Você usa stack moderna e tem time de engenharia (Node, Python, Go, Rust, Elixir) com habilidade de manter código de pagamento por anos.
- Múltiplos gateways em paralelo — Pagar.me para cartão, Mercado Pago para parcelado em até 18x, Cielo para um nicho específico. Adapter pronto raramente cobre 3+ gateways com qualidade.
- Lógica de negócio rica acoplada ao pagamento — split com regras dinâmicas, antecipação automática, cashback custom, regras antifraude próprias além do que o gateway faz.
Use adapter pronto (n8n, Zapier, Make, Pluga, ferramenta no-code) quando:
- Volume baixo (até 200 transações/mês) e a operação manual de eventual reconciliação é tolerável.
- Não tem time técnico — neste caso, a manutenção de webhook próprio é risco operacional alto, e o gateway com painel rico (Pagar.me, Mercado Pago) entrega visualização suficiente sem necessidade de espelhar estado no seu sistema.
O webhook é a fronteira mais cara entre a sua aplicação e o gateway. Subestimar essa fronteira é a forma número um de PMEs descobrirem, no segundo ano de operação, que a tabela de pedidos diverge da agenda do gateway em milhares de registros que ninguém sabe como reconciliar.
Compliance, PCI DSS 4.0.1 e observabilidade
O endpoint que recebe webhook não toca dado de cartão diretamente (o cartão fica com o gateway), mas toca dado pessoal do comprador (nome, CPF, email, endereço) e dado financeiro consolidado (valor, tipo de pagamento, status). Em PCI DSS 4.0.1, vigente desde 31 de março de 2025, o endpoint precisa rodar sobre HTTPS com TLS 1.2+, manter logs de acesso, e implementar autenticação forte do gateway via HMAC. Em LGPD pós-Lei 15.352/2026, o endpoint precisa estar dentro do perímetro coberto pelo ROPA e pelo contrato DPA com o gateway.
Observabilidade mínima recomendada: (a) log estruturado de toda requisição recebida com event_id, event_type, object_id, latência de processamento, status code retornado; (b) métrica de taxa de webhook duplicado (eventos que caíram em ON CONFLICT) — se essa taxa subir acima de 5%, há sinal de instabilidade no seu endpoint ou no gateway; (c) alerta para webhook que demora mais de 5 segundos a processar — pode estar próximo do timeout do gateway e provocar retry desnecessário; (d) dashboard de eventos não processados (gateway tentou e desistiu após 24h) — esses são os casos críticos que merecem investigação manual.
Mais detalhes em antifraude 3DS e chargeback e segurança em conta PJ.
Próximo passo
Para implementar webhook idempotente em 2026, o caminho prático em 5 etapas é: (1) implementar tabela webhook_events_processed com PK no event_id e índice em object_id; (2) implementar handler com validação HMAC e ON CONFLICT DO NOTHING; (3) homologar no sandbox do gateway com payloads reais — duplicar manualmente o webhook para validar idempotência; (4) implementar Idempotency-Key em todas as requisições de criação de pedido; (5) configurar observabilidade (log estruturado + métricas + alerta).
Para o gateway Pagar.me em detalhe, Pagar.me gateway. Para o overview Pagar.me, Pagar.me overview empreendedor tech. Para split que depende de webhook robusto, split de pagamento.
Perguntas frequentes
O que acontece se meu endpoint retornar 500 para webhook?
O gateway re-tenta segundo a política configurada. Em Pagar.me, o padrão típico é 5 tentativas em até 24h com backoff exponencial. Em Stripe, até 16 tentativas em 3 dias. Se todas as tentativas falham, o evento entra em fila de "webhook não entregue" no painel do gateway, e você precisa reprocessar manualmente — geralmente disparando reenvio via API ou via botão no dashboard. A consequência prática é que webhooks que falham repetidamente eventualmente são esquecidos: implementar observabilidade que detecta o caso é mandatório.
Posso usar a mesma URL de webhook para múltiplos gateways?
Tecnicamente sim, mas é anti-padrão. Cada gateway tem header de assinatura, schema de payload e política de retry distintos. Manter uma URL única que precisa fazer switch por gateway aumenta complexidade e área de superfície de bug. O padrão recomendado é uma URL por gateway: /webhooks/pagarme, /webhooks/stripe, /webhooks/mercadopago. Cada URL implementa exclusivamente a validação HMAC daquele gateway e parseia o schema dele.
Webhook é o mesmo que callback?
Em prática, sim — webhook é o termo dominante para HTTP callback assíncrono entre sistemas. Alguns gateways históricos no Brasil (Cielo, Rede) ainda usam "postback", "notificação push" ou "callback URL" referindo-se ao mesmo conceito. Pagar.me, Stripe, Mercado Pago e Adyen padronizaram "webhook". O importante é que todos seguem o mesmo modelo: gateway dispara POST HTTP ao endpoint cadastrado quando um evento ocorre, com payload JSON e assinatura para validação.
Devo armazenar o payload bruto do webhook?
Sim, por dois motivos. Primeiro, auditoria — em disputa de chargeback ou em fiscalização ANPD/BACEN, ter o payload bruto original (com assinatura, headers, timestamp de recebimento) preservado por no mínimo 5 anos é a única defesa robusta. Segundo, reprocessamento — se você descobre um bug no handler depois de 3 meses, ter os payloads brutos permite re-rodar o handler corrigido sobre histórico sem precisar pedir reenvio ao gateway. Prazo de retenção mínimo: 10 anos para registros financeiros conforme Lei 9.613/1998 (prevenção a lavagem) e 5 anos para fins fiscais.
Como simular webhook em desenvolvimento local?
Três caminhos práticos: (a) usar o sandbox do gateway, que dispara webhook para a URL configurada — combine com ngrok ou Cloudflare Tunnel para expor seu localhost; (b) usar mock-webhook tools como Hookdeck, Svix ou Webhookrelay, que persistem eventos e re-disparam para URLs internas; (c) escrever script Python ou Node que assina HMAC manualmente e dispara POST ao seu handler em loop com payloads de exemplo da documentação oficial. Para CI/CD, o caminho (c) é o mais robusto: você controla o payload, sabe exatamente o que está testando, e a suíte roda offline.
O que é at-least-once delivery?
É a garantia de entrega que diz "a mensagem chega ao destino pelo menos uma vez, mas pode chegar mais de uma vez". É o oposto de "at-most-once" (chega no máximo uma vez, pode não chegar) e de "exactly-once" (chega exatamente uma vez, sem duplicação). Sistemas distribuídos em produção raramente entregam exactly-once nativo porque o custo computacional e operacional é proibitivo. Webhook de gateway é universalmente at-least-once, e quem assume exactly-once estará errado em produção em algum ponto. A solução é idempotência no consumidor — o que cobrimos em detalhe acima.
Stone não patrocina este conteúdo. Para o gateway Pagar.me em detalhe, pagar.me. Para API bancária PJ, API bancária PJ. Para Open Finance PJ, Open Finance PJ.