Skip to content
Open beta — everything’s free right now, and your rate is locked when it ends.

Paging records over the API: bookmarks, not page numbers

The records list pages by an opaque bookmark: pass back what the last response gave you until it stops giving one. The loop in bash and Node, the page_size bounds, and why there is no page 2.

Updated August 2026

A page of records comes with a bookmark when there are more. Pass it back verbatim to continue; when a response has no bookmark, you have everything. That is the entire protocol — two parameters, one loop.

The two parameters

  • page_size= — how many records per page: 1 to 1,000, default 100. Out of bounds is a 400: "page_size" must be a whole number between 1 and 1000.
  • bookmark= — the value from the previous response. Opaque: don't parse it, don't store it long-term, don't mint your own. A garbage value is a 400 (Invalid bookmark.), and a bookmark taken under one sort is refused under another (Bookmark does not match the sort.) — the bookmark encodes where you are in that ordering. That includes changing view= between pages, since a view brings its own sort: finish a walk before you switch, or start the new one without a bookmark.

Walking the 14-record demo Plants table at page_size=6 gives three responses: six records with a bookmark, six with a bookmark, then two with none. The last page omits the key; that absence is the whole end signal.

The loop in bash

TABLE="tbl_jukzu86nxtcj"
BOOKMARK=""
while : ; do
  if [ -z "$BOOKMARK" ]; then
    PAGE=$(curl -s "https://tabladb.com/api/tables/$TABLE/records?page_size=100" \
      -H "Authorization: Bearer key_xxxx.your-secret")
  else
    PAGE=$(curl -s -G "https://tabladb.com/api/tables/$TABLE/records" \
      -H "Authorization: Bearer key_xxxx.your-secret" \
      --data-urlencode "page_size=100" --data-urlencode "bookmark=$BOOKMARK")
  fi
  echo "$PAGE" | jq -r '.records[].id'      # do your work here
  BOOKMARK=$(echo "$PAGE" | jq -r '.bookmark // empty')
  [ -z "$BOOKMARK" ] && break
done

The loop in Node

const KEY = "key_xxxx.your-secret";
const base = "https://tabladb.com/api/tables/tbl_jukzu86nxtcj/records";

async function allRecords() {
  const records = [];
  let bookmark;
  do {
    const url = new URL(base);
    url.searchParams.set("page_size", "100");
    if (bookmark) url.searchParams.set("bookmark", bookmark);
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${KEY}` },
    });
    if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
    const page = await res.json();
    records.push(...page.records);
    bookmark = page.bookmark; // undefined on the last page
  } while (bookmark);
  return records;
}

Both loops above were run as printed (against the demo table) and returned every record exactly once. Filters, sort and search apply per request and carry through the walk — set them once and repeat them on every page call along with the bookmark (filtering).

Why there are no page numbers

?page=7 means "skip 600 records, then read". If someone inserts a record while you walk, page 7 re-serves a record page 6 already gave you; if someone deletes one, a record silently falls between pages. Offset paging is a position in a list that moves under you.

A bookmark is not a position — it records the last row you actually saw (its sort values, under the ordering you asked for). The next page resumes strictly after that row, so inserts and deletes elsewhere in the table cannot make the walk skip or repeat a record. The one honest caveat: a record whose own sort value changes mid-walk moves within the ordering, so it can still be missed or met twice. The longer version of this argument, with both schemes drawn out, is in keyset vs offset pagination.

The practical consequences:

  • There is no "jump to page 40". You walk. For "the newest 20", you don't want page 40 anyway — you want sort=-Placed&page_size=20, one request.
  • A bookmark is a continuation, not an address. It survives inserts and deletes, but don't bookmark a bookmark for next week; start the walk fresh.
  • The screen does the same thing. The grid pages records through this exact mechanism, so a million-record table costs the API what it costs the screen: one page at a time.

The full parameter list — fields=, cell_format=, key_by= and the rest — is at /docs/api.

Tabla is the database we build these on.

A no-code database with real Postgres underneath: every feature on every plan, a million records per database, and your whole database back out in one file, any day.

More guides