PEPINO ZAPdocs
Webhooks

Entrega e retry

Como o PEPINO ZAP entrega cada webhook — método, headers, timeout, critério de sucesso/falha, política de retry com backoff e idempotência.

Ver como Markdown

Esta página descreve, ponta a ponta, como o PEPINO ZAP entrega cada evento ao endpoint de webhook que você cadastrou: o método e os headers da requisição, o critério que define sucesso e falha, o timeout, a política de reenvio (retry com backoff) e por que o seu endpoint precisa ser idempotente. É o que você precisa para construir um receptor de webhook robusto.

Antes de continuar, confira os pré-requisitos:

  • Você cadastrou o webhook via POST /v1/webhooks (self-service) e guardou o secret (prefixo whsec_). Ver Webhooks.
  • Para validar a assinatura de cada entrega, ver Assinatura.
  • Para a estrutura do envelope e a lista de eventos, ver Eventos.

A requisição de entrega

Cada evento é entregue como uma única requisição HTTP:

  • Método: POST.
  • URL: a url que você cadastrou no webhook.
  • Corpo: o envelope JSON { event, instanceId, timestamp, data } — os bytes crus desse corpo são o que a assinatura cobre.

Exemplo do que chega ao seu endpoint (ilustrativo — formatado para leitura; o corpo real é JSON compacto):

POST /webhook/pepinozap HTTP/1.1
Host: meusistema.com
Content-Type: application/json
User-Agent: pepino-zap-webhooks/1
X-Pepino-Event: message.received
X-Pepino-Webhook-Id: wh_abc123
X-Pepino-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

{"event":"message.received","instanceId":"inst_123","timestamp":"2026-05-30T12:00:00Z","data":{ }}

Headers de toda entrega

Estes headers acompanham todas as entregas, sem exceção:

HeaderValorPara que serve
Content-Typeapplication/jsonO corpo é sempre JSON.
User-Agentpepino-zap-webhooks/1Identifica o agente de entrega. Útil para filtrar/logar tráfego do PEPINO ZAP.
X-Pepino-Eventnome do evento, ex. message.receivedPermite rotear o tratamento sem precisar dar parse no corpo. É o mesmo valor do campo event do envelope.
X-Pepino-Webhook-Ididentificador do webhook, ex. wh_abc123Identifica qual webhook cadastrado originou a entrega. Ver o gotcha de idempotência abaixo.
X-Pepino-Signaturesha256=<hmac-hex>HMAC-SHA256 do corpo bruto, calculado com o seu secret. Use para validar autenticidade e integridade. Ver Assinatura.

O X-Pepino-Webhook-Id identifica o webhook cadastrado (a configuração), não a entrega individual. Ele é constante para todas as entregas daquele mesmo webhook — não é um número de sequência nem um ID único por POST. Para deduplicar entregas repetidas (ver Idempotência), use um identificador de negócio presente em data (por exemplo waMessageId em message.received), não este header sozinho.

Critério de sucesso e falha

O que decide o destino de cada tentativa é, exclusivamente, o status HTTP que o seu endpoint responde (ou a ausência de resposta):

  • Sucesso: o endpoint responde com status na faixa 2xx (200299). A entrega é considerada concluída, o campo lastSuccessAt do webhook é atualizado e não há reenvio.
  • Falha: qualquer outra coisa — status 4xx ou 5xx, timeout, ou erro de rede (DNS, conexão recusada, TLS, etc.). A tentativa é marcada como falha (registra lastFailureAt e lastFailureReason) e o evento entra em reenvio, conforme a política de retry.

Redirecionamentos (3xx) são seguidos automaticamente (até o limite padrão de redirects), e o que decide sucesso/falha é o status da resposta final: se a cadeia terminar em 2xx, a entrega é considerada concluída. Ainda assim, aponte o webhook diretamente para a URL final do seu endpoint, sem redirect intermediário (por exemplo, cadastre a URL https já de cara) — cada salto consome tempo do timeout de 15s e passa pela mesma checagem anti-SSRF, e um redirect que não possa ser seguido vira falha.

O corpo da sua resposta é ignorado — não precisa retornar nenhum JSON. O que importa é o código de status. Por isso, a resposta ideal é simplesmente:

HTTP/1.1 200 OK

Responda rápido, processe depois

Valide a assinatura, responda 2xx imediatamente e processe a carga de forma assíncrona (fila interna, job, etc.). Não bloqueie a resposta enquanto grava no banco, chama outra API ou faz qualquer trabalho lento — se você demorar além do timeout, a entrega vira falha e o mesmo evento será reenviado, mesmo que o seu processamento já tenha começado.

Timeout

Cada tentativa de entrega tem 15 segundos de limite. Se o seu endpoint não responder por completo (headers + status) dentro desse intervalo, a tentativa é abortada, tratada como falha e entra em reenvio.

