8 min read

API pagination for AI agents: bounded discovery

API pagination for AI agents needs page budgets, opaque cursors, safe retries, and reports that state exactly what the agent did not inspect.

API pagination for AI agents: bounded discovery

An agent that calls a list endpoint without a page budget is not doing discovery. It is starting an open-ended remote procedure and hoping the bill, rate limit, and result set stay friendly. The opposite mistake is just as damaging: fetch page one, see a plausible answer, and quietly treat it as the whole system.

Pagination changes what an agent can honestly claim. A returned item proves that item exists. It does not prove there are no others. A missing item proves very little unless the agent can show the scope it inspected, the ordering it relied on, and why the server indicated completion.

I have seen agents turn a harmless request such as "find stale access tokens" into thousands of calls because nobody told them where discovery ends. I have also seen a one-page inventory lead to an attempted cleanup of accounts that sat on page two. The remedy is not clever prompt wording. Give the agent a bounded traversal contract and make its final report expose the boundary.

Discovery calls need an explicit budget

Every paginated discovery call needs limits that the agent cannot silently stretch. Define a maximum number of pages, a maximum number of returned records, a deadline, and a rate-limit allowance before the first request. The appropriate values depend on the task, but the existence of limits does not.

Treat discovery as a separate phase from action. During discovery, the agent gathers identifiers and facts. It should not delete, rotate, or modify objects just because one page contains something suspicious. Once it has enough evidence, it can present a scoped plan or begin a separately authorized action phase.

A useful contract has four parts:

  • The endpoint and all filters, including the sort order when the API allows one.
  • A page size and a ceiling for pages or records.
  • A completion condition defined by the API, such as an absent next cursor.
  • An early-stop condition, such as finding a named object or exhausting the allotted budget.

Do not confuse a record cap with a page cap. If the service permits 100 records per page and an agent has a cap of 500 records, five calls may be enough. If the service lowers the effective page size due to filtering or permissions, the same job might need more calls. A good implementation checks both limits after every response.

For example, an agent asked to locate a repository named billing-service could stop as soon as it receives an exact match if the endpoint's filter and ordering make that conclusion safe. An agent asked to identify every repository without branch protection cannot stop at the first match. That task requires a complete enumeration or an explicit partial result.

The word "all" should cost something. An agent may use it only after the traversal reaches the server's terminal condition without hitting its page, record, time, or error budget. If one of those limits fires, the report must say "partial scan" and name the boundary.

Page size is a cost control, not a completeness setting

The limit, per_page, or page_size parameter tells the service how many records to try to return in one response. It does not tell the service how much of the collection the agent needs to inspect. Setting it to the maximum reduces some round trips, but it can make each response expensive enough to time out, exceed a context budget, or hide important details in a mass of irrelevant data.

Start with the smallest page that supports the decision. If an agent needs one exact account, requesting 20 terse records is usually better than requesting 1,000 full objects. If it must build an inventory, use a larger supported page size only after confirming that the endpoint returns a bounded, useful representation.

Fields matter as much as page size. Many APIs offer fields, include, expand, or a similar mechanism. A discovery pass should request identifiers, names, state, timestamps, and the one property that drives the decision. Fetch full detail only for candidates that need inspection. This reduces traffic and gives the model less incidental text to misread.

Use a request shape like this when the provider supports cursor pagination:

GET /v1/projects?state=active&limit=50&sort=id HTTP/1.1
Authorization: Bearer injected-by-gateway
Accept: application/json

And expect an output shape that separates records from continuation state:

{
  "data": [
    {"id": "prj_104", "name": "billing-service", "state": "active"}
  ],
  "next_cursor": "eyJvcmRlciI6ImlkIiwicG9zIjoiMTA0In0"
}

The agent should record that it asked for 50 records and received one. It should not infer completion from the short array. The only useful completion signal in this example is the absence or documented null value of next_cursor.

Avoid an arbitrary rule such as "always use 100." The maximum page size differs by API, and some services count expanded child objects or response bytes against different limits. Request the documented maximum only when the task benefits from it. For broad scans, a medium page size often produces better checkpoints, easier retries, and reports that a human can audit.

Preserve cursors as opaque server state

A cursor is not an offset with a fancy name. The server owns its meaning, and the agent must copy it byte for byte into the next request. It may encode a sort position, a snapshot identifier, a permission boundary, or a signature. The fact that it looks like Base64 does not grant permission to decode, edit, or manufacture it.

The safe loop is simple: request the first page with stable filters and sort parameters, save the cursor returned by that response, then submit the same query with that cursor. Keep every original parameter unless the API documentation explicitly says otherwise. Changing the filter between requests can invalidate the cursor or, worse, produce a plausible but discontinuous result set.

