Sincronização incremental
Como manter o seu sistema em dia com as vendas e os recebimentos de cada cliente, sem perder registros, guardando só uma data por cliente.
O que guardar por cliente
| Dado | Quando muda |
|---|---|
| CNPJ | Nunca |
company_token | Na aprovação e em cada reemissão |
updated_since de vendas | A cada rodada de /v1/sales |
updated_since de recebimentos | A cada rodada de /v1/payments |
A rodada
Uma rodada é buscar page=1, 2, 3... com o mesmo updated_since e os mesmos filtros, até has_more: false.
- Primeira vez: não envie
updated_since. Vêm todos os registros dos últimos 90 dias, o máximo que a API entrega. - Chame
page=1. A resposta traz onext_updated_since: anote, ele é o ponto de partida da próxima rodada. - Para cada item, faça upsert pelo
id: atualize se já existe, insira se não. - Se
has_morefortrue, chame a página seguinte, com o mesmoupdated_since. - Quando
has_moreforfalse, a rodada acabou. Grave onext_updated_sinceda página 1 como o novoupdated_sincedesse cliente e desse feed. - Espere a próxima rodada (por exemplo, a cada 15 minutos ou de hora em hora) e volte ao passo 2.
:::warning Use o next_updated_since da página 1
Só a página 1 traz o next_updated_since; nas outras ele vem null. Ele marca o início da rodada, com 30 segundos de folga para trás: o que for alterado enquanto você pagina volta na rodada seguinte, e os registros desses 30 segundos vêm de novo (o upsert absorve). Não use a hora em que a rodada terminou.
:::
Exemplo em JavaScript
Node.js 18 ou mais novo (usa o fetch nativo). As funções getAccessToken, upsert, saveUpdatedSince e handleUnauthorized são do seu sistema.
const BASE_URL = process.env.BASE_URL
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
/**
* Uma rodada de um feed para um cliente.
* @param {object} client { companyToken, updatedSince: { sales, payments } }
* @param {'sales' | 'payments'} feed
*/
async function sync(client, feed) {
// Na primeira vez fica vazio: vêm os últimos 90 dias
const since = client.updatedSince[feed]
let nextSince = null
let page = 1
while (true) {
const url = new URL(`/v1/${feed}`, BASE_URL)
url.search = new URLSearchParams({
...(since && { updated_since: since }),
page: String(page),
page_size: '1000',
})
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${await getAccessToken()}`, // reutilizado por 10 min
'X-Company-Token': client.companyToken,
},
})
if (response.status === 429) {
const seconds = Number(response.headers.get('Retry-After') ?? 1)
await sleep(seconds * 1000)
continue // repete a mesma página
}
if (response.status === 401) {
await handleUnauthorized(client) // ver "Quando dá 401"
return
}
if (!response.ok) {
throw new Error(`${feed} página ${page}: HTTP ${response.status}`)
}
const { data } = await response.json()
if (page === 1) nextSince = data.next_updated_since
for (const item of data.items) {
await upsert(feed, item.id, item)
}
if (!data.has_more) break
page += 1
}
// Só grava no fim: se a rodada cair no meio, a próxima recomeça do mesmo ponto
await saveUpdatedSince(client, feed, nextSince)
}
// Uso: um cliente por vez, vendas e depois recebimentos
for (const client of clients) {
await sync(client, 'sales')
await sync(client, 'payments')
}
Por que deduplicar pelo id
Um registro aparece de novo sempre que é alterado. Por exemplo: uma venda cancelada ou um recebimento que passa de PENDING para PAID. Ele também pode vir repetido dentro da mesma rodada, se for alterado enquanto você pagina. Por isso:
- Não trate cada item como um registro novo.
- Sempre use o
idcomo chave e substitua os dados antigos pelos novos. - Repetir uma rodada inteira (depois de uma falha, por exemplo) não causa problema: o upsert torna as repetições inofensivas.
Com upsert pelo id, nenhum registro se perde, mesmo que mude durante a paginação.
Se algo der errado
| Situação | O que fazer |
|---|---|
| A rodada caiu no meio | Recomece do page=1 com o mesmo updated_since de antes. |
Perdeu o updated_since guardado | Use uma data alguns dias para trás, ou não envie updated_since para recarregar os últimos 90 dias. O upsert absorve o que vier repetido. |
| Quer reprocessar um período | Filtre por date_from/date_to, dentro dos últimos 90 dias. |
Muitos clientes
O limite por minuto soma todos os clientes da sua software house. Com muitos clientes:
- Sincronize os clientes em fila, um ou poucos de cada vez, não todos ao mesmo tempo.
- Use um único token de acesso para todos os clientes enquanto ele valer.
- Ao receber
429, espere oRetry-Afterantes de repetir a mesma página. - Use
page_size=1000para fazer menos chamadas.
Primeira carga de um cliente grande
A API entrega no máximo os últimos 90 dias, então a primeira carga é limitada a esse período. Se o volume for grande, páginas muito fundas demoram mais: divida a carga com date_from e date_to (por exemplo, um mês por vez), como em Consultar um dia ou período. Depois disso, siga com rodadas normais por updated_since.
Quando o cliente revoga
Se um feed começar a dar 401 só para um cliente, siga o roteiro de Quando dá 401. Se o pedido estiver REVOKED, tire esse cliente da fila de sincronização até ele autorizar de novo.