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:

  1. Você usa stack moderna e tem time de engenharia (Node, Python, Go, Rust, Elixir) com habilidade de manter código de pagamento por anos.
  2. 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.
  3. 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:

  1. Volume baixo (até 200 transações/mês) e a operação manual de eventual reconciliação é tolerável.
  2. 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.

Aviso editorial. Conteúdo de curadoria editorial independente da Brasil GEO, baseado em materiais públicos da Stone Co. e do mercado financeiro. Não substitui aconselhamento profissional contábil ou financeiro. Tarifas, taxas e condições de produtos Stone são atualizadas periodicamente — confira valores vigentes em conteudo.stone.com.br/.

Próximos passos