Os webhooks fora de ordem em PHP são um problema de consistência, não apenas de conectividade. Um provedor pode reenviar uma entrega porque não recebeu uma resposta válida, uma fila pode atrasar uma mensagem ou dois eventos da mesma entidade podem percorrer rotas diferentes. Se a aplicação pressupõe que cada evento chega uma vez e em sequência, uma confirmação antiga pode sobrescrever um cancelamento posterior, ou uma repetição pode executar duas vezes uma operação irreversível.
A regra inicial é simples: um webhook é uma notificação de que algo pode ter mudado em outro sistema. Não é, por si só, uma instrução confiável para alterar o estado local sem verificações. O design deve preservar a evidência recebida, decidir quais eventos são admissíveis e aplicar alterações de forma idempotente e ordenada de acordo com as regras do domínio.
Separar recebimento, validação e aplicação ao domínio

O endpoint HTTP deve fazer pouco e fazê-lo de forma previsível. Sua responsabilidade é receber a solicitação, verificá-la, persistir um registro imutável e responder dentro do prazo esperado pelo remetente. O trabalho que modifica pedidos, assinaturas, estoque ou qualquer outra entidade de negócio deve ocorrer depois, normalmente por meio de um processo assíncrono.
Separar as fases evita que uma falha transitória de uma API interna transforme uma entrega válida em uma nova tentativa ambígua. Também permite retomar o processamento sem pedir ao provedor que reenvie eventos antigos.
- Recebimento: capturar cabeçalhos, corpo sem transformação, instante de recebimento e origem identificada.
- Validação de entrada: verificar assinatura, formato, tamanho, tipo de conteúdo e campos mínimos.
- Persistência: armazenar o evento e seu estado inicial em uma transação curta.
- Enfileiramento: sinalizar que há trabalho pendente, sem depender de processá-lo dentro da resposta HTTP.
- Aplicação: um worker interpreta o evento, obtém o estado necessário e executa uma transição de negócio controlada.
É importante distinguir uma entrega de um evento. Uma mesma entrega pode se repetir, e alguns provedores atribuem um identificador diferente a cada tentativa de entrega. Se houver um identificador de evento estável, ele costuma ser a melhor base para a deduplicação. Se não houver, será necessário definir uma chave com origem, entidade externa, tipo e uma versão ou timestamp com significado conhecido.
O que registrar para poder auditar e reprocessar
Uma tabela de eventos não deve guardar apenas o JSON interpretado. Preserve o corpo original, pois normalizá-lo antes de armazená-lo pode eliminar informações necessárias para verificar uma assinatura, investigar um incidente ou adaptar um parser posterior.
No mínimo, o registro deve conter:
- Origem ou provedor e ambiente de integração.
- Identificador externo do evento e, se houver, identificador de entrega.
- Tipo de evento, identificador de entidade externa e versão, sequência ou data efetiva.
- Cabeçalhos relevantes e payload original protegido contra modificações.
- Instante de recebimento local e, separadamente, o timestamp declarado pelo remetente.
- Hash criptográfico do payload para diagnóstico e deduplicação auxiliar.
- Estado de processamento: recebido, validado, pendente, aplicado, ignorado, falho ou em revisão.
- Número de tentativas, erro resumido, instante da última tentativa e referência à entidade local afetada.
Uma restrição única sobre (origem, external_event_id) resolve a repetição quando o provedor oferece um ID estável. Insira primeiro e trate o conflito como uma entrega já conhecida, não como um erro de negócio. A resposta pode continuar sendo bem-sucedida para interromper novas tentativas.
Porém, deduplicar a mensagem não basta para garantir idempotência. Por exemplo, dois eventos diferentes podem expressar a mesma confirmação e ambos tentar criar um lançamento contábil. A operação de negócio deve ter sua própria proteção: uma chave de idempotência, uma restrição única sobre o efeito ou uma transição que verifique se o resultado já existe.
Validar autenticidade e limitar a superfície de entrada
Não aceite um webhook porque ele vem de um endereço IP esperado ou porque inclui um campo que parece secreto. Quando o provedor permitir, valide uma assinatura calculada sobre o corpo bruto e um timestamp. A comparação deve ser em tempo constante, e a janela temporal deve limitar ataques de replay, considerando uma defasagem razoável entre relógios.
Antes de persistir, imponha limites operacionais: tamanho máximo do corpo, tempo de leitura, formatos aceitos e esquema mínimo. Um JSON válido não é necessariamente um evento válido. Rejeite tipos desconhecidos se não houver uma política explícita para arquivá-los sem aplicar efeitos.
Os segredos de assinatura exigem rotação. Durante uma alteração, pode ser necessário aceitar uma chave anterior e outra nova durante um período delimitado, registrando qual delas validou a entrega. Não inclua corpos completos, tokens nem dados pessoais desnecessários nos logs da aplicação. O registro de auditoria deve ter controles de acesso e uma política de retenção compatível com a sensibilidade dos dados.
Decidir a ordem lógica, não confiar na ordem da rede
O horário de recebimento não define o que ocorreu primeiro. Tampouco uma data incluída no payload é sempre suficiente: ela pode ser aproximada, corresponder à criação do evento e não à transição, ou ser afetada por relógios não sincronizados. O melhor sinal é uma versão monotônica ou um número de sequência por entidade fornecido pelo sistema de origem.
Quando houver uma versão, guarde a última versão aplicada na entidade local. Um worker pode aplicar um evento apenas se sua versão for superior à armazenada; uma versão igual indica repetição, e uma inferior é um evento tardio. Se houver lacunas na sequência, não invente o estado intermediário: marque a entidade para reconciliação ou consulte a API de origem, se essa API for o sistema de registro de referência.
Se não houver sequência nem versão, as regras devem ser de domínio. Uma máquina de estados explícita é mais segura do que atribuir diretamente um texto recebido. Por exemplo, uma entidade cancelada poderia impedir o retorno ao estado confirmada, salvo uma transição documentada e autorizada. O modelo deve definir o que fazer com cada combinação de estado atual e evento de entrada.
if ($eventVersion <= $entity->lastExternalVersion) {
markIgnored($event, 'version_no_mas_reciente');
return;
}
applyAllowedTransition($entity, $event);
$entity->lastExternalVersion = $eventVersion;O código ilustra o critério, não substitui a transação nem as regras de transição. Para eventos sem versão, uma comparação de datas só é aceitável se o contrato do remetente garantir sua semântica e precisão.
Tratar eventos tardios conforme o custo de errar
Nem todos os eventos atrasados merecem a mesma resposta. Escolher entre ignorar, registrar, recalcular ou compensar depende de o evento poder alterar uma obrigação real e de qual é a fonte da verdade.
- Ignorar: apropriado para uma versão antiga cujo efeito já está incluído em um estado posterior verificável.
- Registrar e alertar: útil se a sequência for incoerente ou faltarem informações para decidir sem intervenção.
- Recalcular: consultar o estado atual no sistema externo e atualizar o espelho local quando a fonte externa prevalecer.
- Compensar: criar uma ação corretiva rastreável quando um efeito anterior já produziu consequências e não puder ser removido com segurança.
Considere um caso hipotético de uma operação externa. Chega uma confirmação com versão 12, depois um cancelamento com versão 13 e, mais tarde, a confirmação 12 é reenviada. Com controle de versão, a repetição não reativa a operação. Se o cancelamento chegar primeiro e o sistema souber que falta a versão 12, poderá aplicar o cancelamento se a máquina de estados permitir ou solicitar uma reconciliação antes de produzir um efeito sensível.
Concorrência interna, filas e bloqueios por entidade
O processamento assíncrono melhora a capacidade de resposta, mas introduz condições de corrida internas: dois workers podem ler o mesmo estado antes que um deles escreva. A deduplicação do evento não evita essa condição.
Para entidades sensíveis, serialize por chave de entidade externa ou local. Isso pode ser obtido com partições de fila baseadas nessa chave, um bloqueio distribuído com expiração cuidadosamente projetada ou um bloqueio de linha dentro de uma transação breve. Outra opção é o controle otimista: atualizar apenas se a versão armazenada continuar sendo a esperada e tentar novamente ao detectar conflito.
Evite manter uma transação aberta enquanto chama serviços remotos. Primeiro reserve ou leia o estado de forma consistente; depois faça a chamada com uma chave idempotente quando possível; por fim, registre o resultado. Se o processo falhar entre as etapas, uma nova tentativa deverá conseguir distinguir uma operação pendente de uma já concluída.
Operação, observabilidade e testes antes de publicar

