---
title: 404 Reasons Third-Party APIs Can't Get Their Errors Straight (And How to Fix It)
slug: 404-reasons-third-party-apis-cant-get-their-errors-straight-and-how-to-fix-it
date: 2026-03-04
author: Uday Gajavalli
categories: [Engineering]
excerpt: "Third-party API errors are wildly inconsistent. Learn how to normalize 200 OK errors, missing rate limit headers, and HTML responses without writing code."
tldr: API error normalization translates provider-specific errors into standard HTTP semantics. Truto uses declarative JSONata expressions to handle edge cases without hardcoded adapter logic.
canonical: https://truto.one/blog/404-reasons-third-party-apis-cant-get-their-errors-straight-and-how-to-fix-it/
---

# 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](https://truto.one/blog/converting-graphql-to-rest-apis-a-deep-dive-into-trutos-proxy-architecture/) 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](https://truto.one/blog/look-ma-no-code-why-trutos-zero-code-architecture-wins/). You write a specific error handler for HubSpot, another for Salesforce, and another for Zendesk.

```typescript
// 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](https://truto.one/blog/build-vs-buy-the-true-cost-of-building-saas-integrations-in-house/). 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](https://truto.one/blog/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](https://truto.one/blog/your-unified-apis-are-lying-to-you-the-hidden-cost-of-rigid-schemas/) based on the Interpreter Pattern. [Zero-code architecture](https://truto.one/blog/look-ma-no-code-why-trutos-zero-code-architecture-wins/) 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. 

> [!NOTE]
> **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:

```typescript
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:

```jsonata
$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`:

```jsonata
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:

```jsonata
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:

```jsonata
$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. 

> Stop wrestling with 50 different API error formats. See how Truto's declarative architecture can standardize your integrations in minutes.
>
> [Talk to us](https://cal.com/truto/partner-with-truto)
