Pular para o conteúdo

Sincronização incremental com updated_after

Você quer uma cópia dos clientes, das vendas ou das OS no seu sistema, e quer mantê-la em dia sem baixar tudo de novo a cada hora. É para isso que existe o filtro updated_after: ele traz só o que mudou desde um instante, na ordem em que mudou.

Os exemplos usam clientes, mas o mesmo vale para toda lista cujo objeto tem updated_at.

Toda lista aceita updated_after. Com ele, três coisas mudam:

  • O recorte. Só vem o registro com updated_at depois do instante que você mandou. O limite é exclusivo: o registro com updated_at exatamente igual ao valor mandado não vem.
  • A ordem. A lista passa a vir por updated_at crescente, do que mudou há mais tempo para o que mudou agora. Sem updated_after, a ordem é created_at decrescente.
  • O desempate. Dois registros com o mesmo updated_at saem sempre na mesma ordem, pelo id. É o que faz a paginação não pular nem repetir registro entre uma página e outra.

created_after e created_before também são exclusivos, e podem ser combinados com updated_after.

O cursor guarda o modo de ordenação. Por isso, mande os mesmos filtros em todas as páginas da mesma rodada, só acrescentando cursor. Um cursor da ordem por updated_at usado numa chamada sem updated_after responde 422: trocar de modo no meio é começar de novo. Veja Paginação.

  1. Na primeira rodada, não há ponto de partida. Mande um instante bem antigo, como 1970-01-01T00:00:00Z: isso traz tudo, já na ordem de sincronização.
  2. Leia página por página, seguindo o next_cursor, até has_more vir false.
  3. Grave cada registro no seu lado pelo id, como atualização: se já existe, sobrescreve; se não existe, cria.
  4. Depois de cada página, guarde o maior updated_at que você viu. Esse é o ponto de partida da próxima rodada.
  5. Na próxima rodada, comece um pouco antes desse ponto, com uma janela de sobreposição.

Mandar exatamente o maior updated_at visto funciona quase sempre. Os buracos estão nas bordas:

  • o registro gravado no mesmo instante do último que você leu, mas que ainda não estava confirmado no banco quando a sua leitura passou, fica de fora, porque o limite é exclusivo;
  • uma gravação longa pode receber um updated_at um pouco anterior ao momento em que ela fica visível para leitura.

Voltar alguns minutos resolve os dois casos. Recomendamos 5 minutos: o custo é reler alguns registros a cada rodada, e a gravação pelo id do passo 3 já torna a releitura inofensiva. Se quiser pular o trabalho repetido, compare o updated_at que chegou com o que você tem e ignore quando for igual.

A janela também protege quando o seu processo cai no meio de uma rodada: você recomeça do último ponto guardado, menos a janela, e nada fica para trás.

Terminal window
# Uma página da rodada. O ponto de partida é o maior updated_at da rodada
# anterior, menos a janela de sobreposição.
curl -G "https://api.fatureihoje.com/public/v1/clients" \
-H "Authorization: Bearer $FH_API_KEY" \
--data-urlencode "updated_after=2026-09-24T12:55:00Z" \
--data-urlencode "limit=100"
# A próxima página: os MESMOS filtros, mais o next_cursor que veio.
curl -G "https://api.fatureihoje.com/public/v1/clients" \
-H "Authorization: Bearer $FH_API_KEY" \
--data-urlencode "updated_after=2026-09-24T12:55:00Z" \
--data-urlencode "limit=100" \
--data-urlencode "cursor=VALOR_DO_NEXT_CURSOR"

O laço respeita o 429: espera o Retry-After e repete a mesma página. Uma primeira rodada numa base grande gasta bastante do orçamento de chamadas da empresa, que é um só para todas as chaves dela; use limit=100 e rode fora do horário de pico. Veja Limites de uso.

Exclusão. Registro excluído some da lista, e por isso nunca aparece numa rodada. O mesmo vale para OS excluída, que é arquivada e passa a responder 404. Para saber o que saiu:

  • inscreva-se nos eventos .deleted da área (client.deleted, service_order.deleted e os outros do Catálogo de eventos);
  • ou, de tempos em tempos, compare a lista inteira de ids com a sua cópia.

Exclusão também pode mexer em outros registros: quando um cliente é excluído, as tarefas e os compromissos dele perdem o vínculo. Ao receber um .deleted, releia o que no seu sistema apontava para o registro excluído.

Listas sem updated_at. Três listas não têm objeto que muda:

  • GET /public/v1/stock_movements e GET /public/v1/events: movimento de estoque e evento não mudam depois de gravados. Nelas, updated_after recorta pelo instante de criação, em ordem crescente, e a receita acima funciona igual usando created_at como ponto de partida.
  • GET /public/v1/team: o objeto não traz data. A lista é pequena; leia inteira quando precisar.

Sincronização e webhook não competem. Cada um cobre o ponto fraco do outro:

WebhookSincronização
Quando chegaNa horaNa próxima rodada
Se o seu servidor caiNovas tentativas por cerca de 3 dias, depois perdeRecomeça do último ponto guardado
Endpoint desligadoOs eventos do período não chegamNão depende de endpoint
Mudanças sem eventoNão avisaTraz, se o updated_at mudou

O desenho que funciona: o webhook avisa, a sincronização garante. Use o evento para agir na hora (ler o objeto e atualizar o seu lado) e rode a sincronização de tempos em tempos, uma vez por hora ou por dia, como rede de segurança. As duas gravam pelo id, então receber a mesma mudança pelos dois caminhos não faz mal.

Veja Boas práticas de webhooks e Entregas e novas tentativas.