Pular para o conteúdo

Contratos

Paginação e datas

Cursor opaco, ordem e teto por rota, as listas que não paginam e como sincronizar sem perder registro.

As listas grandes paginam por cursor, nunca por número de página — a ordenação é estável mesmo com escritas acontecendo entre uma página e a próxima. Nem toda listagem pagina, e isso está detalhado abaixo.

O envelope da página

200 OK
{
  "data": [ /* ... */ ],
  "nextCursor": "WyIyMDI2LTA5LTEwIDEyOjAwOjAwLjEyMzQ1NiIsImNfMDFIWFkiXQ"
}

nextCursor: null = não há mais página. Devolva o valor recebido no parâmetro cursor da chamada seguinte, e repita até vir null. Não mude os filtros no meio do laço: o cursor foi emitido para aquela consulta.

O cursor é opaco

Ordem, padrão e teto por rota

O limit vai até 500 em todas as listas paginadas, mas o padrão não é o mesmo em todas. Valor acima do teto ou abaixo de 1 responde 400 — não é silenciosamente aparado.

RotaOrdemlimit padrãoTeto
GET /contactscreatedAt desc50500
GET /opportunitiescreatedAt desc50500
GET /opportunity-eventsat (momento da transição) desc50500
GET /taskscreatedAt desc50500
GET /interactionscreatedAt desc100500
GET /appointmentsstartAt asc100500
GET /platform/userscreatedAt desc50500

Repare na exceção: /appointments é a única lista em ordem crescente, e por startAt — a grade de reuniões se lê em ordem de horário. Se o seu laço assume "mais novo primeiro", ele está errado nessa rota.

As listas que não paginam

Estas devolvem tudo de uma vez e não aceitam cursor nem limit: /pipelines, /calendars, /users, /custom-fields, /opportunities/:id/history e /appointments/:id/history — todas no envelope { data }, sem nextCursor. Duas ainda mais diferentes: /tags devolve { tags } e /calendars/:id/slots devolve { timezone, slots }.

Datas nos filtros

Inclusividade: quem é maior, quem é maior ou igual

Os filtros de polling (createdAfter, updatedAfter, wonAfter, lostAfter, stageChangedAfter) são estritamente maior (>) — é por isso que reusar o último valor visto como próximo updatedAfter nunca reprocessa o mesmo registro. Os *Before, onde existem, são estritamente menor.

A exceção que costuma morder: em GET /appointments, from é maior ou igual e to é estritamente menor — o intervalo é [from, to) e recorta pelo horário da reunião, não pela data de criação. A tabela por rota está em convenções.

Três formatos de data, não um

Toda data de saída é ISO-8601 UTC com Z — mas a entrada não é uniforme: há rota que só aceita YYYY-MM-DD, campo de escrita que recusa data sem fuso explícito e campo que não valida formato nenhum. As três regras estão em convenções. Não presuma um formato só.

Sincronizar sem perder registro

Para espelhar o CRM por polling, guarde uma marca d'água por recurso e, a cada ciclo:

pseudocódigo
marca = ultima_marca_salva      // ex.: "2026-09-13T18:04:11.231Z"
cursor = null
maiorUpdatedAt = marca

repita:
  r = GET /contacts?updatedAfter={marca}&limit=500 + (cursor ? "&cursor=" + cursor : "")
  para cada item em r.data:
    processe(item)
    se item.updatedAt > maiorUpdatedAt: maiorUpdatedAt = item.updatedAt
  cursor = r.nextCursor
enquanto cursor != null

salve(maiorUpdatedAt)           // só DEPOIS de varrer todas as páginas

Use o mesmo laço em /opportunities e /appointments. Para a linha do tempo de atendimento use /interactions?createdAfter= — ela é imutável, não tem updatedAfter e não precisa. E combine com webhooks, não troque um pelo outro: webhook é a baixa latência, o polling é a rede de segurança — importação em massa não dispara webhook nenhum.