request = { state: "active", limit: 50, sort: "id" }
seen_ids = set()
pages = 0

while pages < 10 and len(seen_ids) < 500:
    response = GET /v1/projects with request
    record response status, request, and response cursor

    for item in response.data:
        if item.id in seen_ids:
            report "duplicate record encountered" with item.id
            stop or apply the provider's documented recovery method
        seen_ids.add(item.id)

    pages += 1
    if response.next_cursor is absent:
        report "complete"
        break

    request.cursor = response.next_cursor
else:
    report "partial: traversal budget reached"

The duplicate check is not decoration. Mutable collections can shift while the agent walks them, and faulty pagination implementations exist. A duplicate does not always mean the service has failed, but it means the agent should stop pretending it has a clean enumeration. If the provider offers a snapshot token, an as_of parameter, or a documented consistency mode, use that for jobs that will lead to a consequential action.

Cursor expiry needs an explicit rule. Some services make cursors short lived; others bind them to a session or invalid them when a query changes. On an expired-cursor response, the agent should preserve the error, then either restart with a stable checkpoint or end the scan as incomplete. It must not skip ahead by guessing a new cursor.

A restart can also create a false sense of completeness. If records changed between the first pass and the restart, a combined list may have gaps or duplicates. Report the restart and the condition used to resume, such as created_at >= last_observed_timestamp. If no stable resume method exists, report that the collection changed during traversal and do not use the result as a deletion list.

Offset pagination drifts when collections change

Offset pagination uses a number such as offset=200&limit=50 or page=5&per_page=50. It is easy to script and easy to explain, which is why it remains common. It also becomes unreliable when new records arrive or old records disappear while the agent moves through the collection.

Suppose page one returns records 1 through 50 in newest-first order. Before the agent requests page two, ten new records arrive. offset=50 now starts after the newly inserted records and overlaps objects the agent already saw. If records instead disappear from page one, the same offset can skip objects that shifted upward. The agent cannot repair this just by deduplicating IDs, because deduplication catches repeats but not omissions.

If the API permits a stable sort, select one with a deterministic tie breaker. created_at alone often is not enough because several records can share a timestamp. A sort such as created_at,id, where the provider documents it, lets the agent record a high-water mark and resume more carefully. If the API gives only an offset and no snapshot or stable ordering, be conservative about conclusions drawn from multiple pages.

For a task that needs a complete answer, use one of these approaches in descending order of confidence:

  1. Ask the service for a snapshot, export job, or cursor that documents a stable view.
  2. Constrain the query to an immutable time range and use a documented stable ordering.
  3. Run a second scan and compare identifiers, then report any disagreement.
  4. Ask a human to approve a narrower, well-defined scope instead of making a broad change.

Do not turn a bad interface into a destructive workflow. An agent can still use offset pages for sampling, locating a particular object, or producing a partial inventory. It should not use an unstable offset scan as proof that every matching credential, project, or user has been found.

Completion must come from the protocol, not a hunch

Gate sensitive list calls
Mark a key for per-call approval when every use during a sensitive scan needs human confirmation.

Different APIs express pagination state in different places. A JSON body may contain next_cursor, has_more, or a URL for the next page. Other APIs use the HTTP Link header. RFC 8288 defines Web Linking and the rel parameter used for relationships such as next; the header provides a relation, not a guarantee that a response body will have a familiar cursor field.

An agent needs an endpoint-specific completion rule. Write it down beside the request definition. For instance: "Complete when next_cursor is null." Or: "Complete when no Link relation has rel="next"." Do not write: "Complete when fewer than 100 records return." That shortcut breaks on filtered pages, permission trimming, service caps, and APIs that deliberately return uneven pages.

A typical Link header can look like this:

Link: </v1/events?limit=100&cursor=a6f3>; rel="next",
      </v1/events?limit=100&cursor=first>; rel="first"

The agent should select only the relation it understands. It must not concatenate the header, assume the first link is a restart-safe checkpoint, or infer that a missing last link means there is no final page. The API's own documentation governs its pagination contract; RFC 8288 only describes how link relations travel in HTTP headers.

Some APIs return has_more: true with an empty page. That sounds absurd until you meet a permission filter, a concurrent deletion, or a delayed index. If the API documents this behavior, continue with the continuation token while the budget permits and record the empty page. If it does not document the behavior, stop and flag an inconsistent pagination response. Continuing forever because has_more remains true is a programming error, not persistence.

Also distinguish a terminal response from a successful HTTP status. A 200 OK says the particular request succeeded. It does not say the collection ended. A 404 may mean an expired cursor, an endpoint mismatch, or a resource that disappeared. Preserve the status, response body, and last cursor in the run record so a human can tell which one occurred.

