Factuarea API
Conceptes clau

Paginació

Paginació per cursor amb starting_after i ending_before. Sense ?page=, semàntica a l'estil Stripe.

Tots els 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ó.

Paràmetres

ParàmetreTipusPer defecteRangDescripció
limitinteger251100Nombre 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

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
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.

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/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/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ò no hi ha cap paràmetre de desplaçament ?page=N — l'única manera de paginar per una llista és 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(
    '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 one

En aquesta pàgina