# Errors

> Source: https://truto.one/docs/api-reference/overview/errors/

Every Truto error comes back with the right HTTP status, a JSON body shaped the same way regardless of route, and — for Unified and Proxy APIs — extra fields that tell you whether the failure happened at Truto's edge or inside the provider you're calling.

## Response shape

Successful responses are documented per endpoint. Error responses always look like this:

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "name is required"
}
```

Three fields are guaranteed:

- **`statusCode`** — the same number as the HTTP status. Mirrors the response status line.
- **`error`** — the canonical name for that status, e.g. `Bad Request`, `Unauthorized`, `Not Found`, `Too Many Requests`, `Internal Server Error`.
- **`message`** — a human-readable description. For provider-originated errors this is extracted from the provider's body (see below).

Errors raised by Truto's own validation, auth, and routing layers (admin endpoints, Unified API parameter checks, missing `integrated_account_id`, malformed request bodies, etc.) stop here.

Errors from the underlying provider — anything you hit through `/unified/*` or `/proxy/*` — carry several extra fields.

## Distinguishing Truto errors from provider errors

When a Unified or Proxy API call fails because the underlying provider returned a non-2xx, the response gains:

- **`truto_is_remote_error: true`** — set on every error that originated outside Truto.
- **`raw_response`** — the provider's response body, parsed as JSON if possible, otherwise the raw text.
- All response headers from the provider, normalized and forwarded back (rate-limit headers, `Retry-After`, etc. — see [Rate limits](/docs/api-reference/overview/rate-limits)).

```json
{
  "statusCode": 422,
  "error": "Unprocessable Entity",
  "message": "Email is invalid",
  "truto_is_remote_error": true,
  "raw_response": {
    "errors": [
      {
        "field": "email",
        "code": "invalid",
        "message": "Email is invalid"
      }
    ]
  }
}
```

The `message` is normalized by walking the provider body for the usual error fields (`message`, `msg`, `errorMessage`, `description`, `detail`, `summary`, `error_description`, and a long list of variants). When that lookup finds nothing, `message` is empty and you should fall back to `raw_response`.

If the field is missing, the error came from Truto itself — almost always a 4xx for input you sent (missing `integrated_account_id`, malformed body, expired token), or a 5xx that means we couldn't reach the provider at all.

## Insights — `truto_error_insight`

Unified API errors come with a `truto_error_insight` block that tells you, in machine-readable form, what to change. Each key is independent; you may see one, several, or none.

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "name is required",
  "truto_error_insight": {
    "missing_required_body_fields": {
      "description": "These body fields are required and are missing in the request",
      "value": ["name"]
    }
  }
}
```

The keys you can see:

- **`missing_required_query_parameters`** — `value` is an array of query parameter names you didn't send but the unified model requires.
- **`missing_required_body_fields`** — same, for body fields.
- **`conditionally_required_query_parameters`** — `value` is an object keyed by parameter name, with the rule that triggers the requirement (e.g. "required when `type` is `lead`").
- **`conditionally_required_body_fields`** — same, for body fields.
- **`rate_limit_error`** — present on a 429 from the provider. Pair with the `Retry-After` header.
- **`remote_error`** — present whenever `truto_is_remote_error` is `true`. The hint is intentionally short — read `raw_response` and `message` for the actual content.
- **`forbidden_error`** — present on a 403 from the provider. `value.missing_scopes` is the list of OAuth scopes the connection is missing for the resource and method you called. Use this to drive a re-consent flow.

Proxy API errors carry `truto_is_remote_error`, `raw_response`, forwarded headers, and — on 403s — a `truto_error_insight.forbidden_error`. The Proxy API has no unified schema to compare your request against, so the missing-parameter, rate-limit, and remote-error insights aren't added.

```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions",
  "truto_is_remote_error": true,
  "raw_response": {
    "error": "insufficient_scope",
    "error_description": "Token does not have crm.objects.contacts.write scope"
  },
  "truto_error_insight": {
    "forbidden_error": {
      "description": "Access forbidden due to insufficient scopes or permissions",
      "value": {
        "missing_scopes": ["crm.objects.contacts.write"]
      }
    }
  }
}
```

## Status codes

The numbers below are what you'll actually see; the meaning given is what they mean inside Truto.

### 2xx

- **`200 OK`** — request succeeded. Body contains the requested resource.
- **`201 Created`** — `POST` succeeded and a new resource was created.
- **`204 No Content`** — request succeeded with no body. Common for `DELETE`.

### 4xx

- **`400 Bad Request`** — Truto rejected the request before it left our edge. On Unified APIs this is also where missing-required-field errors land; check `truto_error_insight`.
- **`401 Unauthorized`** — without `truto_is_remote_error`, your API token is missing, expired, or wrong. With `truto_is_remote_error: true`, the provider rejected the connection's credentials — Truto flips the integrated account to `needs_reauth`, sets `last_error` on it, and fires the [`integrated_account:authentication_error`](/docs/guides/webhooks/list-webhook-events) webhook. The connection will keep returning 401 until the end user [reconnects](/docs/guides/integrated-accounts/reauthorizing-connections).
- **`403 Forbidden`** — the caller is authenticated but not allowed. On Unified and Proxy APIs check `truto_error_insight.forbidden_error.value.missing_scopes` — if it's non-empty, this is an OAuth scope problem and a reconnect with the right scopes will fix it.
- **`404 Not Found`** — the resource doesn't exist, or the integrated account / environment isn't visible to your token.
- **`405 Method Not Allowed`** — the HTTP method isn't supported on that route. Sandbox accounts return this on `POST` / `PATCH` / `DELETE` calls because they're read-only.
- **`409 Conflict`** — the provider rejected a create or update because of a uniqueness constraint. Always carries `truto_is_remote_error: true`; check `raw_response` for the specific field.
- **`422 Unprocessable Entity`** — payload is well-formed but the provider rejected it (validation error inside the provider).
- **`429 Too Many Requests`** — see [Rate limits](#rate-limits) below.
- **`503 Service Unavailable`** — the integrated account has been blocked. Contact `support@truto.one`.

### 5xx

- **`500 Internal Server Error`** — Truto failed to process the request. Retry with backoff; if it persists, capture the response and contact support.
- **`502 Bad Gateway`** / **`504 Gateway Timeout`** — Truto reached the provider but the provider didn't respond cleanly. Safe to retry.

## Rate limits

Truto enforces two rate-limit tiers in front of every request. Both return a 429 with `Retry-After: 10`.

| Scope | Limit | Triggered by |
| --- | --- | --- |
| Per API token | 50 requests / 1s | The bearer token in `Authorization` |
| Per integrated account | 50 requests / 10s | The `integrated_account_id` query parameter (Unified and Proxy APIs only) |

```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Too many requests. You can make 50 requests every 10 seconds per integrated account."
}
```

When the underlying provider rate-limits you, the response is a 429 with `truto_is_remote_error: true` and the provider's `Retry-After` (or our normalized equivalent) in the headers. On Unified API routes you'll also get `truto_error_insight.rate_limit_error`. See [Rate limits](/docs/api-reference/overview/rate-limits) for the full set of headers Truto normalizes.

## Retry strategy

- **Retry on**: 429 (after `Retry-After`), 502, 503 (only if you didn't trigger it via a blocked account), 504, and 5xx that don't carry `truto_is_remote_error`.
- **Don't retry on**: 400, 401, 403, 404, 409, 422 — fix the request, the token, or the connection first.
- **Watch `truto_is_remote_error`**: a 5xx with `truto_is_remote_error: true` is the provider failing, not Truto. Retry policy should be the same as for any flaky upstream — exponential backoff, capped attempts.
