Skip to content

404 Reasons Third-Party APIs Can't Get Their Errors Straight (And How to Fix It)

Third-party API errors are wildly inconsistent. Learn how to normalize 200 OK errors, missing rate limit headers, and HTML responses without writing code.

Uday Gajavalli Uday Gajavalli · · 6 min read
404 Reasons Third-Party APIs Can't Get Their Errors Straight (And How to Fix It)

If you have built B2B SaaS integrations, you know the sinking feeling of debugging a silent failure, only to discover a third-party API returned a 200 OK with an error message buried deep inside a JSON payload.

API error normalization is the process of translating disparate, provider-specific error responses into standardized HTTP semantics. It allows your application to handle a Slack authentication failure, a Salesforce rate limit, and a GraphQL partial failure using the exact same logic, eliminating the need for integration-specific error handling code.

When you integrate with dozens of SaaS platforms, you quickly realize that software vendors fundamentally disagree on how HTTP works. Your codebase ends up absorbing this domain complexity, turning your engineering team into an expensive API maintenance crew.

Why Standardizing API Errors is a Nightmare

The reality of third-party APIs is that error formats are wildly inconsistent. Building a reliable integration means handling edge cases that directly violate standard HTTP conventions.

Here are the most common API error anti-patterns developers face:

  • The 200 OK Error Trap: GraphQL APIs are notorious for this. Even when a query fails entirely, GraphQL endpoints almost always return a 200 OK status. The actual error is hidden inside an errors array in the response body. Standard monitoring tools that watch for 5xx responses are completely blind to these failures. Slack does something similar, returning 200 OK with { "ok": false, "error": "invalid_auth" }.
  • Semantically Incorrect Status Codes: Freshdesk returns a 429 Too Many Requests when a customer's subscription plan doesn't include API access. A real rate limit returns a 429 with a Retry-After header. Without that header, the error actually means 402 Payment Required, but the API forces you to guess.
  • Enterprise Authentication Edge Cases: Salesforce REST APIs will throw 401 Unauthorized for expired tokens and 403 Forbidden for missing permissions. Add in their strict daily API limits, and your integration requires sophisticated exponential backoff and proactive token refresh logic just to stay alive.
  • Non-JSON Responses: When legacy APIs fail catastrophically, they often abandon JSON entirely, returning plain text or HTML error pages that break standard JSON parsers.

The Brute Force Approach (And Why It Fails)

Most engineering teams try to solve this with the Adapter Pattern. You write a specific error handler for HubSpot, another for Salesforce, and another for Zendesk.

// The "Brute Force" Adapter Pattern
if (provider === 'slack' && response.data.ok === false) {
  throw new AuthError(response.data.error);
} else if (provider === 'freshdesk' && response.status === 429) {
  if (!response.headers.get('retry-after')) {
    throw new PaymentRequiredError('API access not available on this plan');
  }
}

This scales linearly with pain. You are hardcoding vendor quirks into your core business logic. Industry data shows that maintaining a single custom API integration costs engineering teams between $50,000 and $150,000 annually. When you multiply that by 50 integrations, the maintenance burden cripples your product roadmap.

Traditional unified APIs suffer from the exact same problem behind the scenes. They maintain massive if/else blocks for every provider, which is why schema normalization is the hardest problem in SaaS integrations.

Truto's Solution: JSONata Error Expressions

At Truto, we refuse to write integration-specific code. Our entire platform uses a programmable integration layer based on the Interpreter Pattern. Zero-code architecture is the only way to scale B2B integrations reliably.

Instead of writing if/else blocks, Truto uses error expressions—declarative JSONata strings stored directly in the integration's configuration.

When a third-party API responds, the expression evaluates the raw response (status, headers, and body) and maps it to a structured object containing the correct HTTP status and a human-readable message.

Info

What is an Error Expression? A JSONata expression configured per-integration that evaluates a third-party API response and produces a standardized ErrorExpressionResult object. This normalizes wildly different vendor errors into predictable HTTP semantics before they reach your application.

The expression must evaluate to a strict schema:

type ErrorExpressionResult = {
  status: number;          // Standard HTTP status code (e.g., 401, 403, 429)
  message: string;         // Human-readable error message
  headers?: Record<string, string>; // Optional normalized headers
  result?: any;            // Used to rewrite the response body if needed
}

Real-World Error Normalization Patterns

Let's look at how this declarative approach handles the worst API offenders without a single line of application code.

Pattern 1: Body-Based Errors (The 200 OK Problem)

For APIs like Slack or GraphQL that return 200 OK for failures, we use JSONata to inspect the body and map the internal error code to a proper HTTP status.

Here is the actual error expression used for Slack:

$not(data.ok) ? {
  "status": $mapValues(data.error, {
    "invalid_auth": 401,
    "missing_scope": 403,
    "ratelimited": 429,
    "internal_error": 500
  }),
  "message": data.error
}