Um painel operacional deve mostrar quantos eventos permanecem pendentes, falham repetidamente, são ignorados por antiguidade, são rejeitados por assinatura e apresentam lacunas de sequência. Meça também a idade da fila e o tempo entre o recebimento e a aplicação. Esses sinais permitem detectar uma integração degradada antes que a defasagem se transforme em um problema de negócio.
Preserve mecanismos de reprocessamento que partam do evento original e de uma versão explícita do parser ou handler. Reprocessar não significa executar às cegas: limite o escopo, registre quem o solicitou e mantenha ativas as mesmas garantias de idempotência.
Lista de verificação
- Enviar o mesmo evento várias vezes, inclusive de forma concorrente.
- Entregar um cancelamento antes da confirmação relacionada.
- Atrasar um evento antigo até depois de outro com versão superior.
- Introduzir lacunas, tipos desconhecidos, payloads truncados e assinaturas inválidas.
- Simular a falha do worker após criar um efeito externo e antes de marcar o evento como aplicado.
- Verificar que dois workers na mesma entidade não produzem uma transição impossível.
- Verificar que o reprocessamento preserva a auditoria e não duplica efeitos.
A integração robusta não tenta forçar a rede a entregar em ordem. Ela projeta uma fronteira confiável: armazena cada entrada verificável, aplica regras de negócio idempotentes, usa uma ordem lógica quando ela existe e reconcilia quando não pode conhecer o estado com segurança.



