Webhooks para quem está começando: cuidados essenciais para não perder a confiabilidade da aplicação
Quando começamos a trabalhar com webhooks, o fluxo parece simples:
um sistema envia um evento → nossa aplicação recebe → processamos → atualizamos o banco.
Na prática, sistemas distribuídos são menos previsíveis.
Um webhook pode chegar mais de uma vez, atrasado, fora de ordem ou ser processado simultaneamente por diferentes workers.
Isso acontece em integrações com WhatsApp, gateways de pagamento, e-commerce, logística, e-mail, ERPs e praticamente qualquer sistema baseado em eventos assíncronos.
Por isso, receber um evento não significa necessariamente que ele deve alterar o estado atual da aplicação.
Eventos podem chegar fora de ordem
Imagine os status de uma mensagem enviada pelo WhatsApp:
sent
↓
delivered
↓
read
O fluxo esperado é simples: a mensagem foi enviada, chegou ao dispositivo do destinatário e foi lida.
Mas os webhooks podem ser recebidos nesta ordem:
sent
read
delivered
Se a aplicação atualizar o campo status sempre que receber um webhook, teremos o seguinte resultado:
status = read
Depois, quando o evento atrasado chegar:
status = delivered
Agora, uma mensagem que já foi lida aparece apenas como entregue.
O estado regrediu.
Isso pode acontecer porque um evento ficou preso em uma fila, porque o provedor realizou uma nova tentativa de entrega ou porque diferentes workers processaram eventos simultaneamente.
A menos que o contrato do provedor ofereça garantias específicas, não devemos presumir que os eventos serão:
- entregues uma única vez;
- recebidos na ordem em que aconteceram;
- processados por apenas um worker;
- processados imediatamente;
- processados sem concorrência.
Uma integração confiável precisa continuar correta mesmo quando essas condições não são ideais.
Três proteções fundamentais
Uma integração robusta normalmente combina pelo menos três mecanismos: idempotência, controle das transições de estado e atualizações atômicas.
1. Idempotência
Idempotência significa que processar novamente a mesma operação produz o mesmo resultado observável, sem duplicar seus efeitos.
Imagine que recebemos:
event_id = abc123
status = delivered
Por não receber uma resposta a tempo, o provedor envia o mesmo evento novamente.
Sem proteção, a aplicação poderia:
- executar automações novamente;
- duplicar métricas;
- enviar notificações repetidas;
- gerar cobranças;
- registrar efeitos colaterais indesejados.
Uma estratégia comum é armazenar o identificador externo do evento em uma tabela com índice UNIQUE.
Ao receber novamente o evento abc123, a aplicação reconhece que ele já foi tratado e não repete seus efeitos.
Entretanto, apenas verificar o event_id na aplicação não é suficiente. Dois workers poderiam fazer a consulta ao mesmo tempo e concluir que o evento ainda não existe.
A proteção precisa estar no banco.
Também é importante que o registro da idempotência e a alteração do estado sejam consistentes. Se a aplicação marcar o evento como processado e falhar antes de executar a atualização principal, esse evento poderá nunca mais ser tratado.
Quando possível, essas operações devem acontecer na mesma transação. Em arquiteturas mais complexas, padrões como transactional inbox e outbox também ajudam a resolver esse problema.
2. Controle das transições de estado
Agora imagine dois eventos legítimos e diferentes:
event_id = 100
status = read
e:
event_id = 101
status = delivered
A idempotência não deve bloquear nenhum deles, pois possuem identificadores diferentes.
O problema é que o segundo evento representa um estado anterior.
Em fluxos lineares, podemos usar transições monotônicas: o estado pode avançar, mas não regressar.
Por exemplo:
sent = 1
delivered = 2
read = 3
Se o estado atual for read, um evento delivered não deve sobrescrevê-lo.
Assim:
sent → delivered
delivered → read
são transições permitidas.
Enquanto:
read → delivered
delivered → sent
devem ser ignoradas.
Essa abordagem funciona bem quando existe uma progressão claramente linear.
Nem todo domínio, porém, pode ser representado apenas com uma sequência numérica.
Pagamentos, pedidos e entregas podem possuir estados como:
failed
cancelled
refunded
chargeback
returned
Nesses casos, é mais seguro modelar uma máquina de estados, definindo explicitamente quais transições são permitidas.
Um pagamento paid, por exemplo, não deveria voltar para pending por causa de um webhook atrasado. Mas ele poderia avançar legitimamente para refunded ou chargeback.
Portanto, a regra correta depende do domínio:
- fluxos lineares podem usar ranks monotônicos;
- fluxos ramificados precisam de transições explicitamente permitidas;
- alguns eventos podem exigir timestamps, versões ou sequências fornecidas pelo provedor.
3. Atualizações atômicas
Ainda existe o problema da concorrência.
Imagine dois workers processando simultaneamente:
Worker A → delivered
Worker B → read
Uma implementação ingênua poderia fazer:
SELECT status
Depois comparar o resultado na aplicação e executar:
UPDATE messages
Os dois workers podem ler o mesmo estado antes que qualquer atualização seja concluída. Isso cria uma condição de corrida.
Por isso, a validação da transição deve participar da própria atualização no banco.
Se a tabela armazenar tanto o status quanto seu rank, podemos fazer conceitualmente:
UPDATE messages
SET
status = 'read',
status_rank = 3
WHERE id = ?
AND status_rank < 3;
O banco atualizará o registro somente se o novo estado representar um avanço.
A quantidade de linhas alteradas também informa se a transição foi aplicada ou descartada.
Isso é mais seguro do que depender apenas de uma comparação na aplicação:
if ($newStatus->rank() > $currentStatus->rank()) {
// atualizar
}
Entre o if e o UPDATE, outro processo poderia modificar o registro.
Dependendo do fluxo, também podemos utilizar:
- transações;
- locks;
- updates condicionais;
- índices únicos;
- constraints;
- controle otimista por versão.
A aplicação expressa a regra de negócio, enquanto o banco ajuda a garantir sua consistência.
Um exemplo em Laravel
Podemos começar representando a ordem dos estados:
enum MessageStatus: string
{
case Sent = 'sent';
case Delivered = 'delivered';
case Read = 'read';
public function rank(): int
{
return match ($this) {
self::Sent => 1,
self::Delivered => 2,
self::Read => 3,
};
}
}
Depois, fazemos uma atualização condicional:
$newStatus = MessageStatus::from($payload['status']);
$updated = Message::query()
->whereKey($messageId)
->where('status_rank', '<', $newStatus->rank())
->update([
'status' => $newStatus->value,
'status_rank' => $newStatus->rank(),
]);
Se $updated for igual a zero, o registro já estava no mesmo estado ou em um estado mais avançado.
A comparação acontece dentro do UPDATE, reduzindo o risco de concorrência entre workers.
Ainda será necessário combinar essa proteção com idempotência, transações e regras adequadas ao domínio.
Idempotência, ordenação e atomicidade resolvem problemas diferentes
Esse ponto merece atenção.
Considere:
Evento A
id = 10
status = delivered
e:
Evento B
id = 11
status = read
São eventos diferentes.
Se chegarem na ordem esperada, tudo funciona:
delivered
read
Mas eles também podem chegar assim:
read
delivered
A idempotência não deve eliminar o segundo evento, pois seu identificador é diferente.
É a regra de transição que deve impedir:
read → delivered
Em resumo:
- Idempotência protege contra efeitos duplicados.
- Regras de transição protegem contra eventos fora de ordem.
- Atomicidade protege contra condições de corrida.
- Transações ajudam a manter consistentes o registro do evento e seus efeitos.
São problemas relacionados, mas diferentes.
Responder rápido não significa responder antes de salvar
Endpoints de webhook devem responder rapidamente para evitar que o provedor considere a entrega como falha e faça novas tentativas.
Mas existe um detalhe importante: a aplicação não deveria retornar sucesso antes de armazenar o evento de forma durável.
Um fluxo mais seguro é:
receber
→ validar assinatura
→ validar estrutura básica
→ persistir em fila ou tabela confiável
→ responder ao provedor
→ processar assincronamente
Se a aplicação responder 200 OK e falhar antes de salvar o evento, o provedor poderá considerá-lo entregue e não enviá-lo novamente.
O processamento assíncrono também precisa ser seguro caso o job seja executado mais de uma vez.
O que pode acontecer sem essas proteções?
Em uma plataforma de atendimento, por exemplo, podem surgir:
- mensagens lidas aparecendo apenas como entregues;
- métricas incorretas;
- dashboards inconsistentes;
- automações executadas com estados antigos;
- notificações duplicadas;
- contadores divergentes;
- cobranças repetidas;
- comportamentos diferentes conforme a ordem de processamento;
- bugs difíceis de reproduzir.
Talvez o pior seja que o sistema pode funcionar durante semanas sem nenhum problema aparente.
Até que uma combinação específica de latência, retries, filas e concorrência revele o erro.
Esses bugs dependem de timing. Por isso, costumam ser difíceis de diagnosticar em ambiente local.
Uma mudança de mentalidade
Ao desenvolver integrações por webhook, precisamos deixar de pensar apenas:
Recebi um evento, então vou atualizar o registro.
E começar a perguntar:
A origem deste evento é autêntica?
O payload é válido?
Este evento já produziu seus efeitos?
Ele representa uma transição permitida?
Ainda faz sentido diante do estado atual?
A atualização está protegida contra concorrência?
Essa mudança separa uma integração que funciona apenas em condições ideais de uma integração preparada para ambientes reais.
Checklist para webhooks em produção
Antes de considerar uma integração pronta, vale verificar:
- A assinatura ou autenticidade do webhook é validada?
- Existe um identificador único para cada evento?
- Esse identificador possui proteção
UNIQUEno banco? - O processamento é idempotente?
- Eventos fora de ordem podem regredir o estado?
- As transições permitidas estão explícitas?
- Atualizações críticas são atômicas?
- O evento é persistido antes da resposta de sucesso?
- Processamentos demorados são enviados para filas?
- Os retries utilizam backoff e, quando necessário, jitter?
- Existe tratamento para falhas permanentes ou uma dead-letter queue?
- Jobs são seguros caso sejam executados novamente?
- Logs evitam expor dados sensíveis?
- Existem métricas e alertas para falhas e eventos descartados?
- Existem testes para duplicidade, desordem e concorrência?
Conclusão
Webhooks parecem simples quando olhamos apenas para o HTTP:
POST → receber JSON → atualizar banco
Mas uma integração realmente confiável exige pensar em sistemas distribuídos.
Eventos podem ser duplicados, atrasados, recebidos fora de ordem ou processados simultaneamente.
Por isso, alguns conceitos devem estar entre os primeiros aprendizados de quem trabalha seriamente com webhooks:
- Idempotência: impede efeitos duplicados.
- Regras de transição: evitam alterações inválidas ou regressões de estado.
- Atualizações atômicas: protegem contra concorrência.
- Persistência durável: evita confirmar eventos que ainda podem ser perdidos.
Não é excesso de engenharia.


