Factuarea API
Core concepts

Pagination

Cursor pagination with starting_after and ending_before. No ?page=, Stripe-style semantics.

Every list endpoint in the public API uses cursor pagination. Same semantics as Stripe / Linear: you paginate by an opaque identifier (the resource id), not by page number. This guarantees stable results even when new resources are created during iteration.

Parameters

ParameterTypeDefaultRangeDescription
limitinteger251100Number of items per page.
starting_afterstringnullid (UUID v7)Returns items created after the resource whose id is passed.
ending_beforestringnullid (UUID v7)Returns items created before the resource whose id is passed.
sortstring-createdper-resource fieldSort field; - prefix for descending (e.g. -total). See Order.

starting_after and ending_before are mutually exclusive. Sending both in the same request responds 422 with a invalid_request_error envelope.

Response shape

{
  "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 of up to limit items, ordered by id DESC (equivalent to created_at DESC because we use UUID v7).
  • has_more: true if there are more items before the first one in data (newer) when paginating with starting_after, or after the last when paginating with ending_before.
  • next_cursor: id of the last item in data. Pass it as starting_after in the next request to move forward.

When there are no more items has_more is false and next_cursor is null.

Iterate all results

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

Iterate with the official SDK

The TypeScript and PHP SDKs hide the cursor entirely: list methods return an iterator that walks every page for you.

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;
}

See SDKs › Paginating with the SDK for the full surface.

Order

Pass ?sort=<field> to order a list. A bare field sorts ascending; a - prefix sorts descending (e.g. ?sort=-total). When omitted, the default is -created — equivalent to id DESC, a stable total order because we use UUID v7 (which encodes a timestamp in the high 48 bits, so "most recently created first" needs no extra created_at column).

The cursor (starting_after / ending_before) keeps working with the order you choose: the chosen field is the primary sort and the resource id is a stable secondary sort, so pagination stays deterministic even when rows share the same value (Stripe-style).

Allowed sort fields are scoped per resource — sending an unsupported field responds 422 with a invalid_request_error envelope:

ResourceAllowed sort fields
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
# Invoices, highest total first
curl -G https://api.factuarea.com/v1/invoices \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  --data-urlencode "sort=-total"

# Quotes by expiry date, soonest first
curl -G https://api.factuarea.com/v1/quotes \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  --data-urlencode "sort=valid_until"

Why not ?page=?

Numeric pages have problems when the dataset changes during iteration:

  • Creating a resource between pages → duplicates rows.
  • Deleting a resource between pages → drops rows.
  • COUNT(*) is expensive past a few thousand rows.

Cursor pagination with UUID v7 eliminates both: the cursor points to a stable position in time, not a variable offset.

That's why there is no ?page=N offset parameter — the only way to page through a list is the starting_after / ending_before cursor. An invalid cursor value responds 422 with a invalid_request_error envelope (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_..."
  }
}

Use case: fetch only new results

If your integration polls every N minutes, save the next_cursor (the newest id you've seen) between pollings. On the next pass use ending_before=<saved_cursor> to fetch only items newer than that point.

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

On this page