O timeout de 15s vale para a tentativa inteira: resolução de DNS, handshake TLS, envio do corpo e recebimento da sua resposta. Em endpoints atrás de cold-start (funções serverless que "dormem"), a primeira entrega após um período ocioso pode estourar esse limite e cair em retry — mantenha o endpoint quente ou garanta que ele responda 2xx antes de qualquer trabalho pesado.

Retry e backoff

Quando uma tentativa falha, o evento é reenviado automaticamente com backoff exponencial, até o limite de 6 tentativas no total (a entrega inicial + até 5 reenvios). Esgotadas as 6 tentativas, o evento deixa de ser reenviado e a última falha fica registrada no webhook (lastFailureAt / lastFailureReason).

O intervalo entre as tentativas cresce a cada falha. A tabela abaixo mostra a janela aproximada (o atraso real recebe um jitter de ±10% para evitar picos sincronizados):

Após falhar a tentativaPróxima tentativaAtraso aproximado
1ª (entrega inicial)~1 s
~16 s
~1 min 21 s
~4 min 16 s
~10 min 25 s
sem novo reenvio (evento descartado)

Na prática, um endpoint indisponível recebe as 6 tentativas ao longo de cerca de 16 minutos antes de o evento ser descartado. Isso cobre quedas curtas (deploy, reinício, pico de carga) sem perder o evento, mas não é uma fila de retenção de longo prazo: se o seu endpoint ficar fora por mais que essa janela, os eventos daquele período não são reentregues depois.

Não há reentrega manual nem "replay" de eventos descartados. Se o seu endpoint ficar indisponível além da janela de retry, trate a lacuna pelo seu lado — por exemplo, reconciliando o estado via API (GET /v1/instances/{id} para conexão, ou consultando suas próprias mensagens). Para protótipos e telas ao vivo, o WebSocket é um transporte paralelo, mas entrega apenas enquanto o cliente está conectado (sem retry).

Quando não há retry

Há um caso em que a entrega falha e não é reenviada: quando a url cadastrada é inválida (não forma uma requisição HTTP válida). Como nenhum número de tentativas resolveria isso, a entrega é cancelada de imediato e a falha fica registrada em lastFailureReason. Cadastre uma URL http(s) absoluta e válida ao criar o webhook — a criação já valida o formato (ver os erros).

Observabilidade: acompanhe a saúde do webhook

Cada tentativa atualiza o estado do webhook, que você consulta a qualquer momento com GET /v1/webhooks:

curl https://api.pepinozap.com.br/v1/webhooks \
  -H "Authorization: Bearer $YOUR_API_KEY"

Resposta 200 OK:

{
  "items": [
    {
      "id": "wh_abc123",
      "name": "meu-endpoint",
      "url": "https://meusistema.com/webhook/pepinozap",
      "events": [],
      "enabled": true,
      "lastSuccessAt": "2026-06-05T12:00:00Z",
      "lastFailureAt": "2026-06-05T11:40:00Z",
      "lastFailureReason": "HTTP 500",
      "createdAt": "2026-05-30T12:00:00Z"
    }
  ]
}

Campos úteis para diagnóstico:

  • lastSuccessAt — instante da última entrega bem-sucedida (2xx). null se ainda não houve nenhuma.
  • lastFailureAt — instante da última tentativa que falhou. null se nunca falhou.
  • lastFailureReason — motivo legível da última falha, por exemplo HTTP 500, HTTP 404, erro de rede: ... ou URL inválida: ... (truncado em 500 caracteres). É o primeiro lugar a olhar quando "o webhook parou de chegar".

lastSuccessAt e lastFailureAt são independentes: cada um reflete o último evento do seu tipo. Um webhook saudável que teve uma falha isolada no passado continuará mostrando aquele lastFailureAt antigo — compare as duas datas para saber o que aconteceu por último.

Log de entregas (tentativa a tentativa)

Os campos last* acima são um resumo (a última entrega de cada tipo). Para o histórico completo — útil para depurar "qual evento falhou, com qual status, quantas vezes" — use GET /v1/webhooks/{id}/deliveries:

curl "https://api.pepinozap.com.br/v1/webhooks/wh_abc123/deliveries?limit=50" \
  -H "Authorization: Bearer $YOUR_API_KEY"

Resposta 200 OK:

{
  "items": [
    {
      "id": 10482,
      "event": "message.received",
      "attempt": 1,
      "statusCode": 200,
      "outcome": "success",
      "errorReason": null,
      "durationMs": 142,
      "createdAt": "2026-06-05T12:00:00Z"
    },
    {
      "id": 10480,
      "event": "message.received",
      "attempt": 2,
      "statusCode": 500,
      "outcome": "failed",
      "errorReason": "HTTP 500",
      "durationMs": 87,
      "createdAt": "2026-06-05T11:59:30Z"
    }
  ]
}

Cada item é uma tentativa (com o retry, um mesmo evento pode aparecer mais de uma vez):

