Skip to content

How Unified APIs Normalize Pagination Differences Across REST APIs

Learn how unified APIs abstract offset, page, Link header, and GraphQL pagination into a single unified cursor format, eliminating custom integration code.

Nidhi KN Nidhi KN · · 11 min read
How Unified APIs Normalize Pagination Differences Across REST APIs

Every integration engineer has lived this story. An engineering team agrees to build five native integrations to unblock a major enterprise deal. The initial proof of concept goes smoothly. The developer reads the API documentation, wires up the first third-party API in an afternoon, adds a ?limit=100 query parameter, parses the JSON response, maps it to the internal schema, and ships a demo. Getting the first page of results is easy.

The illusion shatters the moment that integration hits production and attempts to sync an enterprise customer's historical data.

You connect the second provider, and the pagination model is completely different. The first API expected a ?page=2 parameter. The second expects an ?offset=100 parameter. By the fifth integration, you have five bespoke while loops, five sets of edge cases, and a growing suspicion that your codebase is turning into a museum of vendor quirks.

Documenting how unified APIs handle pagination differences and unified cursor formats across REST APIs reveals a fundamental shift in integration architecture. Engineering teams waste hundreds of hours writing custom loops to handle the fractured landscape of third-party API pagination. By normalizing disparate pagination methods into a single unified cursor format, developers can write integration logic once and apply it across dozens of platforms.

This guide breaks down why REST API pagination is harder than it looks, the architectural trade-offs between common pagination methods, and how a unified API abstracts the mess into a single interface your team codes against once.

The Hidden Complexity of REST API Pagination

REST API pagination looks trivial until you try to normalize it across dozens of providers. API pagination standardization is the process of mapping provider-specific pagination mechanisms (like offsets, page numbers, or link headers) into a single, predictable format for the client consuming the data.

If your application requires deep historical syncs—pulling tens of thousands of tickets or CRM contacts—your custom integration code quickly becomes a brittle collection of if/else statements. Look at the reality of the SaaS ecosystem:

  • Salesforce hands you cursor-based pagination with a nextRecordsUrl.
  • HubSpot returns a paging.next.after token.
  • Jira uses startAt offsets.
  • Zendesk is actively migrating from offset to cursor pagination.
  • Shopify serves RFC 5988 Link headers.
  • GitHub caps results per page and rejects deep offsets outright.
  • Linear (and other GraphQL-only APIs) wraps its cursors inside Relay-style edges and pageInfo objects.

The maintenance tax compounds fast. You have to maintain state for each specific provider and handle their unique end-of-list indicators. Some return has_more: false, others return an empty array, and others omit the next token entirely.

Furthermore, each provider ships schema changes on its own timeline and has undocumented behaviors: cursors that silently expire after 15 minutes, page tokens that return the same record twice at the boundary, or total_count fields that lie because they were computed against a stale cache. Your engineers turn into full-time pagination archaeologists.

As noted in our guide on how unified APIs handle pagination differences across REST APIs, fetching page one is a solved problem. Fetching page 400 reliably across 50 providers is not. Offloading this logic to a unified API layer is often the only sustainable path for a scaling engineering team.

Cursor vs. Offset: Why Unified APIs Standardize on Cursors

Before looking at how a unified layer normalizes pagination, it helps to understand why the industry is coalescing around cursors.

Unified cursor formats are abstracted string tokens generated by a unified API proxy. These tokens encode the necessary state (such as an upstream offset value or page number) required to fetch the next batch of records, allowing the client to use a consistent ?cursor= parameter regardless of the upstream provider's actual pagination method.

To understand why unified APIs default to cursor formats, you have to understand the architectural flaws of offset pagination.

Offset pagination uses ?offset=1000&limit=100 semantics. It relies on skipping a specific number of records in the database query. It is easy to reason about and easy to jump to arbitrary pages, but it is terrible under load. If a client requests ?limit=100&offset=10000, the database must scan, fetch, and discard 10,000 rows before returning the next 100. This causes severe database performance issues on deep pages; page 500 costs 500x more database CPU than page 1.

