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.
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 osecret(prefixowhsec_). 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
urlque 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:
| Header | Valor | Para que serve |
|---|---|---|
Content-Type | application/json | O corpo é sempre JSON. |
User-Agent | pepino-zap-webhooks/1 | Identifica o agente de entrega. Útil para filtrar/logar tráfego do PEPINO ZAP. |
X-Pepino-Event | nome do evento, ex. message.received | Permite rotear o tratamento sem precisar dar parse no corpo. É o mesmo valor do campo event do envelope. |
X-Pepino-Webhook-Id | identificador do webhook, ex. wh_abc123 | Identifica qual webhook cadastrado originou a entrega. Ver o gotcha de idempotência abaixo. |
X-Pepino-Signature | sha256=<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(200–299). A entrega é considerada concluída, o campolastSuccessAtdo webhook é atualizado e não há reenvio. - Falha: qualquer outra coisa — status
4xxou5xx, timeout, ou erro de rede (DNS, conexão recusada, TLS, etc.). A tentativa é marcada como falha (registralastFailureAtelastFailureReason) 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 OKResponda 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 tentativa | Próxima tentativa | Atraso aproximado |
|---|---|---|
| 1ª (entrega inicial) | 2ª | ~1 s |
| 2ª | 3ª | ~16 s |
| 3ª | 4ª | ~1 min 21 s |
| 4ª | 5ª | ~4 min 16 s |
| 5ª | 6ª | ~10 min 25 s |
| 6ª | — | 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).nullse ainda não houve nenhuma.lastFailureAt— instante da última tentativa que falhou.nullse nunca falhou.lastFailureReason— motivo legível da última falha, por exemploHTTP 500,HTTP 404,erro de rede: ...ouURL 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):
| Campo | Significado |
|---|---|
event | Tipo do evento entregue (ex.: message.received). |
attempt | Número da tentativa (1 = primeira; 2+ = reenvio do retry). |
statusCode | Status HTTP que o seu endpoint respondeu; null em falha de rede/timeout (sem resposta). |
outcome | success (2xx) ou failed. |
errorReason | Motivo legível quando failed (ex.: HTTP 500, erro de rede: ...); null no sucesso. |
durationMs | Quanto a requisição demorou, em milissegundos. |
createdAt | Quando 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:nameausente, JSON malformado,urlque não é uma URLhttp(s)absoluta (ou aponta para rede interna — bloqueio anti-SSRF), ou um valor deeventsque não é um evento suportado.401— API key ausente ou inválida, ou que não casa com o host (pzk_live_emapi.pepinozap.com.br;pzk_test_emsandbox.pepinozap.com.br).403— projeto inativo ou sem permissão.404— (emDELETE /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-Signatureem Node, Python e Go. - Eventos — o envelope e o
datade cada um dos dez eventos. - Webhooks — cadastrar, listar e remover webhooks via API.
- WebSocket — receber os mesmos eventos sem expor um endpoint público.
Assinatura HMAC
Como validar que a entrega de webhook veio mesmo do PEPINO ZAP, recalculando o HMAC-SHA256 do corpo bruto com o secret whsec_ e comparando com o header X-Pepino-Signature.
WebSocket (tempo real)
Receba os eventos do PEPINO ZAP por uma conexão WebSocket, sem precisar expor um endpoint público — endpoint wss, autenticação por ?token=, envelope dos eventos, ciclo de vida e tratamento de erros.