CampoSignificado
eventTipo do evento entregue (ex.: message.received).
attemptNúmero da tentativa (1 = primeira; 2+ = reenvio do retry).
statusCodeStatus HTTP que o seu endpoint respondeu; null em falha de rede/timeout (sem resposta).
outcomesuccess (2xx) ou failed.
errorReasonMotivo legível quando failed (ex.: HTTP 500, erro de rede: ...); null no sucesso.
durationMsQuanto a requisição demorou, em milissegundos.
createdAtQuando a tentativa ocorreu (UTC).

Mais recente primeiro; paginado por ?limit (1–200, padrão 50) e ?offset. O log é retido por 30 dias e nunca inclui o secret nem o corpo da resposta do seu endpoint.

No painel, esse mesmo log fica no botão Entregas de cada webhook (Integrações → Webhooks). E há um inspetor de eventos ao vivo (Integrações → Eventos ao vivo) que mostra os eventos do projeto em tempo real, conforme acontecem — ótimo para conferir o que está saindo enquanto você desenvolve.

Idempotência

A política de retry implica que o mesmo evento pode ser entregue mais de uma vez: um reenvio acontece sempre que a tentativa anterior não respondeu 2xx a tempo — inclusive quando o seu endpoint processou o evento mas demorou a responder (e estourou o timeout) ou caiu logo depois. Portanto, o seu endpoint deve tratar as entregas de forma idempotente.

Não assuma que cada entrega corresponde a um evento único. Deduplique pelo identificador de negócio presente em data — por exemplo waMessageId em message.received ou waMessageIds em message.status. O header X-Pepino-Webhook-Id não serve para isso: ele é o mesmo em todas as entregas do webhook (ver headers).

Recomendações práticas:

  • Registre o que já processou. Mantenha um índice dos identificadores de negócio já tratados e ignore entregas repetidas antes de qualquer efeito.
  • Torne a escrita idempotente. Use INSERT ... ON CONFLICT DO NOTHING (ou equivalente) com chave única no identificador do evento, para que reprocessar o mesmo evento não duplique linhas nem dispare ações repetidas (um e-mail enviado duas vezes, por exemplo).
  • Valide a assinatura antes de processar. Descartar entregas com assinatura inválida evita que um terceiro injete eventos forjados. Ver Assinatura.

Esqueleto de um receptor correto

Reunindo tudo — validar a assinatura, deduplicar e responder 2xx rápido:

import crypto from 'node:crypto';

const SECRET = process.env.PEPINO_WEBHOOK_SECRET; // "whsec_..."
const processados = new Set(); // troque por um store persistente (Redis/DB)

function assinaturaOk(rawBody, header) {
  const esperado = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
  const a = Buffer.from(header || '');
  const b = Buffer.from(esperado);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Capture o RAW body (express.raw), não o JSON já parseado.
app.post('/webhook/pepinozap', express.raw({ type: 'application/json' }), (req, res) => {
  // 1) Valide a assinatura sobre os bytes crus.
  if (!assinaturaOk(req.body, req.get('X-Pepino-Signature'))) {
    return res.status(401).end();
  }

  // 2) Responda 2xx imediatamente — processe de forma assíncrona.
  res.status(200).end();

  // 3) Deduplique por identificador de negócio e processe fora do caminho da resposta.
  const evt = JSON.parse(req.body.toString('utf8'));
  const chave = evt.data?.waMessageId ?? `${evt.event}:${evt.timestamp}`;
  if (processados.has(chave)) return; // entrega repetida — ignore
  processados.add(chave);

  enfileirarParaProcessar(evt); // seu job interno
});

Erros ao cadastrar o webhook

Os erros abaixo ocorrem no momento de criar o webhook (POST /v1/webhooks) — não na entrega. O envelope de erro é { "error": { "code", "message" } }.

  • 400 — corpo inválido: name ausente, JSON malformado, url que não é uma URL http(s) absoluta (ou aponta para rede interna — bloqueio anti-SSRF), ou um valor de events que não é um evento suportado.
  • 401 — API key ausente ou inválida, ou que não casa com o host (pzk_live_ em api.pepinozap.com.br; pzk_test_ em sandbox.pepinozap.com.br).
  • 403 — projeto inativo ou sem permissão.
  • 404 — (em DELETE /v1/webhooks/{id}) webhook não encontrado neste Projeto.

As entregas seguem o modo (teste/produção) em que o webhook foi criado: um webhook criado no sandbox só recebe eventos de instâncias de teste, e um webhook de produção só recebe eventos de produção. Para receber eventos reais, cadastre o webhook com uma key pzk_live_ no host de produção.

Próximos passos

  • Assinatura — validar X-Pepino-Signature em Node, Python e Go.
  • Eventos — o envelope e o data de cada um dos dez eventos.
  • Webhooks — cadastrar, listar e remover webhooks via API.
  • WebSocket — receber os mesmos eventos sem expor um endpoint público.

On this page