Webhooks são um tap, não a fonte da verdade. A estratégia robusta é “webhook como gatilho, REST como fonte da verdade” + um job noturno que reconcilia o que o webhook possa ter perdido. Isso cobre o pior caso: a Malvo esgotou os retries ou o seu endpoint ficou fora do ar por mais de ~3 horas.
Todos os endpoints exigem o header X-API-KEY e respondem em https://api.malvo.io. Os webhooks chegam de um único IP de egresso estático que você libera no firewall.

As duas camadas

1

Camada 1 — Webhooks (tempo real)

Em cada evento item/*, re-busque o recurso canônico (nunca confie só no payload): GET /items/{itemId} e GET /accounts?itemId=.... Em transactions/created / transactions/updated, liste as contas do itemId, pagine GET /v2/transactions para cada conta e faça upsert por id da transação. Em transactions/deleted, apague por id.
2

Camada 2 — Job noturno (rede de segurança)

Liste todos os Items ativos; para cada Item com lastUpdatedAt mais velho que o esperado, chame PATCH /items/{id} para disparar um sync; e re-busque transações dos últimos N dias para pegar o que escapou durante quedas de webhook.

Camada 1 — Handler idempotente

Toda entrega carrega um eventId (UUID). O mesmo eventId é reusado em todas as re-entregas de um evento e entre todos os endpoints inscritos no mesmo evento (ex.: um webhook all e um item/error recebem o mesmo eventId para uma ocorrência). Persista o eventId e trate duplicatas como no-op antes de aplicar qualquer efeito colateral.
As entregas não são ordenadas. Nunca assuma ordem — sempre reconcilie re-buscando o recurso canônico. Responda 2XX em menos de 5 segundos e processe de forma assíncrona; qualquer outra resposta (timeout, 3xx, 4xx, 5xx, erro de conexão) conta como falha e dispara o retry.
Handler (ack-first + dedupe por eventId)

Camada 2 — Job noturno de reconciliação

Job noturno
Reconcilie por dateFrom/createdAtFrom e faça upsert por id — reprocessar os mesmos dias nunca duplica dados. Prefira GET /v2/transactions (cursor-based): pagine seguindo o next até ele vir null.

Lidando com 429 e limites mensais do Open Finance

Dois limites distintos te afetam:
Backoff com jitter + Retry-After
Disparar PATCH /items em excesso queima a sua cota mensal de coletas no Open Finance e gera 429. Trate o PATCH como recurso escasso: só para Items defasados, espaçados ao longo da janela noturna.

Quando o retry do webhook se esgota

A Malvo tenta entregar cada evento em até 9 tentativas, distribuídas em 3 fases: Esgotadas as 9 tentativas, o evento é descartado permanentemente. (Exceção: item/login_succeeded é entregue no máximo 3 vezes, sem backoff — é só uma dica de UX.)
Não há re-entrega automática depois das 9 tentativas. A recuperação é exatamente o job noturno (Camada 2): liste os Items, dê PATCH /items/{id} nos defasados e re-busque GET /v2/transactions dos últimos N dias. Como tudo é upsert por id, você fecha o buraco sem duplicar nada.
Para um evento específico que você sabe ter perdido, use a re-entrega manual na página de Eventos do Dashboard — ela reusa o mesmo eventId, então o seu handler idempotente trata sem efeito duplicado.

Checklist de robustez

1

Dedupe por eventId

Persista todo eventId e trate duplicatas como no-op antes de qualquer efeito colateral.
2

Ack first, work later

Responda 2XX em <5s e processe na fila. Trabalho pesado síncrono causa timeout e retries.
3

REST como fonte da verdade

Em cada item/*, re-busque GET /items/{id} e GET /accounts. Upsert por id.
4

Job noturno

Liste Items, PATCH nos defasados, refetch dos últimos N dias.
5

Backoff com jitter

Em 429, respeite Retry-After e espace os jobs para não queimar a cota mensal do Open Finance.

Próximos passos

Webhooks

Eventos, entrega, retries e segurança em detalhe.

Sincronizar transações

Paginação por cursor e atualização incremental com createdAtFrom.