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:
{
"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_parameters—valueis 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—valueis an object keyed by parameter name, with the rule that triggers the requirement (e.g. "required whentypeislead").conditionally_required_body_fields— same, for body fields.rate_limit_error— present on a 429 from the provider. Pair with theRetry-Afterheader.remote_error— present whenevertruto_is_remote_erroristrue. The hint is intentionally short — readraw_responseandmessagefor the actual content.forbidden_error— present on a 403 from the provider.value.missing_scopesis 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 Created—POSTsucceeded and a new resource was created.204 No Content— request succeeded with no body. Common forDELETE.
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; checktruto_error_insight.401 Unauthorized— withouttruto_is_remote_error, your API token is missing, expired, or wrong. Withtruto_is_remote_error: true, the provider rejected the connection's credentials — Truto flips the integrated account toneeds_reauth, setslast_erroron it, and fires theintegrated_account:authentication_errorwebhook. 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 checktruto_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 onPOST/PATCH/DELETEcalls because they're read-only.409 Conflict— the provider rejected a create or update because of a uniqueness constraint. Always carriestruto_is_remote_error: true; checkraw_responsefor 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. Contactsupport@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 carrytruto_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 withtruto_is_remote_error: trueis the provider failing, not Truto. Retry policy should be the same as for any flaky upstream — exponential backoff, capped attempts.