Factuarea APIDevelopers

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àmetreTipusPer defecteRangDescripció
limitinteger251–100Nombre d'elements per pàgina.
starting_afterstringnullid (UUID v7)Retorna els elements creats després del recurs el id del qual es passa.
ending_beforestringnullid (UUID v7)Retorna els elements creats abans del recurs el id del qual es passa.
sortstring-createdcamp per recursCamp 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 a limit elements, ordenats per id DESC (equivalent a created_at DESC perquè usem UUID v7).
  • has_more: true si hi ha més elements abans del primer de data (més nous) en paginar amb starting_after, o després de l'últim en paginar amb ending_before.
  • next_cursor: id de l'últim element de data. Passa'l com a starting_after en 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
done

Iterar 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 everything
use 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àmetresEmbolcallOrdre
GET /v1/companies/{company}/pdf-templatesq, state, document_type, purpose, format, page, per_pageitems i metaNom i després id
GET /v1/companies/{company}/pdf-commercial-identitiesq, state, page, per_pageitems i metaNom i després id
GET /v1/companies/{company}/{invoices|quotes|proformas|delivery-notes}/{id}/presentation-historypage, per_pagedata i metaNúmero de captura, descendent
  • page és un enter des de 1 (per defecte 1). per_page és un enter d'1 a 100 (per defecte 25).
  • state és active (per defecte), archived o all.
  • Un page, per_page o filtre fora de rang respon 422; l'API mai no el retalla en silenci.
  • meta sempre é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 és 200 amb un array buit i "total": 0; no és un error.
  • No hi ha has_more, next_cursor ni links: atura't quan page * per_page arribi a total.
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:

RecursCamps sort permesos
invoicescreated, total, number
quotescreated, total, number, valid_until
proformascreated, total, number, valid_until
delivery_notescreated, number, delivery_date
purchase_invoicescreated, total, issued_on, due_on
recurring_invoicescreated, 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

En aquesta pàgina

Et donem un cop de mà?Contactar amb suport