Paginació
Paginació per cursor amb starting_after i ending_before, a l'estil Stripe, i les llistes per pàgina de la biblioteca de plantilles i de l'historial de presentacions.
La majoria dels endpoints de llistat de l'API pública usen paginació per cursor.
Mateixa semàntica que Stripe / Linear: pagines per un identificador opac (l'id
del recurs), no per número de pàgina. Això garanteix resultats estables
fins i tot quan es creen nous recursos durant la iteració. Un conjunt reduït de llistes
pagina per número de pàgina; consulta Llistes per pàgina.
Els endpoints de llistat viuen al seu eix: els recursos d'empresa sota
/v1/companies/{company}/… i els de compte sota /v1/accounts/{account}/….
Els snippets de sota llegeixen aquest id opac d'una variable COMPANY_ID —
GET /v1/me enumera les empreses a què arriba la teva credencial, cadascuna
amb el seu id i el seu tax_id.
Paràmetres
| Paràmetre | Tipus | Per defecte | Rang | Descripció |
|---|---|---|---|---|
limit | integer | 25 | 1–100 | Nombre d'elements per pàgina. |
starting_after | string | null | id (UUID v7) | Retorna els elements creats després del recurs el id del qual es passa. |
ending_before | string | null | id (UUID v7) | Retorna els elements creats abans del recurs el id del qual es passa. |
sort | string | -created | camp per recurs | Camp d'ordre; prefix - per a descendent (p.ex. -total). Vegeu Ordre. |
starting_after i ending_before són mútuament excloents. Enviar
tots dos en la mateixa petició respon 422 amb un embolcall d'error
invalid_request_error.
Forma de la resposta
{
"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 fins alimitelements, ordenats peridDESC (equivalent acreated_atDESC perquè usem UUID v7).has_more:truesi hi ha més elements abans del primer dedata(més nous) en paginar ambstarting_after, o després de l'últim en paginar ambending_before.next_cursor:idde l'últim element dedata. Passa'l com astarting_afteren la petició següent per avançar.
Quan no hi ha més elements, has_more és false i next_cursor és
null.
Iterar tots els resultats
import os, requests
COMPANY_ID = os.environ['COMPANY_ID']
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/companies/{COMPANY_ID}/{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'])const COMPANY_ID = process.env.COMPANY_ID;
async function* iterate(endpoint) {
let cursor = null;
while (true) {
const url = new URL(
`https://api.factuarea.com/v1/companies/${COMPANY_ID}/${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/companies/$COMPANY_ID/invoices?limit=100"
else
url="https://api.factuarea.com/v1/companies/$COMPANY_ID/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 amb l'SDK oficial
Els SDK de TypeScript i PHP amaguen el cursor completament: els mètodes de llistat retornen un iterador que recorre totes les pàgines per tu.
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 amb l'SDK per veure la superfície completa.
Llistes per pàgina
Un conjunt reduït de llistes no usa el cursor. La biblioteca de plantilles i l'historial de presentacions d'un document són petits, van ordenats per una clau fixa i necessiten el total, així que paginen per número de pàgina.
| Operació | Paràmetres | Embolcall | Ordre |
|---|---|---|---|
GET /v1/companies/{company}/pdf-templates | q, state, document_type, purpose, format, page, per_page | items i meta | Nom i després id |
GET /v1/companies/{company}/pdf-commercial-identities | q, state, page, per_page | items i meta | Nom i després id |
GET /v1/companies/{company}/{invoices|quotes|proformas|delivery-notes}/{id}/presentation-history | page, per_page | data i meta | Número de captura, descendent |
pageés un enter des de1(per defecte1).per_pageés un enter d'1a100(per defecte25).stateésactive(per defecte),archivedoall.- Un
page,per_pageo filtre fora de rang respon422; l'API mai no el retalla en silenci. metasempre és{ "page", "per_page", "total" }, tres enters: la pàgina que vas demanar, la seva mida i el total de files que coincideixen. Una llista buida és200amb un array buit i"total": 0; no és un error.- No hi ha
has_more,next_cursornilinks: atura't quanpage * per_pagearribi atotal.
curl -G https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-templates \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
--data-urlencode "state=all" \
--data-urlencode "per_page=50" \
--data-urlencode "page=2"{
"items": [],
"meta": { "page": 2, "per_page": 50, "total": 31 }
}L'exemple és la resposta quan la llista té 31 plantilles: la pàgina 2 de 50 queda
més enllà del final, així que items és buit i total et continua dient on acaba la llista.
Altres operacions que retornen un historial acotat, com
GET /v1/companies/{company}/invoices/{invoice}/administrative-revisions, no es paginen: retornen tot
l'historial en una sola resposta ordenada per versió. Algunes llistes més antigues també
accepten page i per_page; llegeix l'esquema de resposta de cada operació a l'
API Reference abans de refiar-te del seu embolcall.
Ordre
Passa ?sort=<camp> per ordenar un llistat. Un camp tot sol ordena de
manera ascendent; el prefix - ordena de manera descendent
(p.ex. ?sort=-total). Si s'omet, el valor per defecte és -created
— equivalent a id DESC, un ordre total i estable perquè usem UUID v7
(que codifica una marca de temps en els 48 bits alts, de manera que "els
creats més recentment primer" no necessita cap columna created_at
addicional).
El cursor (starting_after / ending_before) continua funcionant amb
l'ordre que triïs: el camp escollit és el criteri principal i el id del
recurs és un criteri secundari estable, de manera que la paginació es
manté determinista fins i tot quan diverses files comparteixen el mateix
valor (a l'estil de Stripe).
Els camps sort permesos estan acotats per recurs — enviar un camp no
suportat respon 422 amb un embolcall d'error invalid_request_error:
| Recurs | Camps sort permesos |
|---|---|
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 |
# Factures, total més alt primer
curl -G https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
--data-urlencode "sort=-total"
# Pressupostos per data de validesa, els que vencen abans primer
curl -G https://api.factuarea.com/v1/companies/$COMPANY_ID/quotes \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
--data-urlencode "sort=valid_until"Per què no ?page=?
Les pàgines numèriques tenen problemes quan el conjunt de dades canvia durant la iteració:
- Crear un recurs entre pàgines → duplica files.
- Eliminar un recurs entre pàgines → omet files.
COUNT(*)és costós passades unes quantes milers de files.
La paginació per cursor amb UUID v7 elimina tots dos: el cursor apunta a una posició estable en el temps, no a un desplaçament variable.
Per això les llistes per cursor no tenen cap paràmetre de desplaçament ?page=N — només
paginen amb el cursor starting_after / ending_before. Un
valor de cursor invàlid respon 422 amb un embolcall d'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_..."
}
}Cas d'ús: obtenir només resultats nous
Si la teva integració fa polling cada N minuts, desa el next_cursor (l'id
més recent que has vist) entre cada sondeig. A la passada següent usa
ending_before=<saved_cursor> per obtenir només els elements més nous que aquell
punt.
last_seen = load_last_cursor() # resource id stored in your DB
resp = requests.get(
f'https://api.factuarea.com/v1/companies/{COMPANY_ID}/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 one