If data.ok is true, the expression falls through, and the system treats it as a success. If false, it intercepts the 200 OK and rewrites it into a proper 401 or 429 error before it ever reaches your application.

Pattern 2: Status Code Remapping

When HighLevel returns a 401 for a scope-based authorization failure, it triggers the wrong internal logic (authentication vs. authorization). We remap it to a 403:

status = 401 and $contains(data.message, "not authorized for this scope") ? {
  "status": 403,
  "message": data.message
}

For the Freshdesk plan-limit issue mentioned earlier, the expression detects the absence of the retry-after header and corrects the status code:

status = 429 and $not($exists(headers.`retry-after`)) ? {
  "status": 402,
  "message": "API access is not available on this plan."
}

Pattern 3: String and HTML Responses

Sometimes APIs fail so badly they return plain text. The expression checks the $type() of the data and matches a regular expression to extract meaning from the chaos.

Here is how we handle Xero's plain-text permission errors:

$type(data) = "string" and $match(data, /You don't have permission to access/i) ? {
  "status": 403,
  "message": "Authorization error: Missing required permissions."
}

How Normalized Errors Power Self-Healing Integrations

Standardizing errors isn't just about clean server logs; it is the foundation of a resilient integration architecture. When every API error looks the same, you can build automated, self-healing systems on top of them.

Reauth Detection

When an error expression evaluates to a 401, Truto flags the resulting exception with a truto_is_remote_error: true property. This distinguishes a third-party authentication failure from a local one.

Truto proactively refreshes OAuth tokens before they expire. But if a token is manually revoked by the user, the API will return a 401. The system intercepts this remote error flag, pauses any active sync jobs, marks the account as needing re-authentication, and fires a webhook. Instead of blindly retrying a dead token, it cleanly halts operations so you can prompt the user to reconnect.

Standardized Rate Limiting

Rate limit implementations vary wildly. Freshdesk might use one format, while Shopify uses another. Alongside error expressions, Truto uses a separate Rate Limit Expressions configuration block with three dedicated JSONata expressions:

  • is_rate_limited: Evaluates the response to determine if a limit was hit. If an API returns a 200 OK with a custom rate limit header, this expression catches it and forces a standard 429 Too Many Requests response.
  • retry_after_header_expression: Extracts the provider's specific retry logic (whether it's seconds or an HTTP-date) and normalizes it into a standard Retry-After header in seconds.
  • rate_limit_header_expression: Maps custom headers (like X-RateLimit-Remaining) into standard ratelimit-limit, ratelimit-remaining, and ratelimit-reset headers.

These standardized headers are attached to both 429 errors and successful 2xx responses, so clients can always track their quota. Your application only ever has to read Retry-After and the standard ratelimit-* headers to manage its queue, regardless of which CRM you are calling.

Actionable Error Insights

When a 403 Forbidden is normalized, Truto automatically generates an error insight. The system compares the required OAuth scopes against the granted scopes stored in the account context, instantly diagnosing permission mismatches. Instead of a generic "Access Denied," you get exactly which scope is missing.

Stop Writing Error Handlers

Every hour your engineers spend writing try/catch blocks for undocumented API errors is an hour they aren't building your core product. Third-party APIs will always have quirks, edge cases, and flat-out broken implementations. Your codebase shouldn't have to absorb them.

By treating error normalization as configuration rather than code, you decouple your application from vendor instability.

FAQ

How does Truto handle APIs that return a 200 OK status for errors?
Truto uses JSONata error expressions to inspect the response body. If an error payload is detected (like Slack's `"ok": false`), the expression overrides the 200 OK and rewrites it into the correct HTTP status code, such as 401 or 429, before it reaches your application.
What happens when a third-party API returns an incorrect HTTP status code?
You can write an error expression to remap semantically incorrect status codes. For example, if an API returns a 429 Too Many Requests for a missing subscription plan, the expression can detect the missing `Retry-After` header and remap the error to a 402 Payment Required.
How do error expressions interact with OAuth token refreshes?
When an error expression evaluates to a 401 Unauthorized, it sets a `truto_is_remote_error` flag. Truto intercepts this, pauses any active sync jobs, marks the account for re-authentication, and automatically triggers the OAuth token refresh flow.
Can I disable or override an integration's default error handling for a specific endpoint?
Yes. Error expressions follow a configuration hierarchy. You can configure them at the integration level, override them per environment, or set them at the specific resource method level. Setting a method's expression to null explicitly disables the parent integration's error handling.
What happens if the API returns plain text or HTML instead of JSON?
The JSONata expression can check the data type of the response. If it detects a string, it can use regular expressions to match specific text patterns (like "Authorization Error") and map it to the appropriate HTTP status code.

More from our Blog