Offset pagination also suffers from data shifting. If a new record is inserted at the top of the list while a client is paginating, all subsequent records shift down. The client will end up processing duplicate records across pages, which causes corrupted or dropped rows during a full sync.

Cursor pagination solves both problems. A cursor is an opaque pointer to a specific row in the database, derived from a stable sort key (often an encoded timestamp or UUID). When the server receives the cursor, it can seek directly to the next record in roughly O(log n) or O(1) time depending on the index layout (WHERE id > cursor_value LIMIT 100). Records that appear or disappear during iteration do not corrupt the cursor, completely eliminating data-shifting bugs.

The evidence for this architectural shift is everywhere. GitHub explicitly limits API results to 100 items per page because returning larger sets or deep offsets causes significant performance degradation on their backend. Twitter relies on cursor-based pagination to handle billions of tweets efficiently. Contentful recently migrated all of its content APIs to cursor-based pagination to provide a more robust foundation for developers. Stripe, Slack, and HubSpot have been cursor-first for years.

This matters for a unified API for one simple reason: if your abstraction normalizes downward to offsets, you inherit the worst pagination behavior of every provider you support. Normalize upward to cursors, and you inherit the best.

Quick Pagination Method Comparison:

Method Deep-page cost Stability under writes Random access Common in
Offset O(n) Poor Yes Legacy REST APIs
Page number O(n) Poor Yes Older CRMs, ticketing systems
Cursor O(log n) / O(1) Strong No Modern APIs, GraphQL
Link header Backend dependent Backend dependent No GitHub, Shopify

How Unified APIs Handle Pagination Differences

A well-designed unified API treats pagination as a declarative mapping problem, not a coding problem. Unified APIs eliminate the need for developers to write separate pagination handlers for every third-party connector. By abstracting away vendor-specific logic, a single generic executor walks the pages at runtime.

Behind the scenes, the unified API acts as a translation layer. When you query a unified /contacts endpoint, the platform intercepts your request, translates your standardized pagination parameters into the provider's expected format, executes the request, and maps the provider's next-page indicator back into a standardized cursor for your next request.

Truto handles this through a declarative pagination system that natively supports six distinct formats out of the box, configurable at both the integration level and per resource method:

1. Cursor Pagination

This is a direct mapping. The upstream API returns a cursor token (e.g., from a configured cursor_field like meta.next_token) in the response body, a header, or a query response. The unified API extracts it and passes it back in subsequent requests via a cursor_query_param, body payload, or header.

2. Page Pagination

Classic page-number based APIs expect parameters like ?page=2. The unified API proxy intercepts your generic cursor, decodes it to determine the current page integer, increments it based on the configured page_increment (for APIs whose pages start at 0 vs 1), and sends the correct page_param to the upstream provider.

3. Offset Pagination

Similar to page pagination, the unified API tracks the skip count. It decodes your cursor to find the current offset, adds the configured limit_param value, and sends the new offset_param to the upstream API.

Following RFC 5988 (and RFC 8288), some APIs return navigation URLs in the HTTP response headers. The unified API parses the Link header, extracts the URL tagged with rel="next", and encodes the relevant query parameters from that URL into the standardized cursor returned to your application.

5. Range Pagination

For APIs that paginate by record ranges (using HTTP Range headers or custom range parameters), the proxy calculates the next byte or record range and maps it accordingly.

6. Dynamic Pagination

Some APIs defy standard categorization. To handle complex, non-standard API responses, unified platforms utilize dynamic pagination strategies. Truto powers this with JSONata expressions. The configuration includes declarative expressions for computing the next cursor, determining if there is a next page, and applying pagination parameters to the outbound request. This is the escape hatch for APIs whose pagination cannot be described by the first five methods.

All six formats normalize into the same downstream contract: your code receives a page of records plus a single unified cursor. You pass that cursor back on the next request. You never touch the upstream token format.

Here is what this looks like in practice:

# First page - no cursor required
curl "https://api.truto.one/unified/crm/contacts?limit=100" \
  -H "x-integrated-account-id: acc_123" \
  -H "Authorization: Bearer $TRUTO_TOKEN"
 
