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
{
"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.
| Rota | Ordem | limit padrão | Teto |
|---|---|---|---|
GET /contacts | createdAt desc | 50 | 500 |
GET /opportunities | createdAt desc | 50 | 500 |
GET /opportunity-events | at (momento da transição) desc | 50 | 500 |
GET /tasks | createdAt desc | 50 | 500 |
GET /interactions | createdAt desc | 100 | 500 |
GET /appointments | startAt asc | 100 | 500 |
GET /platform/users | createdAt desc | 50 | 500 |
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:
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áginasUse 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.