Paginación
Paginación por cursor con starting_after y ending_before. Sin ?page=, semántica al estilo Stripe.
Todos los endpoints de listado de la API pública usan paginación por cursor.
Misma semántica que Stripe / Linear: paginas por un identificador opaco (el
id del recurso), no por número de página. Esto garantiza resultados estables
incluso cuando se crean nuevos recursos durante la iteración.
Parámetros
| Parámetro | Tipo | Por defecto | Rango | Descripción |
|---|---|---|---|---|
limit | integer | 25 | 1–100 | Número de elementos por página. |
starting_after | string | null | id (UUID v7) | Devuelve los elementos creados después del recurso cuyo id se pasa. |
ending_before | string | null | id (UUID v7) | Devuelve los elementos creados antes del recurso cuyo id se pasa. |
sort | string | -created | campo por recurso | Campo de orden; prefijo - para descendente (p.ej. -total). Ver Orden. |
starting_after y ending_before son mutuamente excluyentes. Enviar
ambos en la misma petición responde 422 con un envoltorio de error
invalid_request_error.
Forma de la respuesta
{
"data": [
{ "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "...": "..." },
{ "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c", "...": "..." }
],
"has_more": true,
"next_cursor": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c"
}data: array de hastalimitelementos, ordenados poridDESC (equivalente acreated_atDESC porque usamos UUID v7).has_more:truesi hay más elementos antes del primero dedata(más nuevos) al paginar constarting_after, o después del último al paginar conending_before.next_cursor:iddel último elemento dedata. Pásalo comostarting_afteren la siguiente petición para avanzar.
Cuando no hay más elementos, has_more es false y next_cursor es
null.
Iterar todos los resultados
import os, requests
def iterate(endpoint):
cursor = None
while True:
params = {'limit': 100}
if cursor:
params['starting_after'] = cursor
resp = requests.get(
f'https://api.factuarea.com/v1/{endpoint}',
params=params,
headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"},
)
resp.raise_for_status()
body = resp.json()
yield from body['data']
if not body['has_more']:
break
cursor = body['next_cursor']
for invoice in iterate('invoices'):
print(invoice['id'], invoice['number'])async function* iterate(endpoint) {
let cursor = null;
while (true) {
const url = new URL(`https://api.factuarea.com/v1/${endpoint}`);
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('starting_after', cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.json();
for (const item of body.data) yield item;
if (!body.has_more) break;
cursor = body.next_cursor;
}
}
for await (const invoice of iterate('invoices')) {
console.log(invoice.id, invoice.number);
}cursor=""
while : ; do
if [ -z "$cursor" ]; then
url="https://api.factuarea.com/v1/invoices?limit=100"
else
url="https://api.factuarea.com/v1/invoices?limit=100&starting_after=$cursor"
fi
resp=$(curl -s -H "Authorization: Bearer $FACTUAREA_API_KEY" "$url")
echo "$resp" | jq -c '.data[]'
has_more=$(echo "$resp" | jq -r '.has_more')
cursor=$(echo "$resp" | jq -r '.next_cursor')
[ "$has_more" = "true" ] || break
doneIterar con el SDK oficial
Los SDK de TypeScript y PHP ocultan el cursor por completo: los métodos de listado devuelven un iterador que recorre todas las páginas por ti.
const page = await factuarea.invoices.list({ status: "paid", limit: 50 });
// iterate every item across every page — cursors handled internally
for await (const invoice of page) {
console.log(invoice.id, invoice.number);
}
// or walk page by page
page.data; // items on this page
page.hasMore; // boolean
page.nextCursor; // opaque cursor or null
const next = await page.getNextPage(); // Page | null
const all = await page.toArray(); // collect everythinguse Factuarea\Sdk\Custom\Pagination\PageIterator;
use Factuarea\Sdk\Models\Operations\PublicApiV1InvoicesListRequest;
$pages = new PageIterator(
fn (?string $cursor) => $factuarea->invoices->publicApiV1InvoicesList(
new PublicApiV1InvoicesListRequest(startingAfter: $cursor),
)->rawResponse,
);
foreach ($pages->items() as $invoice) {
echo $invoice['id'], PHP_EOL;
}Consulta SDKs › Paginar con el SDK para ver la superficie completa.
Orden
Pasa ?sort=<campo> para ordenar un listado. Un campo a secas ordena de
forma ascendente; el prefijo - ordena de forma descendente
(p.ej. ?sort=-total). Si se omite, el valor por defecto es
-created — equivalente a id DESC, un orden total y estable porque
usamos UUID v7 (que codifica una marca de tiempo en los 48 bits altos, así
que "los más recientemente creados primero" no necesita ninguna columna
created_at adicional).
El cursor (starting_after / ending_before) sigue funcionando con el
orden que elijas: el campo escogido es el criterio principal y el id del
recurso es un criterio secundario estable, de modo que la paginación se
mantiene determinista incluso cuando varias filas comparten el mismo valor
(al estilo Stripe).
Los campos sort permitidos están acotados por recurso — enviar un
campo no soportado responde 422 con un envoltorio de error
invalid_request_error:
| Recurso | Campos sort permitidos |
|---|---|
invoices | created, total, number |
quotes | created, total, number, valid_until |
proformas | created, total, number, valid_until |
delivery_notes | created, number, delivery_date |
purchase_invoices | created, total, issued_on, due_on |
recurring_invoices | created, next_run_at |
# Facturas, mayor total primero
curl -G https://api.factuarea.com/v1/invoices \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
--data-urlencode "sort=-total"
# Presupuestos por fecha de validez, los que vencen antes primero
curl -G https://api.factuarea.com/v1/quotes \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
--data-urlencode "sort=valid_until"¿Por qué no ?page=?
Las páginas numéricas tienen problemas cuando el conjunto de datos cambia durante la iteración:
- Crear un recurso entre páginas → duplica filas.
- Eliminar un recurso entre páginas → omite filas.
COUNT(*)es costoso pasadas unos pocos miles de filas.
La paginación por cursor con UUID v7 elimina ambos: el cursor apunta a una posición estable en el tiempo, no a un desplazamiento variable.
Por eso no hay un parámetro de desplazamiento ?page=N — la única forma de paginar
por una lista es el cursor starting_after / ending_before. Un
valor de cursor inválido responde 422 con un envoltorio de error
invalid_request_error (code: parameter_invalid_cursor):
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid_cursor",
"message": "The provided cursor is not a valid resource id.",
"param": "starting_after",
"request_id": "req_..."
}
}Caso de uso: obtener solo resultados nuevos
Si tu integración hace polling cada N minutos, guarda el next_cursor (el
id más reciente que has visto) entre cada sondeo. En la siguiente pasada usa
ending_before=<saved_cursor> para obtener solo los elementos más nuevos que ese
punto.
last_seen = load_last_cursor() # resource id stored in your DB
resp = requests.get(
'https://api.factuarea.com/v1/invoices',
params={'limit': 100, 'ending_before': last_seen} if last_seen else {'limit': 100},
headers={'Authorization': f"Bearer {API_KEY}"},
)
new_invoices = resp.json()['data']
if new_invoices:
save_last_cursor(new_invoices[0]['id']) # the newest oneErrores y límites de peticiones
Formas de error JSON-RPC mapeadas desde el contrato v1, la tabla completa de códigos y throttling por token / por plan con Retry-After.
Etiquetas y campos personalizados
Clasifica documentos con tags y adjunta custom_fields tipados. Filtra listados por tag. En qué se diferencian de metadata.