Skip to content

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:

{
  "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).
{
  "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.

{
  "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_parametersvalue 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_parametersvalue 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.

{
  "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 CreatedPOST 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 webhook. The connection will keep returning 401 until the end user reconnects.
  • 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 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)
{
  "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 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.