Quando a API do parceiro mente: validação defensiva de payload em integração externa
Existe uma frase que todo dev de backend deveria tatuar no antebraço: “a API do parceiro vai te mandar lixo, e vai ser num dia de sexta-feira.”
Não é maldade do outro lado. É a vida. Sistemas externos têm bug, fazem deploy no pior horário, mudam contrato sem avisar e, de vez em quando, mandam um pedido com o valor total errado bem na hora do almoço — justo quando o restaurante está lotado e ninguém tem tempo de investigar.
Esse artigo nasceu de um caso real e dolorosamente comum: pedidos chegando de uma integração de delivery com valores divergentes do esperado. O total não batia com a soma dos itens. E quando isso acontece, a pergunta que importa não é “de quem é a culpa?” e sim, “por que meu sistema aceitou isso?”.
A resposta, quase sempre, é a mesma: a gente confiou no payload. E confiar cegamente em dado externo é o caminho mais curto pra um incidente em produção.
Contexto: a API externa não é sua amiga
Quando você consome dado que você mesmo gerou, dá pra relaxar um pouco — você conhece as regras, controla o formato, sabe o que esperar. Já quando o dado vem de fora, todas as garantias evaporam.
O parceiro pode mandar:
- um campo que mudou de nome no último deploy dele;
- um total que não corresponde à soma dos itens;
- um
nullonde você jurava que sempre viria número; - o mesmo pedido duas vezes (oi, retry mal configurado);
- um valor zerado porque um cupom bugou no lado deles.
Nada disso é culpa sua. Mas tudo isso vira problema seu no segundo em que você persiste no banco. O dado externo é como comida de procedência duvidosa: pode até estar ótima, mas você cheira antes de comer.
O problema real: confiar no total que veio pronto
O padrão ingênuo (que todos nós já escrevemos, sem vergonha de admitir) é mais ou menos assim:
public function receberPedido(array $payload): Pedido
{
$pedido = new Pedido();
$pedido->externalId = $payload['order_id'];
$pedido->total = $payload['total']; // confiança cega 🙈
$pedido->itens = $payload['items'];
$this->repository->save($pedido); // e reza
return $pedido;
}
Funciona lindamente na demo. Aí chega o dia em que total vem como R$ 0,00, ou não bate com a soma dos itens, e você descobre o problema pela pior fonte possível: o cliente reclamando que pagou errado. O $payload['total'] que você aceitou de olhos fechados virou prejuízo.
A solução: desconfiar com método
A boa notícia é que blindar isso não exige nenhuma mágica — só disciplina em quatro frentes.
1. Guard clauses no boundary
O ponto onde o dado externo entra no seu sistema é uma fronteira. Trate como alfândega: nada passa sem revista. Antes de qualquer coisa, valide a estrutura.
final class PayloadPedidoValidator
{
public function validar(array $payload): void
{
foreach (['order_id', 'total', 'items'] as $campo) {
if (!array_key_exists($campo, $payload)) {
throw new PayloadInvalidoException("Campo obrigatório ausente: {$campo}");
}
}
if (!is_array($payload['items']) || $payload['items'] === []) {
throw new PayloadInvalidoException('Pedido sem itens');
}
}
}
Parece óbvio? É. Mas “óbvio” e “implementado” são dois estados bem diferentes na vida de um sistema legado.
2. Validar invariantes (as regras que NUNCA podem ser violadas)
Aqui mora o ouro. Estrutura certa não significa dado correto. O caso dos valores divergentes é exatamente isso: o JSON estava perfeitamente formado, mas a regra de negócio estava quebrada. A soma dos itens tem que bater com o total. O total não pode ser zero. São invariantes — verdades que a sua aplicação se recusa a violar.
public function validarInvariantes(array $payload): void
{
$somaItens = array_sum(
array_map(
fn(array $item) => $item['price'] * $item['quantity'],
$payload['items']
)
);
// Comparar dinheiro com tolerância de centavo (arredondamento existe)
if (abs($somaItens - $payload['total']) > 0.01) {
throw new InvarianteVioladaException(
"Total divergente: payload={$payload['total']}, calculado={$somaItens}"
);
}
if ($payload['total'] <= 0) {
throw new InvarianteVioladaException('Total deve ser maior que zero');
}
}
Dica de quem já se queimou: trabalhe com dinheiro em centavos inteiros sempre que puder. Comparar float de dinheiro com == é um esporte radical que ninguém deveria praticar em produção.
3. Idempotência: porque o parceiro VAI mandar o mesmo pedido duas vezes
Retry é uma boa prática — do lado de quem chama. Do seu lado, significa que o mesmo pedido pode bater na sua porta múltiplas vezes. Sem proteção, você duplica venda, duplica cobrança, duplica dor de cabeça.
A solução é a velha e boa idempotência por chave natural (o order_id do parceiro). É o mesmo princípio que o Stripe usa com Idempotency-Key: a primeira vez processa, as próximas devolvem o resultado já existente.
public function receberPedido(array $payload): Pedido
{
$existente = $this->repository->buscarPorExternalId($payload['order_id']);
if ($existente !== null) {
// Já vimos esse pedido. Devolve o que já temos, sem reprocessar.
return $existente;
}
$this->validator->validar($payload);
$this->validator->validarInvariantes($payload);
// ... persiste com unique constraint em external_id no banco
}
E um detalhe que separa o código que funciona na demo do código que aguenta produção: coloque uma unique constraint em external_id no banco. Race condition entre dois requests simultâneos do mesmo pedido é real, e o banco é o seu último juiz — quem chegar em segundo toma o erro de constraint, e você trata com elegância em vez de duplicar.
4. Log do payload bruto: seu seguro contra “mas eu não mandei isso”
Quando der ruim — e vai dar —, você precisa do payload exatamente como chegou, antes de qualquer transformação. É o que transforma uma discussão de “a culpa é sua / não, é sua” numa conversa baseada em evidência. Além de permitir reprocessar (replay) o pedido depois que o bug for corrigido.
$this->logger->info('payload.pedido.recebido', [
'external_id' => $payload['order_id'] ?? null,
'parceiro' => 'delivery-x',
'raw' => json_encode($payload), // o crú, sem maquiagem
'recebido_em' => now()->toIso8601String(),
]);
Junte as quatro peças e o resultado é uma camada de integração que falha cedo, falha alto e falha com contexto — em vez de engolir lixo silenciosamente e explodir três dias depois num relatório financeiro.
Vale o aviso pra não cair no extremo oposto: validação defensiva não é desconfiar de tudo a ponto de rejeitar pedido legítimo. É proteger as invariantes que, se violadas, causam prejuízo real. Total que não bate, valor zerado, pedido duplicado — esses doem no caixa. Um campo opcional ausente, nem sempre. Saber a diferença é o que separa robustez de paranoia.
E tem o ganho silencioso: quando o pedido divergente é barrado no boundary, com log do payload bruto e exceção clara, o seu time descobre o problema do parceiro antes do cliente. Isso muda completamente a conversa com quem está do outro lado da integração.
A regra de ouro de integração com terceiro cabe em uma linha: confie, mas verifique — e logue tudo. A API do parceiro não é inimiga, mas também não é sua amiga; é uma fonte de dados que você não controla, e isso já é motivo suficiente pra tratar cada payload como suspeito até prova em contrário.
As quatro frentes — guard clauses, invariantes, idempotência e log do cru — não são arquitetura sofisticada. São higiene básica de backend que integra com o mundo real. O caso dos valores divergentes só virou incidente porque faltou a segunda: ninguém perguntou ao payload se a soma batia com o total.
Da próxima vez que você for plugar uma API externa, lembre da tatuagem imaginária no antebraço. O parceiro vai mandar lixo. A única pergunta que você controla é se o seu sistema vai aceitar.


