Skip to content

Pagination

Walk long lists with a cursor: stable even if records are created while you paginate.

What paginates how

Endpoint Cursor Offset Order meta
GET /api/v1/cotizaciones Yes Yes Newest first limit, offset, total, next_cursor
GET /api/v1/clientes Yes Yes By empresa (A to Z) limit, offset, total, next_cursor
GET /api/v1/productos Yes Yes Active first, then by nombre limit, offset, total, next_cursor
GET /api/v1/facturas Yes No Newest first next_cursor
GET /api/v1/events Yes No Newest first next_cursor
GET /api/v1/webhooks No No Newest first No meta: returns all of the key’s endpoints.
GET /api/v1/cobranza No No — No meta: returns the full receivables portfolio.

On every paginated list, limit goes from 1 to 200 and defaults to 50.

Use the cursor

Request the first page without a cursor. If meta.next_cursor has a value, repeat the same request, with the same filters, adding cursor=<next_cursor>. When it comes back null, there’s nothing left.

curl "https://cordhq.app/api/v1/clientes?limit=2" \
  -H "Authorization: Bearer sk_test_..."
{
  "data": [
    { "id": "0e6f2b8a-91c4-4d7e-b3a5-6c2d8f1e4a90", "empresa": "Aceros Monterrey", "email": "compras@acerosmty.mx" },
    { "id": "7a2d4c19-5e8b-4f3a-a1d6-9c0e2b4f6d81", "empresa": "Bodega Central", "email": "" }
  ],
  "meta": {
    "limit": 2,
    "offset": 0,
    "total": 142,
    "next_cursor": "WyJCb2RlZ2EgQ2VudHJhbCIsIjdhMmQ0YzE5LTVlOGItNGYzYS1hMWQ2LTljMGUyYjRmNmQ4MSJd"
  }
}
curl "https://cordhq.app/api/v1/clientes?limit=2&cursor=WyJCb2RlZ2EgQ2VudHJhbCIsIjdhMmQ0YzE5LTVlOGItNGYzYS1hMWQ2LTljMGUyYjRmNmQ4MSJd" \
  -H "Authorization: Bearer sk_test_..."

The example trims each client to three fields; the real response has the full object.

The cursor is an opaque token: don’t interpret it, don’t build it and don’t keep it for another list. Send it back as is, URL-encoded (the examples below do that for you). With cursor, offset is ignored and meta.offset comes back null.

Why cursor and not offset

With offset, a quote created while you walk the list shifts everything by one position: the next page repeats a record, and if one is deleted, you skip another without noticing. The cursor points at the last row you saw, so the next page starts exactly after it, whether or not there were writes.

offset still works on quotes, clients and products for compatibility. meta.total counts every record matching the filter: it’s for showing “142 clients”, not for deciding when to stop. Use next_cursor for that.

Walk a complete list

#!/usr/bin/env bash
# Requires jq. Prints the id and company of every client.
set -euo pipefail
args=(-sSG "https://cordhq.app/api/v1/clientes" -d limit=200 -H "Authorization: Bearer $CORD_API_KEY")
cursor=""
while :; do
  if [ -n "$cursor" ]; then
    page=$(curl "${args[@]}" --data-urlencode "cursor=$cursor")
  else
    page=$(curl "${args[@]}")
  fi
  echo "$page" | jq -r '.data[] | [.id, .empresa] | @tsv'
  cursor=$(echo "$page" | jq -r '.meta.next_cursor // empty')
  [ -z "$cursor" ] && break
done

listAll() exists on quotes, clients, products, invoices and events, and always paginates by cursor since version 1.1.0 of the SDK. See Node SDK.

Errors

Situation Response
A cursor that isn’t from quotes, clients or products 400, code: "invalid_cursor", error: "El cursor no es válido. Usa el next_cursor de la respuesta anterior."
A cursor that isn’t from events 400, code: "invalid_request", error: "cursor inválido"
limit out of range Not an error: it’s clamped to 1 or 200.
{
  "error": "El cursor no es válido. Usa el next_cursor de la respuesta anterior.",
  "code": "invalid_cursor",
  "request_id": "req_5d2b8f1a4c7e0b3d6a9f2c5e",
  "doc_url": "https://docs.cordhq.app/docs/desarrolladores/esenciales/errores#invalid_cursor"
}

A cursor doesn’t carry across lists or filters: if you change status, type or any other filter, start again without a cursor.