A partial result needs a boundary statement

An agent should report discovery the way a careful operator would write an incident note: state what it asked, what it observed, and what it did not inspect. Most bad agent reports fail at the last part. They list findings but omit the stopped cursor, cap, or error that makes those findings incomplete.

Use a report format that a human can act on without reconstructing the session:

Scope: GET /v1/projects?state=active&sort=id
Requested page size: 50
Pages fetched: 10
Records received: 487
Completion: partial
Stop reason: page budget reached
Last continuation cursor: eyJvcmRlciI6ImlkIiwicG9zIjoiNTg3In0
Observed finding: 12 projects matched the review rule
Uninspected scope: records after the last continuation cursor
Action taken: none

That final line matters. A discovery run should say whether it changed anything. A human reviewing the report should never have to infer whether the agent merely listed objects or acted on them.

Do not expose a sensitive cursor in a chat transcript if the provider treats it as a bearer capability or if its contents can reveal account structure. Store the exact token in protected run metadata, then present a fingerprint or a redacted prefix in the human-facing report. The agent still needs enough state to resume or audit the traversal, but people do not need continuation tokens scattered through tickets and terminals.

Language controls risk here. "I found no matching records in the first 500 inspected" is accurate. "There are no matching records" is accurate only after complete traversal under a stable enough view. The difference sounds pedantic until a cleanup or compliance decision relies on it.

Rate limits and retries need their own stopping rules

Keep crawl credentials out
Sallyport injects HTTP credentials from its encrypted vault, so the agent never receives the API key.

Pagination amplifies rate-limit mistakes because a single request becomes a loop. An agent that receives 429 Too Many Requests should not respond by hammering the endpoint with the same cursor. Respect Retry-After when the server provides it, account for the wait against the run deadline, and stop when the retry budget is exhausted.

For transient failures such as a timeout or a 5xx response, retry the same page before advancing. If the API supports an idempotency or request identifier for list calls, use it according to its documentation. Never advance to the next cursor after an uncertain response merely because the request may have succeeded. That creates a silent hole.

A bounded retry policy might state:

  • Retry the current page at most twice after transient transport or server failures.
  • Honor Retry-After for rate limits if the remaining deadline allows it.
  • Do not retry authentication or authorization failures without a changed authorization state.
  • Stop on malformed pagination data, a repeated cursor, or an undocumented continuation response.

A repeated cursor deserves special attention. If page three returns the same next_cursor that the agent submitted, continuing can create an infinite loop. Compare each new cursor to the submitted one and to a set of prior cursors. Stop on repetition unless the provider documents a case where repetition is expected, which is rare and should be handled with a provider-specific rule.

The agent should preserve enough response metadata to diagnose a retry without storing secrets. Record status code, request path, selected non-sensitive headers, page sequence number, cursor fingerprint, timestamps, and a digest of the response body if policy permits. Do not paste authorization headers, full bearer tokens, or credential-bearing URLs into logs just because a request failed.

A failure walk-through shows why page one is dangerous

Consider an agent asked to disable every inactive integration older than a stated date. The integrations endpoint defaults to 25 items, sorts by most recently updated, and supplies next_cursor only when more pages exist. The agent fetches the first page, finds three inactive integrations, and disables them. It then reports that it cleaned up inactive integrations.

That report is wrong in two ways. The agent did not inspect every integration, and it took action during discovery. The fact that it found three candidates says nothing about page two onward. Worse, disabling items changes updated_at, which can reorder the collection if the endpoint uses its default sort. The agent has made its own traversal less stable.

A safer run starts with an explicit filter and stable ordering if the API supports it:

GET /v1/integrations?status=inactive&updated_before=2024-01-01&limit=50&sort=id HTTP/1.1

The agent gathers IDs across pages without changing them. It stops only when the server emits no next cursor or when its discovery budget fires. It then reports either a complete candidate set or a partial candidate set. A separate action request can use the gathered IDs, ideally after a human sees the count and scope.

If the list endpoint does not support a stable ordering, the agent should say so. It can still gather candidates, but it should not claim a complete and race-free set. The popular advice to "act as you find items" feels efficient because it saves a second pass. It is wrong for mutable lists where the action changes sort position, eligibility, or permissions.

The same pattern applies to security findings, user accounts, deployment records, and build artifacts. Read first, establish scope, then change. There are exceptions for urgent containment, such as revoking a specifically named compromised credential, but that is not a paginated cleanup job. It is a targeted action with a known identifier.

Put credentials and observability outside the agent

See every page request
The Activity journal records individual HTTP calls, making page sequences and retries available for review.

An agent should not need the API secret merely to paginate. The component that executes HTTP requests can inject credentials, enforce human authorization where required, and record the actual request sequence. This keeps the agent focused on query construction and interpretation rather than handling tokens that would let it run unbounded calls elsewhere.

