Pagination styles (REST source)
The REST source walks multi-page responses automatically. Set pagination.type
to one of the styles below. Every style has a loop/termination guard so a
misbehaving API can’t loop forever. max_pages is an optional cap across all of
them (unset by default). A pass the cap cuts short keeps only a persist_cursor
bookmark, which names the next unread page. An incremental or windowed pass the
cap cuts short fails the run: a record-derived or window bookmark would skip
the pages it never read, and keeping none would re-read the same pages every run
while each run looked green. Set assume_ordered: true when the feed returns
records in ascending replication_key order and the bookmark is pushed to the
server with a replication_bind — a truncated incremental pass then persists the
largest key it read and the next run’s first request starts at it. The bind
must compare inclusively (>=, e.g. template: "gte|${bookmark}"): rows that
share the last key read can sit on the first unread page, and a strict >
server filter would skip them for good (the re-read rows at that key arrive
again, so pair it with an upsert sink). Without a
replication_bind the combination is refused at load time: the next run would
start at page 1 again and re-read the same pages. assume_ordered does not
apply to windowed passes or across requests: entries.
| Style | Stops when |
|---|---|
None |
after the first page |
Cursor |
the next-token JSONPath is null/absent (or repeats) |
CursorInBody |
the next-token JSONPath is null/absent (or repeats) — for POST-search APIs that take the cursor in the request body |
PageNumber |
a page returns zero records (or repeats the previous page; repeating an earlier, non-adjacent page fails the run, since rows may be unread — set allow_repeated_pages: true with max_pages for a feed whose pages can legitimately repeat) |
Offset |
the offset reaches total (via total_path), or an empty page arrives (a short page alone does not stop it: servers clamp limit; stop_when_short: true opts in for servers that reject an offset past the end) |
OffsetInBody |
like Offset (total, an empty page, or stop_when_short: true) — offset/limit are written into the JSON request body (POST-query APIs) |
RecordFieldCursor |
a short page arrives — keyset paging by the running max/min of a record field |
LinkHeader |
there’s no rel="next" in the Link response header |
NextLinkInBody |
the next-page URL in the body is absent, null, or empty |
Cursor
Section titled “Cursor”pagination: type: Cursor next_token_path: $.meta.next_cursor # JSONPath to the next-page token param_name: starting_after # query param to send it back asCursor in body (POST search)
Section titled “Cursor in body (POST search)”For endpoints that page a POST search body — the next cursor comes back in
the response and must be written back into the request JSON body (e.g. a
CRM POST …/objects/{obj}/search):
source: type: rest config: method: POST body: { limit: 100, sorts: ["hs_lastmodifieddate"] } # base search body records_path: $.results[*] pagination: type: CursorInBody next_token_path: $.paging.next.after # JSONPath to the next cursor body_cursor_field: after # body field to inject it intoThe first request sends body unchanged; each later request adds
body[body_cursor_field] = <cursor>. Pagination stops when the cursor is
null/absent or repeats.
body_cursor_field may be a JSON Pointer for a nested location, so a GraphQL
API reached through the REST source can page by variables.after:
method: POST body: query: "query($first: Int, $after: String) { orders(first: $first, after: $after) { nodes { id } pageInfo { endCursor } } }" variables: { first: 250 } records_path: $.data.orders.nodes[*] pagination: type: CursorInBody next_token_path: $.data.orders.pageInfo.endCursor body_cursor_field: /variables/afterA pointer must resolve to an existing value or to a new key of an existing
object (here variables); intermediate objects and array elements are never
created, and a pointer that does not resolve fails the request. The same
applies to OffsetInBody’s offset_field / limit_field, and to
replication_bind / window binds with into: body + path (see the
state cookbook).
Page number
Section titled “Page number”pagination: type: PageNumber param_name: page start_page: 1 page_size: 500 page_size_param: per_pageOffset / limit
Section titled “Offset / limit”pagination: type: Offset limit: 1000 limit_param: limit offset_param: offset total_path: $.meta.total # optional; enables an exact stopOffset / limit in the body (POST-query APIs)
Section titled “Offset / limit in the body (POST-query APIs)”For POST endpoints that take offset/limit in the JSON request body (not the query string). The offset advances by each page’s record count and paging stops on an empty page or once the offset reaches total_path. A short page alone does not stop it (servers clamp limit) unless stop_when_short: true. When records_path fans out to nested children ($.orders[*].lines[*]) or record_ancestors is set, set rows_path to the server’s row array, or the offset would advance by child records.
pagination: type: OffsetInBody offset_field: offset limit_field: limit limit: 500 stop_when_short: true # only for servers that never cap a page (default false)Report APIs return the whole response as one record (headers plus a rows
array, e.g. a runReport endpoint with records_path: "$"). Counting records would
read every page as short and stop after page 1, so count the rows instead, and
stop at the reported total:
pagination: type: OffsetInBody offset_field: offset limit_field: limit limit: 10000 rows_path: "$.rows" # advance by the rows on each page total_path: "$.rowCount" # stop once offset reaches the totalrows_path counts an array’s elements, 1 for an object, 0 for null or no match
(which ends paging). A missing or non-numeric total_path falls back to the
short-page rule.
Keyset (record-field cursor)
Section titled “Keyset (record-field cursor)”Page by the running max (or min) of a record field — the pattern accounting journal APIs use (offset = max(JournalNumber) of the last page). Stops on a short page.
The cursor is taken from every record a page returns, including ones the incremental filter drops, so a page of only-old rows still moves it. Values compare like replication bookmarks: numbers and numeric strings by value ("59" follows "9"), timestamps chronologically, other strings as text. A full page that leaves the cursor where it was fails the run — more than a page of rows share that value, or the feed isn’t ordered by the field — rather than stopping and silently skipping the rest.
pagination: type: RecordFieldCursor field: JournalNumber into: query # or `body` param: offset agg: max # or `min` page_size: 100 stop_when_short: true inclusive: false # true: the server returns rows >= the cursorBy default the cursor is an exclusive bound, so field must be unique: rows sharing the boundary value at a page cut are never read (a warning fires when a full page ends on such a tie). For a non-unique field like updated_at, filter the server on >= the cursor and set inclusive: true; the rows already emitted at the boundary value are dropped by whole-record fingerprint, so each row is read once.
Resumable cursor & multi-array responses
Section titled “Resumable cursor & multi-array responses”persist_cursor: trueon aCursor/CursorInBodystream saves the terminal cursor as the run’s bookmark (via astate:store) and seeds it into the next run’s first request — so an envelope-cursor feed (e.g. a/transactions/syncendpoint) resumes incrementally instead of re-pulling from the start.records_multiemits several response arrays in one pass (one pagination advance), each stamped with a configurableop_field— pair with a sinkwrite_mode: upsert+delete_markerto route added/modified→upsert and removed→delete from a single sync response.record_ancestorslifts fields from an enclosing array-element ancestor onto records unwrapped from a nestedrecords_path(e.g. keep an event’s envelopeidon each unwrapped object).
See the faucet-source-rest README for the full field reference.
Link header
Section titled “Link header”pagination: type: LinkHeader # follows the RFC 5988 `Link: <…>; rel="next"` headerNext link in body
Section titled “Next link in body”pagination: type: NextLinkInBody next_link_path: $.links.next # JSONPath to the absolute next-page URLA server-given link (either style, or an async-job URL) that points at another
scheme, host or port than base_url is fetched without the source’s
credentials, and an https base never follows a plain-http link. A redirect
to another origin likewise keeps only standard request headers (Accept,
Content-Type, User-Agent, Range, …): auth headers, a shared provider’s
credential and every configured headers: entry stay behind. When the API
really serves pages or results from a second host, list it:
trusted_hosts: [files.example.com, "files.example.com:8443", "*.cdn.example.com"]A link or a redirect to a listed host keeps every header when it keeps the
scheme; a bare host covers any port, host:port only that port.
See also
Section titled “See also”- Authentication — pair pagination with the right auth for your API.
- REST API → BigQuery (incremental) — a full paginated REST pipeline, end to end.