A forma correta de manter as transações de um usuário em dia é dirigida por webhooks: você assina transactions/created, transactions/updated e transactions/deleted, e a cada evento puxa as transações de forma incremental e aplica um upsert (ou delete) por id. Nada de polling em intervalo fixo.
Há um único host de API: https://api.malvo.io. Toda leitura de transação é server-side com o header X-API-KEY. Os webhooks são a fonte da verdade do que mudou.

Por que webhooks, e não polling

transactions/created

Novas transações detectadas durante o sync. Traz itemId e transactionsCount.

transactions/updated

Transações existentes mudaram (ex.: PENDINGPOSTED). Traz itemId e transactionsCount.

transactions/deleted

Transações removidas (merge de duplicatas no Open Finance). Traz transactionIds inline.
Em transactions/created e transactions/updated, liste as contas do itemId e pagine GET /v2/transactions?accountId=... para cada conta. Use createdAtFrom para puxar somente o que entrou desde a última sincronização. O payload não traz accountId nem link de transações.

1. Pagine /v2/transactions por cursor

O endpoint atual é GET /v2/transactions, paginado por cursor. A resposta traz results e um campo next: uma query string pronta para anexar à URL. Você continua chamando enquanto next não for null.
Resposta de /v2/transactions
status=UNKNOWN é um registro válido cujo provedor não enquadrou o lançamento como liquidado ou pendente. Preserve-o e consulte providerStatus (por exemplo, OTHR em alguns ASPSPs); não o trate como delete. Cancelamentos/rejeições confirmados chegam por transactions/deleted.
Loop até next === null (Node)
Não há parâmetro pageSize no /v2/transactions: o servidor controla o tamanho da página (500 por página). O cursor after é opaco — trate como string e não tente decodificá-lo.

2. Sync incremental com createdAtFrom

Para uma sincronização incremental, filtre por createdAtFrom (horário de ingestão, formato yyyy-mm-ddThh:mm:ss.000Z). Guarde o maior createdAt que você já processou e use-o como createdAtFrom na próxima execução.
GET incremental
createdAtFrom não pode ser combinado com dateFrom no mesmo request. Use createdAtFrom para sync incremental (o que entrou desde a última coleta) e dateFrom/dateTo para janelas por data da transação.

3. Upsert por id, delete por id

Como os webhooks não têm ordem garantida e podem ser reentregues, a regra de ouro é tornar a escrita idempotente: faça upsert por id da transação. Em transactions/deleted, apague por id.
Aplicar transações no seu banco
O payload de transactions/deleted traz os ids inline (sem link):
Webhook transactions/deleted

4. Worker de exemplo

Junte tudo em um worker que reage aos webhooks. Cada execução faz uma sincronização, seguindo o link do evento (ou paginando /v2/transactions), e aplica os efeitos por id.
Worker dirigido por webhook (Node)

Boas práticas

Uma sync por execução

Cada webhook dispara exatamente uma sincronização da conta afetada. Não acumule nem rode em paralelo várias coletas da mesma conta.

Dirigido por webhook

Prefira os webhooks transactions/* a polling. Quando precisar puxar manualmente, use createdAtFrom em /v2/transactions, uma vez por sincronização.

Upsert por id

Escreva sempre por id da transação. Como os eventos não têm ordem e podem repetir, o upsert idempotente evita duplicatas.

Dedupe pelo eventId

Persista o eventId e ignore entregas repetidas antes de aplicar efeitos colaterais.
Use GET /v2/transactions (cursor) para qualquer sincronização nova. O antigo GET /transactions (paginado por page/pageSize) é descontinuado — mantido só por compatibilidade.

Próximos passos

Conectar um banco

O fluxo ponta a ponta que cria o Item de onde vêm as transações.

Webhooks

Os payloads exatos de transactions/created, transactions/updated e transactions/deleted.