# The unified response includes a standardized next_cursor
{
  "result": [ /* 100 contacts mapped to unified schema */ ],
  "next_cursor": "eyJvZmZzZXQiOjEwMH0"
}
 
# Next page - simply pass the unified cursor back
curl "https://api.truto.one/unified/crm/contacts?limit=100&cursor=eyJvZmZzZXQiOjEwMH0" \
  -H "x-integrated-account-id: acc_123" \
  -H "Authorization: Bearer $TRUTO_TOKEN"

The exact same call shape works whether the underlying provider is Salesforce (nextRecordsUrl), HubSpot (paging.next.after), Zendesk (offset), or GitHub (Link headers). The behavior is described in configuration; the code path is completely generic.

The internal architecture of this execution pipeline looks like this:

flowchart TD
    A["Client request<br>with unified cursor"] --> B[Generic Pagination Executor]
    B --> C{Evaluate Strategy}
    C -->|cursor| D["Inject token<br>into query or body"]
    C -->|page| E[Compute next page integer]
    C -->|offset| F[Compute next offset skip]
    C -->|link_header| G["Follow Link rel=next"]
    C -->|range| H[Set HTTP Range header]
    C -->|dynamic| I[Evaluate JSONata Expression]
    D --> J[Upstream API Request]
    E --> J
    F --> J
    G --> J
    H --> J
    I --> J
    J --> K["Extract records<br>and next-page marker"]
    K --> L["Encode & Emit unified cursor<br>back to Caller"]

This architecture guarantees that your application only ever interacts with a single parameter (?cursor=) and a single response field (next_cursor), completely isolating your codebase from upstream API changes.

Handling Edge Cases: GraphQL, Rate Limits, and Dynamic Pagination

Abstracting pagination is only half the battle. The real test of any pagination abstraction is not the happy path. It is the long tail of enterprise provider quirks that would otherwise fill your codebase with special cases.

Mapping GraphQL Edges to RESTful CRUD

Modern tools like Linear and Monday expose GraphQL APIs, but your application almost certainly wants a REST CRUD contract. GraphQL paginates using Relay-style connections, where records are wrapped in edges and node objects, and pagination state is stored in a pageInfo object.

To keep the developer experience consistent, a unified API must bridge this gap by exposing GraphQL-backed integrations as standard RESTful CRUD resources. The declarative mapping layer issues a GraphQL query with variables, automatically extracts the endCursor from the pageInfo block, and unwraps the nested node objects into a flat JSON array. To your code, GET /unified/pm/issues behaves identically whether the upstream is REST or GraphQL. When your application sends the next unified cursor, the unified API injects it into the GraphQL query variables as the after argument.

Transparent Rate Limit Handling

A common misconception is that a unified API should automatically retry requests when an upstream provider rate limits the connection. Be skeptical of any unified API that claims to make rate limits "disappear."

Hiding HTTP 429 (Too Many Requests) errors behind automatic retries is a dangerous anti-pattern. Absorbing 429s inside a unified layer creates worse problems than it solves. If a unified API holds a connection open for 30 seconds while applying exponential backoff, it will exhaust your application's connection pool and cause cascading timeouts. Automatic retries turn a fast failure into a slow one, blow past your ingestion SLOs, and mask real capacity problems. They also break idempotency guarantees for write operations.

Instead, a well-architected unified API transparently passes HTTP 429 errors straight through to the caller. What it does normalize is the surrounding observability signals. Truto normalizes upstream rate limit information into standardized IETF headers, regardless of how the provider originally spelled them (X-RateLimit-Reset, Retry-After, etc.):

HTTP/1.1 429 Too Many Requests
ratelimit-limit: 100
ratelimit-remaining: 0
ratelimit-reset: 1678901234

By normalizing the headers, your application can implement a single, reliable circuit breaker and backoff strategy. You know exactly when the limit resets, and you can let the client decide the retry policy based on its own budget, priority, and idempotency semantics.

Handling Unpredictable API Quirks

