Mecânica de sincronização incremental

Os endpoints inbound retornam registros ordenados pela data de criação (createdAt), usando keyset pagination: a cada resposta a API devolve um nextToken que…

Comum a todos os modelos (NFS-e, CT-e, NF-e). Este documento cobre o que é idêntico entre os endpoints /api/integration/dfe/{modelo}/inbound/{documentos|eventos} de qualquer modelo: paginação por cursor, regras de token, semântica de polling, rate limit e o padrão de erros. Cada modelo tem sua própria página com endpoint, filtros específicos, exemplos de payload e os valores numéricos que lhe correspondem — ver seção "Por modelo" em Inbound.


1. Pré-requisitos

  • Autenticação via JWT (Bearer token) válida. O customerId é extraído automaticamente do token.
  • Todos os timestamps devem ser informados em epoch milliseconds UTC.

2. Paginação por cursor (keyset pagination)

Os endpoints inbound retornam registros ordenados pela data de criação (createdAt), usando keyset pagination: a cada resposta a API devolve um nextToken que codifica a posição do último registro retornado (internamente, (createdAt, id)). Esse token é reenviado nas chamadas seguintes para avançar a partir daquele ponto — não é um OFFSET tradicional.

Registros atrasados são silenciosamente ignorados. Se um registro for persistido com createdAt anterior à posição já ultrapassada pelo cursor (ex.: processamento fora de ordem no backend), ele não aparece em nenhuma página futura da sequência — o cursor já avançou além daquele ponto. Isso é uma propriedade inerente ao design de keyset por createdAt, não um bug. Na prática, isso é raro (a janela entre a criação e a indexação é curta) mas o client não deve assumir garantia de entrega de 100% dos registros por este mecanismo isoladamente; para reconciliação crítica, considere reprocessar periodicamente uma janela createdFrom recuada.

Semântica do hasMore

hasMoreSignificado
trueA página retornou a quantidade exata de registros solicitada (size). Provavelmente há mais dados.
falseMenos registros que size foram retornados, ou nenhum. Não há mais dados neste momento — não significa fim do processo (ver § 6).

3. Regras do token

Opacidade

O nextToken é uma string codificada que contém internamente a posição do cursor e um hash dos filtros utilizados para gerá-lo. O client não deve interpretar ou manipular o conteúdo do token — apenas armazená-lo e reenviá-lo.

A chave nextToken é omitida quando não há valor — nunca aparece como null

A API não serializa campos com valor null. Quando não há próxima página (resposta vazia, ou fim de uma sequência), o objeto data simplesmente não tem a chave nextToken — não é "nextToken": null. Um client que leia o JSON com acesso direto (data["nextToken"] em Python, data.nextToken tipado como não-opcional, etc.) quebra nesse caso; use uma leitura tolerante a chave ausente (data.get("nextToken"), Optional, valor default) — ver §9.

Imutabilidade dos filtros

Todos os parâmetros de filtro usados para gerar um nextToken devem permanecer idênticos em todas as chamadas daquela sequência de paginação (o conjunto exato de filtros varia por modelo/endpoint — ver a página específica). Se algum filtro for alterado com um token ativo, a API rejeita a requisição com HTTP 400 e um código {MODELO}_LISTING_TOKEN_FILTER_MISMATCH (ex.: CTE_LISTING_TOKEN_FILTER_MISMATCH, NFSE_LISTING_TOKEN_FILTER_MISMATCH). Para mudar os filtros, descarte o token e inicie uma nova sequência.

O token de um endpoint (documentos) não é intercambiável com o de outro (eventos), nem entre modelos.


4. Como paginar passo a passo

Passo 1 — Primeira chamada

Defina a janela temporal (createdFrom obrigatório) e os filtros desejados. Não envie nextToken.

Passo 2 — Páginas seguintes

Repita a chamada adicionando o nextToken retornado, com os mesmos filtros. Continue enquanto hasMore == true, sobrescrevendo o token a cada resposta.

Passo 3 — Fim dos dados

Em algum momento a resposta virá com menos registros que size, ou vazia — items: [], hasMore: false e sem a chave nextToken (ver nota na §3 sobre omissão de campos null). Não descarte o último token não-nulo que você já tinha — ele será usado para o polling contínuo.