For teams using Sallyport, HTTP calls can go through its action gateway while credentials stay in the encrypted vault, and the Activity journal records individual calls. That record helps a reviewer compare the agent's claimed page count with the requests that actually ran, but it does not replace page budgets in the agent instructions.

Keep the traversal policy close to the task definition. Specify allowed endpoints, filters, fields, maximum pages, retry behavior, and the required boundary statement. A general-purpose authorization layer cannot decide whether a scan for one repository is complete after one page or whether a compliance inventory needs every page.

Sallyport's session journal and per-call activity trail can make revocation and review practical when an agent run goes off course. Still, the agent must be told to stop on an unrecognized cursor shape, an exhausted budget, or an API response that contradicts the endpoint's documented contract. Logging a bad crawl after the fact is better than no evidence, but it does not undo unnecessary calls or an incorrect action.

Test the traversal against hostile pagination fixtures

Happy-path pagination hides the defects that matter. Before trusting an agent workflow, test it against fixtures that return an empty first page with a cursor, a short page with more results, a duplicated item, a repeated cursor, an expired cursor, and a rate-limit response between two normal pages.

The expected behavior should be boring and specific. The agent continues after an empty page only when the documented continuation state says to continue. It deduplicates or stops on repeated IDs according to the task's consistency requirement. It never invents a cursor after expiry, and it reports partial coverage when a safety limit ends the run.

Use this acceptance table when reviewing a tool call loop:

FixtureExpected result
12 records, no next cursorComplete after one request
12 records, next cursor presentContinue despite the short page
Same cursor returned twiceStop and report a pagination loop
429 with Retry-AfterWait only within deadline and retry current page
Cursor rejected as expiredRestart only through a documented checkpoint, or report incomplete

Inspect the raw calls as well as the final prose. A polished report can conceal a skipped cursor or an extra request after the stop condition. The execution log should show one initial request, each continuation request, any retry of the same cursor, and no calls after the terminal response.

Set a small default budget for exploratory work and require an explicit task change for broad enumeration. That one constraint prevents the two failures that waste the most time: agents that scan forever and agents that quietly confuse the first page with the whole answer.

FAQ

What does pagination mean for an AI agent using an API?

Pagination is the protocol's way of dividing a collection into chunks. An agent must treat each chunk as partial evidence, not as the complete collection, unless it has reached a documented terminal cursor or another explicit completion condition.

How many API pages should an autonomous agent fetch?

It depends on the endpoint's response size, rate limits, and the action the agent intends to take. For discovery, start with a deliberately small page budget and expand only when the task requires broader coverage; do not let the agent decide that every list deserves an unbounded crawl.

Should an agent parse or modify an API cursor?

A cursor is an opaque continuation token issued by the server. Save and replay it exactly as received, including its encoding and case, because parsing, modifying, or reconstructing it can skip records or produce an invalid request.

Does a short API page mean there are no more results?

No. A page that happens to contain fewer records than requested may still have a next cursor, especially when the service applies filtering, access controls, or internal limits. Continue only when the documented continuation signal says more data exists.

Is it better to request the largest page size possible?

Use the maximum supported page size only when the endpoint documentation, payload cost, and rate limit make it sensible. Large pages reduce round trips but can increase timeouts, memory use, and the cost of retrieving fields the task never needed.

What should an agent report after a partial paginated search?

The report should state the endpoint, filter, requested page size, pages fetched, records returned, terminal status, and any cursor or page boundary that stopped the run. If the run stopped early, say that the result is partial and avoid language such as "all" or "none."

What should an agent do when a cursor expires?

Retry the same request with the same cursor if the API documentation permits it. If the service returns an expired or invalid cursor, restart from a stable checkpoint or narrow the query, then report that the result may have changed while the scan restarted.

What is the difference between cursor pagination and offset pagination?

Offset pagination asks for a numeric position such as offset=200; cursor pagination uses a server-provided token tied to the current traversal. Offsets are easy to understand but drift when records are inserted or deleted, while cursors usually behave better for changing collections when the provider supports them correctly.

Can an agent safely retry paginated GET requests?

A GET request may be safe to repeat in HTTP terms, but its list contents can still change between pages. Agents should record the query and observed boundaries, use a server snapshot token when available, and avoid making destructive decisions from a scan they cannot describe as complete.

Can an API gateway solve pagination safety by itself?

A gateway can make the actual HTTP call while keeping credentials outside the agent, but it cannot infer whether a business task needs two pages or two hundred. Put credentials and approvals at the gateway, then encode page budgets, stop conditions, and honest reporting in the agent's operating instructions.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov