Skip to content

How to Normalize Pagination and Error Handling Across 50+ APIs Without Building It Yourself

Maintaining custom API integrations costs 10-20% of your build budget annually. Learn how to normalize pagination, errors, and rate limits across 50+ APIs without writing vendor-specific code.

Roopendra Talekar Roopendra Talekar · · 14 min read
How to Normalize Pagination and Error Handling Across 50+ APIs Without Building It Yourself

You are sitting in sprint planning. A massive enterprise deal is blocked because your product does not sync with the prospect's legacy ERP and custom CRM instance. Your engineering lead glances at the API documentation and says, "I can build those connectors by Friday. We don't need to buy an expensive integration tool just to make a few HTTP requests."

They are not lying. The initial GET request to fetch a contact record is the easy part, but assuming it's just a few API calls is a trap that frequently drains engineering resources.

What they are ignoring is the hidden operational lifecycle of that integration. They are not factoring in the reality that one API uses cursor-based pagination, while the other relies on deep offsets that will eventually time out your database queries. They are not anticipating the moment an API returns an HTTP 200 OK response with a nested {"error": "rate_limit_exceeded"} payload that silently breaks your background workers.

And the scope of the problem is only growing. On average, companies already use 106 SaaS applications, and in large enterprises, that number can exceed 131. Connecting your product to that sprawling ecosystem is no longer a roadmap luxury; it is a baseline requirement for closing enterprise deals and building integrations your B2B sales team actually asks for. But treating integrations as a side project is a trap.

The average annual integration maintenance cost usually runs between 10% and 20% of the initial development cost. That doesn't sound catastrophic until you multiply it. The ongoing support and maintenance for these systems typically adds $5,000 to $10,000 annually — per integration. When your sales team needs Salesforce, HubSpot, Pipedrive, Zoho, and Close supported, those numbers stack fast. At 50 integrations, you are burning through $250,000 to $500,000 per year on pure plumbing. For a deeper dive into the financial math, read Build vs. Buy: The True Cost of Building SaaS Integrations In-House.

This guide breaks down why API pagination and error handling are notoriously difficult to standardize, where legacy unified APIs fall short, and how to architect a zero-code normalization layer that gets your engineers out of the plumbing business entirely.

Why API Pagination is a Nightmare to Standardize

If you want to pull 10,000 employee records from an HRIS, you have to paginate. If you are building integrations in-house, your background workers must understand the specific pagination dialect of every single API you connect to.

Pagination is the mechanism an API uses to split large result sets into smaller, retrievable chunks. It sounds simple, but there are at least six distinct patterns in common use across major SaaS APIs — and most of them are incompatible with each other.

Pagination Style How It Works Common In
Cursor-based API returns an opaque token (e.g., after=eyJpZ...) for the next page HubSpot, Stripe, Slack
Page-number Client requests page N of results (page=2&per_page=100) Many legacy APIs
Offset-based Client specifies how many records to skip (limit=100&offset=200) Older REST APIs, SQL-backed endpoints
Link-header Next/prev URLs embedded in HTTP Link headers per RFC 5988 GitHub, GitLab
Range-based Uses HTTP Range headers or custom range params Some file/storage APIs
Dynamic/custom Vendor-specific logic (e.g., SOQL cursors, GraphQL relay) Salesforce, Linear

If you are building a product that needs to list contacts from five different CRMs, you are writing five different pagination implementations. Each with its own cursor extraction logic, its own "has more pages" detection, and its own query parameter format.

The Deep Pagination Trap

Offset-based pagination is the most intuitive pattern, and the easiest to implement — which is why many legacy APIs use it. But it falls apart at scale.

When you ask a database for LIMIT 100 OFFSET 50000, it cannot magically jump to row 50,001. It must read the first 50,000 rows from disk, sort them, and throw them away just to return the final 100 rows. This is the O(N) "scan and discard" problem — performance degrades linearly as the offset increases.

This is not theoretical. Limit-Offset Pagination has response times that start to increase significantly after page 50 (meaning at data depth 50,000), showing poor scaling for large datasets. When paginating with a large offset, say you want to see page 1000, which shows the results starting at an offset of 100,000, this can quickly exceed in-memory capacity for efficient sorting. Halodoc's engineering team reported that high-traffic API latencies dropped by over 60% by avoiding inefficient OFFSET and full count queries.

Offset pagination degrades linearly as your data grows. Cursor-based pagination stays flat. But the problem for integration builders is that you don't get to choose which pagination style a vendor uses. If Pipedrive uses offset pagination and your customer has a massive Pipedrive instance, you either build workarounds or watch your sync jobs time out.

If your integration layer does not abstract this away, your internal application logic becomes tightly coupled to the performance bottlenecks of third-party systems. Your workers will stall out trying to fetch page 500 from a slow API, causing job queue backups that impact your entire platform.

Real-World Pagination Chaos

Here is a taste of what normalizing pagination looks like across just three CRM APIs:

  • HubSpot: Cursor-based via an after parameter, with the cursor extracted from paging.next.after in the response body
  • Salesforce: Uses SOQL query cursors when searching, but standard REST-style pagination for object listing, with a nextRecordsUrl that is a completely different pattern
  • Pipedrive: Page-number based with start and limit parameters, plus a more_items_in_collection boolean

To give your application a single next_cursor/prev_cursor interface across all three, you need three separate implementations — each handling cursor extraction, "has next page" detection, and query parameter construction differently. Force your engineers to write custom while loops for each of these formats, and you are building a fragile, unscalable system.

404 Reasons Third-Party API Error Handling Will Break Your App

Pagination is a structural problem. Error handling is a semantic disaster.

In a perfect world, every API would follow RFC 7807 (Problem Details for HTTP APIs). In reality, there is no universal standard for how APIs communicate errors. HTTP status codes exist, but vendors use them inconsistently — or ignore them entirely.

When 200 OK Isn't OK

The single most infuriating pattern in third-party APIs is returning HTTP 200 with an error buried in the response body. Slack is the poster child for this. Slack will respond with a 200 — OK status code for almost all requests, including errors. To debug errors, it is necessary to check the response body of the message.

If you send an invalid authentication token, Slack does not return an HTTP 401 Unauthorized. It returns an HTTP 200 OK with a body that looks like this:

{
  "ok": false,
  "error": "invalid_auth"
}

Slack's Web API provides a JSON object with an ok boolean. When ok is false, the error field includes a short, machine-readable string (e.g., invalid_auth or channel_not_found) that identifies the issue.

Your standard HTTP client sees a 200 and calls it a success. Your retry logic never fires. Your monitoring shows green. But the data is not there. Your application proceeds as if the sync completed, leading to silent data corruption and confused users. And this is not just Slack — plenty of APIs, particularly older enterprise ones, follow similar anti-patterns.

The Rate Limit Header Zoo

Rate limiting introduces another layer of chaos. When your AI agent or background worker hits an API too fast, you need to back off. But how do you know how long to wait?

API Rate Limit Signal Retry Guidance
HubSpot HTTP 429 + Retry-After header Seconds until reset
Salesforce HTTP 403 with REQUEST_LIMIT_EXCEEDED in body No standard header
Slack HTTP 429 + Retry-After header Seconds until reset
Zendesk HTTP 429 + Retry-After header Seconds
GitHub HTTP 403 + X-RateLimit-Remaining: 0 X-RateLimit-Reset (Unix timestamp)
Shopify HTTP 429 + Retry-After Also provides X-Shopify-Shop-Api-Call-Limit

Some APIs signal rate limits with a 429. Others use a 403. Some return the retry delay in seconds; others give you a Unix timestamp you need to subtract from Date.now(). Some embed rate limit info in response headers on every request; others only tell you when you have already exceeded the limit. One provider will bury the warning deep inside a nested JSON array in the response body.

If you are handling 50 APIs, you are maintaining 50 different rate limit detection and retry strategies. You are no longer writing business logic. You are writing API babysitting code.

For a deeper exploration of these specific pain points, check out 404 Reasons Third-Party APIs Can't Get Their Errors Straight (And How to Fix It) and our guide on Best Practices for Handling API Rate Limits and Retries Across Multiple Third-Party APIs.

How Unified APIs Solve the Normalization Problem (And Where They Fail)

A unified API sits between your application and multiple third-party services, presenting a single interface regardless of which vendor is behind it. Instead of calling HubSpot's GET /crm/v3/objects/contacts and Salesforce's GET /services/data/v59.0/sobjects/Contact, you call one endpoint — something like GET /unified/crm/contacts — and get back a consistent response shape with standardized pagination cursors.

This is the right architectural pattern for the problem. But not all unified APIs are architected equally. When you look under the hood, many of them pass the hardest normalization problems right back to you.

  • Rigid schemas that hide data. Some unified APIs enforce a rigid unified data model that strips out endpoint-specific fields. If a prospect has highly customized Salesforce objects, a rigid schema will drop that data before it ever reaches your application. Any unified API worth using should preserve the original response alongside the normalized one.
  • Error handling left as an exercise for the reader. Some platforms normalize the happy path beautifully but pass through raw vendor error responses when things go wrong. If your integration with one vendor returns a 200-with-error-body and another returns a proper 4xx, you are back to writing vendor-specific error handling.
  • Rate limit passthrough. If the underlying vendor rate-limits a request, some platforms simply forward the vendor's raw rate limit response. That means your application still needs to understand HubSpot's rate limit format vs. Salesforce's vs. Zendesk's.
  • Code-per-integration architectures. Many platforms maintain separate handler code for each integration behind their unified facade. This means new integration support is slow, edge cases in one provider do not get fixed in others, and the platform accumulates technical debt at the same rate you would if you built it yourself.
Warning

The Abstraction Illusion: If your unified API provider requires you to write custom retry logic for HubSpot's rate limits versus Salesforce's rate limits, you have not bought an abstraction layer. You have just bought an expensive proxy.

The question is not whether to use a unified API. It is whether the one you pick actually normalizes the hard stuff — pagination, errors, rate limits — or just normalizes the data model and leaves you holding the bag for everything else.

The Zero-Code Approach to Normalizing Pagination and Errors

As we explored in our 2026 PM guide to integration solutions, the most effective architecture for this problem treats integration-specific behavior as data, not code. Instead of using the "Strategy Pattern" — where developers write separate adapter classes for HubSpot, Salesforce, and Zendesk — you use an "Interpreter Pattern." Integration behavior is defined entirely as declarative JSON configurations, and a generic engine reads that configuration and executes it. The same code path handles every integration.

This is the approach Truto takes. The entire platform contains zero integration-specific code. No if (provider === 'hubspot'). No switch statements. Instead, data transformation is handled by JSONata expressions stored per integration. Here is how this architecture permanently solves pagination and error normalization.

Six Pagination Strategies, Zero Pagination Code

Truto's proxy layer supports six pagination formats — cursor, page, offset, link-header, range, and dynamic — all driven by a JSON configuration object stored per integration. The engine does not contain a single provider-specific branch.

The configuration for cursor-based pagination on HubSpot looks something like this:

{
  "format": "cursor",
  "config": {
    "cursor_field": "paging.next.after",
    "cursor_query_param": "after"
  }
}

For a page-number API, the configuration shifts:

{
  "format": "page",
  "config": {
    "page_param": "page",
    "page_size_param": "per_page",
    "start_page": 1
  }
}

When your application calls GET /unified/crm/contacts?limit=100, Truto's proxy layer automatically looks up the pagination strategy for that specific provider. If the provider uses Link headers, Truto parses the header, extracts the next URL, and maps it to a standard next_cursor field. If the provider uses offset pagination, Truto calculates the math behind the scenes.

Your application always sees the same response contract:

{
  "result": [{ "id": "123", "name": "Jane Doe", "email": "jane@example.com" }],
  "next_cursor": "eyJpZCI6...",
  "prev_cursor": null,
  "result_count": 25
}

You pass next_cursor back in your next request, and Truto translates it into whatever arcane format the third-party API demands. No matter whether the underlying API uses cursor pagination, offset pagination, or link headers, your client code handles exactly one pagination pattern. Learn more about this in our deep dive on the Declarative Pagination system in Truto Unified Real-time API.