Passo 4 — Polling contínuo

Reenvie a última chamada usando o último nextToken não-nulo armazenado, com os mesmos filtros. Novos registros criados desde a última consulta serão retornados. Se não houver novidades, a resposta volta vazia — aguarde um intervalo e tente de novo.


5. O que o client deve armazenar

DadoPor quê
createdFromPonto de partida fixo da janela. Não muda ao longo da sequência.
createdTo (se usado)Limite superior fixo, reenviado igual em todas as chamadas.
Filtros opcionais do endpointReenviados identicamente enquanto o token estiver ativo.
Último nextToken não-nuloCursor que permite retomar da posição exata, sem reprocessar desde createdFrom.

Dica: armazene filtros + token como uma unidade (registro de banco ou JSON em disco), para restaurar o estado exato após falhas/reinícios sem risco de inconsistência.


6. Quando não houver mais registros

items: [] e hasMore: false significam que neste instante não há mais dados além do cursor — não que o processo terminou. Mecânica de polling incremental:

  1. Manter o último nextToken não-nulo.
  2. Aguardar um intervalo (respeitando o rate limit).
  3. Repetir a chamada com o token e os mesmos filtros.
  4. Processar os novos registros, se houver.
  5. Repetir.

7. Rate limit

Isolado por customerId (tenant): cada tenant tem cota própria, sem interferência entre clientes distintos. O valor (requisições por janela) é específico de cada modelo/endpoint — ver a página correspondente — mas o mecanismo é sempre o mesmo:

  • Excedeu a cota → HTTP 429, código {MODELO}_LISTING_RATE_LIMITED.
  • Recomenda-se distribuir o polling ao longo da janela (ex.: com cota de 5/60s, um intervalo de 12–15s entre chamadas evita rajadas).
  • Em caso de 429, aplique backoff (ex.: aguardar o período de refresh completo antes de tentar novamente).

8. Padrão de erros

Todos os endpoints inbound seguem o mesmo conjunto de erros, com o código prefixado pelo modelo (NFSE_LISTING_*, CTE_LISTING_*, etc.):

Erro (sufixo)HTTPCausaAção
_INVALID_DATE_RANGE400createdTo <= createdFromCorrigir as datas.
_INVALID_TOKEN400Token malformado/corrompido/versão não suportadaDescartar e reiniciar do createdFrom.
_TOKEN_FILTER_MISMATCH400Filtros diferem dos que geraram o tokenReenviar filtros idênticos ou iniciar nova sequência.
_RATE_LIMITED429Cota excedidaAplicar backoff e tentar novamente.

9. Exemplo genérico de loop de sincronização

FILTERS = { "createdFrom": 1710400000000, "size": 50 }   # + filtros específicos do modelo/endpoint
POLL_INTERVAL_SECONDS = 15
next_token = storage.load("<modelo>_<endpoint>_token")   # None na primeira execução

while True:
    params = {**FILTERS}
    if next_token is not None:
        params["nextToken"] = next_token

    response = http_get("/api/integration/dfe/{modelo}/inbound/{documentos|eventos}", params)
    if response.status == 429:
        wait(60); continue

    data = response.json()["data"]
    for item in data["items"]:
        process(item)

    # nextToken pode estar AUSENTE do JSON (não é uma chave "null") — use .get(), nunca data["nextToken"]
    new_token = data.get("nextToken")
    if new_token is not None:
        next_token = new_token
        storage.save("<modelo>_<endpoint>_token", next_token)

    if data["hasMore"]:
        continue          # próxima página (respeitando rate limit)
    wait(POLL_INTERVAL_SECONDS)   # sem mais dados — polling

Pontos-chave

  • Inicialização: na primeira execução next_token local é None; a chave nextToken não é enviada na primeira chamada, e a API retorna os registros mais antigos a partir de createdFrom, em ordem cronológica.
  • Leitura tolerante: sempre leia nextToken com .get() (ou equivalente) — a API omite a chave quando não há token, nunca a envia como null.
  • Drenagem: enquanto hasMore == true, busque páginas consecutivas (respeitando o rate limit).
  • Polling: quando hasMore == false, espere e repita.
  • Idempotência: em falha/restart, recarregue o nextToken do storage e retome exatamente de onde parou — sem duplicatas.

Nesta página