Even with standard formats, APIs in the wild will surprise you. Some APIs return a cursor but require you to pass it back in the request POST body instead of the query string. Others require you to base64 encode the cursor before sending it back. Some hide a continuation_token inside a deeply nested meta.response.cursor object.

A declarative configuration system handles these quirks through simple toggles (e.g., cursor_in_body: true) or JSONata transformations, ensuring that no custom imperative code is introduced into the unified proxy layer. You describe how to compute the next cursor, how to detect end-of-pages, and how to inject paging parameters using expressions. No integration-specific code. No forked executor.

Zero Integration-Specific Code

The true value of a unified API architecture is not just the initial speed of development. It is the long-term elimination of maintenance debt. The goal is to make onboarding the 51st integration cost the exact same amount of engineering effort as onboarding the 5th.

When a third-party API deprecates offset pagination and forces a migration to cursors, your engineering team does not have to touch your application code. The unified API provider updates the declarative mapping configuration for that specific integration. Your application continues sending its standard ?cursor= parameter, completely unaware that the underlying translation layer has shifted from offset logic to cursor logic.

When pagination is defined declaratively, adding a new provider means writing a configuration row, not shipping code. There is no new branch in a runtime switch statement. There is no new executor path. The same generic pipeline that walked HubSpot's paging.next.after yesterday walks Pipedrive's start offset today, because both are described in the same schema.

That is the actual promise of a unified API: not just "we hide the messy stuff," but "we make the messy stuff data instead of code." Data can be reviewed in a pull request. Data can be tested in isolation. Data does not accrue compounding maintenance debt the way per-provider while loops do.

By relying on a generic execution pipeline driven by declarative configurations, you remove integration-specific code from your codebase entirely. You stop writing custom while loops, you stop parsing vendor-specific rate limit headers, and you stop debugging data-shifting bugs on deep historical syncs. For a broader treatment of this pattern, our guide to normalizing pagination and error handling across 50+ APIs walks through the trade-offs in more depth.

Focus your engineering resources on building your core product, and let a unified architecture handle the chaotic reality of third-party APIs.

Where to go from here

  • Audit which of your current integrations use offset, cursor, page-number, or Link header pagination. The distribution will surprise you.
  • Identify the APIs whose pagination format does not fit any standard bucket. Those are the ones your custom code was written for, and the ones a dynamic strategy would easily eliminate.
  • Prototype the same integration against a declarative pagination model. Measure the code delta and the time to add your next integration.
  • If you are evaluating third-party platforms to handle this abstraction, review these 10 questions to ask your unified API provider to ensure their architecture aligns with your scaling needs.

FAQ

How do unified APIs handle pagination differences across different REST APIs?
A unified API describes each upstream pagination format (cursor, page number, offset, Link header, range, or dynamic) as declarative configuration. A generic proxy layer intercepts client requests, translates standardized parameters into the provider-specific format, and maps the provider's next-page indicator back into a single unified cursor string.
What is a unified API pagination mapping cursor format?
It is a single abstracted string token generated by the unified API layer that encodes whatever the upstream provider needs to fetch the next page—an offset, a page number, a Link header URL, or an upstream cursor. Callers pass it back unchanged, and the layer translates it to the correct upstream format.
Why do unified APIs standardize on cursor pagination instead of offsets?
Cursor pagination scales in roughly O(log n) or O(1) time and stays consistent when records are added or deleted mid-sync. Offset pagination degrades linearly on deep pages and drops or duplicates rows under concurrent writes, which is why major providers like GitHub and Contentful have moved away from it.
How does a unified API handle GraphQL pagination alongside REST?
GraphQL-backed integrations are exposed as RESTful CRUD resources. The unified executor issues the underlying GraphQL query, extracts Relay-style edges and the pageInfo.endCursor, and returns the same unified cursor your REST integrations use, keeping the developer experience completely consistent.
Do unified APIs automatically retry rate-limited pagination requests?
Well-architected unified APIs do not automatically retry HTTP 429 errors, as this can exhaust client connection pools and break idempotency. Instead, they pass the error through and normalize the rate limit metadata into standard IETF headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset) so the client can handle backoff.

More from our Blog