Skip to main content
Large list endpoints in the Breadbox API (GET /transactions and GET /rules) use cursor-based pagination rather than offset-based pagination. You navigate through results by passing an opaque cursor token from one response into the next request, rather than specifying a page number or offset. Cursor pagination is stable: if new transactions are synced while you are iterating through pages, you will not see duplicates or skip records. This makes it the right choice for transaction data, which can be modified by background syncs at any time. Smaller list endpoints (GET /accounts, GET /categories, GET /connections, GET /tags, GET /users, GET /reports) are not paginated — they return every matching record in a single response (either as a top-level array or wrapped in a resource-named envelope like {"tags": [...]}).

Response envelope

Each paginated endpoint wraps its current page inside a resource-named array alongside next_cursor and has_more. The array key is different per endpoint (transactions or rules) — the pagination fields are identical.
{
  "transactions": [ ... ],
  "next_cursor": "eyJkYXRlIjoiMjAyNS0xMi0xNSIsImlkIjoiYWJjZGVmIn0",
  "has_more": true
}
transactions | rules
array
required
The current page of results, keyed by resource name. May be an empty array when no records match the filters.
next_cursor
string
required
An opaque, base64url-encoded token. Pass this value as the cursor query parameter in your next request to retrieve the following page. An empty string when there are no more pages.
has_more
boolean
required
true if additional pages exist beyond the current one. When has_more is false, you have retrieved all matching records.

Request parameters

cursor
string
Opaque pagination cursor from a previous response’s next_cursor field. Omit this parameter to fetch the first page. Do not attempt to construct or parse cursors manually — treat them as opaque strings.
limit
integer
default:"50"
Number of records to return per page. Minimum 1, maximum 500. The default for most endpoints is 50, but check each endpoint’s documentation for its specific default.

How cursors work

Internally, a cursor encodes a (date, id) pair that lets the database resume from the exact position where the previous page ended. This is why cursor pagination only works with the default date sort — if you switch to sorting by amount or name, pagination is not supported and you must fetch all results in a single request (using a high limit). If you provide a cursor that is malformed or refers to a record that no longer exists, the API returns a 400 error with code INVALID_CURSOR. In that case, restart pagination from the first page.

Fetching all pages

To retrieve every record matching a set of filters, keep fetching pages until has_more is false:
curl -H "X-API-Key: bb_key" \
  "http://localhost:8080/api/v1/transactions?limit=100&start_date=2025-01-01"
A typical pagination loop in pseudocode:
cursor = None
all_transactions = []

while True:
    params = {"limit": 500, "start_date": "2025-01-01"}
    if cursor:
        params["cursor"] = cursor

    response = get("/api/v1/transactions", params=params)
    all_transactions.extend(response["transactions"])

    if not response["has_more"]:
        break

    cursor = response["next_cursor"]

Tips

  • Use the maximum limit of 500 when you need to export all data — fewer round trips means faster completion.
  • Do not cache cursors across sessions. Cursors may become invalid after schema migrations or server restarts. Always start fresh from the first page in new sessions.
  • Filters must be consistent across pages. If you change filter parameters mid-pagination (for example, changing start_date), the cursor will produce incorrect results or a 400 error. Keep all parameters constant and only change cursor between requests.
  • The /transactions/count endpoint lets you know how many records a filter returns before you begin paginating — useful for progress tracking in long exports.
Last modified on May 27, 2026