sequenceDiagram
    participant Client
    participant Truto Engine
    participant Third-Party API

    Client->>Truto Engine: GET /unified/crm/contacts?limit=100
    Note over Truto Engine: Load Integration Config<br>(Zero custom code)
    Truto Engine->>Third-Party API: GET /v3/objects/contacts?count=100
    Third-Party API-->>Truto Engine: 200 OK (Native Pagination)
    Note over Truto Engine: Apply JSONata<br>Response Mapping
    Note over Truto Engine: Extract native cursor<br>Map to next_cursor
    Truto Engine-->>Client: Normalized JSON Response<br>with Standard Pagination

Error Expression Engine

To handle the chaos of third-party errors (like Slack's fake 200 OKs), Truto utilizes Error Expressions.

Every integration config can define a JSONata expression that evaluates the raw HTTP response. Before Truto passes a response back to your application, it runs this expression to determine if the response is actually an error, what the true HTTP status code should be, and what the human-readable message is.

An error expression for Slack might look like:

response.ok = false ? {
  "status": response.error = "invalid_auth" ? 401
           : response.error = "channel_not_found" ? 404
           : response.error = "ratelimited" ? 429
           : 400,
  "message": response.error
}

This expression is stored as data in the integration's configuration — not as a code branch in the application. The generic engine evaluates it against every response from Slack and produces a standardized HTTP error when the expression fires.

flowchart TD
    A[Third-Party API Response] --> B{HTTP Status?}
    B -->|200 OK| C{Contains Error Body?}
    C -->|Yes| D[Evaluate JSONata error_expression]
    B -->|4xx / 5xx| D
    D --> E[Extract Real Status Code]
    D --> F[Extract Clean Message]
    E --> G[Standardized HttpError Thrown]
    F --> G
    G --> H[Client Receives Predictable 4xx/5xx]

Because JSONata is Turing-complete for data transformation, it can extract meaningful error messages from deeply nested arrays, map proprietary error codes to standard HTTP statuses, and strip out useless HTML payloads when a provider's load balancer crashes. Your application receives a predictable, standardized error object every single time.

Unified Rate Limit Standardization

Truto applies the same declarative pattern to rate limits. Each integration's configuration defines:

  1. How to detect a rate limit — a JSONata expression that returns true when the response indicates throttling (whether that is HTTP 429, a 403 with a specific body, or a custom header)
  2. How to extract retry timing — an expression that normalizes vendor-specific retry headers into a standard Retry-After value in seconds
  3. How to extract rate limit metadata — an expression that maps vendor headers to standard ratelimit-limit, ratelimit-remaining, and ratelimit-reset headers

The result: every API call that hits a rate limit returns a standard 429 to your application, with a standard Retry-After header. Your background workers only need a single, simple retry function that looks for a 429 status and reads the Retry-After header. Truto handles the translation layer entirely.

flowchart LR
    A["Your App"] -->|Single retry logic| B["Truto"]
    B -->|Config-driven<br>detection| C["HubSpot<br>429 + Retry-After"]
    B -->|Config-driven<br>detection| D["Salesforce<br>403 + body error"]
    B -->|Config-driven<br>detection| E["GitHub<br>403 + X-RateLimit headers"]
    C -->|Standardized| F["429 + Retry-After<br>+ ratelimit-* headers"]
    D -->|Standardized| F
    E -->|Standardized| F
    F --> A

Why This Matters Architecturally

The key insight is that this is not just convenient — it is a fundamentally different maintenance profile. In a code-per-integration architecture, fixing a pagination bug in the Salesforce handler does not help the HubSpot handler. Improving error detection for Slack does not improve it for Zendesk. Every fix is local.

In a declarative, config-driven architecture, improvements to the pagination engine benefit every integration. A more sophisticated error expression evaluator improves error handling across the board. The maintenance burden scales with the number of unique API patterns — not the number of integrations.

The Override Hierarchy: Escape Hatches for Enterprise Edge Cases

The biggest complaint about unified APIs is that they are too rigid. When an enterprise prospect demands that you sync data to a custom object they built in Salesforce, a rigid unified schema will block the deal.

Truto solves this through an override hierarchy. Because mappings are just JSONata expressions stored in a database, they can be overridden at three levels:

  1. Platform Level: The default mapping that works for 90% of use cases.
  2. Environment Level: You can override the mapping for your entire staging or production environment.
  3. Account Level: You can override the mapping for a single, specific customer account.

If one massive enterprise customer needs a custom field extracted from NetSuite, you simply add a JSONata override to their specific integrated_account record. The core codebase does not change. No new deployments are required. The engine just evaluates the customer-specific expression at runtime.

This gives you the speed of a unified API with the extreme flexibility of a custom-built integration.

Trade-offs Worth Acknowledging

No architecture is perfect. A declarative approach has trade-offs:

  • Expression complexity. Some vendor APIs are genuinely bizarre, and the declarative expressions needed to handle their edge cases can get complex. Salesforce's SOQL-based filtering, for example, requires non-trivial JSONata expressions to translate unified filter parameters into valid SOQL.
  • Debugging opacity. When something goes wrong in a declarative pipeline, the error is not always as obvious as a stack trace in a code file. Good observability tooling is essential.
  • Initial mapping effort. Someone still has to write the configuration for each integration. The difference is that this is a data authoring task, not a software engineering task — and it does not require deployments.

The payoff is that these are one-time authoring costs. The ongoing maintenance burden is an order of magnitude lower than maintaining separate code paths for every provider.

Stop Building Plumbing and Start Shipping Features

Every hour your engineers spend debugging why Salesforce returns a 403 instead of a 429 for rate limits, or why your Pipedrive sync dies on page 5,000, is an hour they are not spending on the product your customers actually pay for.

API integration is not a "set it and forget it" job. APIs change. Tools update. Something eventually breaks. Annual maintenance costs can reach $50,000 — $150,000 depending on integration scope, industry, and compliance needs.

The math is straightforward. If your team maintains 20 integrations at $5,000-$10,000 each per year in maintenance costs, you are spending $100,000-$200,000 annually just keeping the lights on. That is two to three senior engineers' worth of time, spent on plumbing instead of product.

The path forward depends on where you are:

  • If you are building integrations from scratch: Don't. Use a unified API that handles pagination, error normalization, and rate limiting at the platform level. Evaluate whether the platform actually normalizes the hard stuff or just the data models.
  • If you already have in-house integrations breaking under maintenance load: Audit which integrations consume the most support tickets and engineering hours. Migrate those first to a platform that treats integration behavior as data, not code.
  • If you are an engineering leader evaluating tools: Ask vendors specifically how they handle Slack's 200-with-error-body pattern, how they standardize rate limit detection across providers, and whether they support all six common pagination formats. The answers will tell you more than any demo.

The integrations your customers need are not going away. The question is whether your engineering team builds and maintains the plumbing, or whether they ship the features that actually win deals.

FAQ

Why does offset pagination fail at scale?
Offset pagination forces the database to scan and skip all previous rows on every request. As the offset grows, query times degrade linearly — response times increase significantly after page 50 (data depth 50,000). Cursor-based pagination maintains constant performance because the database jumps directly to the last-seen record using an indexed column.
How do you normalize API errors when some APIs return 200 OK with errors in the body?
Use an error expression engine that evaluates the full response — not just the HTTP status code. For APIs like Slack that return 200 with an error body, the expression inspects fields like 'ok: false' and maps them to proper HTTP status codes (401, 404, 429, etc.) before your application sees them.
What does it cost to maintain API integrations annually?
Integration maintenance typically costs 10-20% of the initial build cost per year. For individual integrations, that translates to roughly $5,000-$10,000 annually for ongoing support, version upgrades, and compatibility fixes. At 50 integrations, costs can reach $250,000-$500,000 per year.
How can I standardize rate limits across multiple APIs?
Instead of writing provider-specific retry logic, use an integration layer with declarative configuration that detects rate limits (whether HTTP 429, a 403 with a specific body, or a custom header) and normalizes them into a standard HTTP 429 response with a simple Retry-After header in seconds.
What is a unified API and how does it help with pagination and error handling?
A unified API provides a single interface across multiple third-party services, normalizing data models, pagination formats, error responses, and rate limits. Instead of writing 50 different pagination implementations, your application calls one endpoint and receives standardized cursors, HTTP status codes, and retry headers.

More from our Blog