Error Handling
Error Response Format
Every Tenanted API error comes back in one envelope, whatever the status:
{
"code": "not_found",
"message": "The requested resource was not found.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": []
}
| Field | What it carries |
|---|---|
code | A stable, machine-readable error code. Branch on this rather than on the message or the HTTP status. |
message | A human-readable description of the failure. Show it or log it, but do not parse it. |
requestId | The identifier for this request. Quote it when you report a failure so it can be traced. |
errors | One entry per request field that failed validation. Always present, and an empty list when the failure is not field-level. |
When a request fails validation (422), each entry in errors names the field, says what is wrong with it, and explains it in words:
{
"code": "invalid_request",
"message": "The request could not be validated.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": [
{
"field": "currency",
"code": "missing",
"message": "This field is required."
},
{
"field": "items[0].externalProductId",
"code": "missing",
"message": "This field is required."
}
]
}
field is the path to the field in your request, under the field's documented camelCase name. Nested fields are dot-separated, and a list entry carries its position, as in items[0].externalProductId. The field-level code is one of four values:
| Field code | Meaning | Message |
|---|---|---|
missing | A required field was not sent. | This field is required. |
invalid_type | The value is the wrong type, such as text where a number belongs. | The value is not of the type this field accepts. |
invalid_value | The value is the right type but not one the field accepts, such as an unknown enum member. | The value is not one this field accepts. |
out_of_range | The value is too long, too short, too large, or too small. | The value is outside the range this field accepts. |
Some fields replace the generic message with a specific one. For example, an API key expiresAt in the past reports expiresAt must be in the future.
HTTP Status Codes
Several more specific codes also use 400, listed under Error Codes.
Error Codes
The code is the part of the response to build on. These are the codes the Tenanted API returns:
| Code | Status | What to do |
|---|---|---|
invalid_request | 400, 422 | Fix the request. On a 422, errors names each field. |
invalid_api_version | 400 | The Api-Version header is not a date. Send YYYY-MM-DD, or omit the header. |
unsupported_api_version | 400 | The Api-Version date is older than the earliest release. Send 2026-07-21 or later, or omit the header. |
missing_target_selector | 400 | Your credential reaches more than one organization. Send Organization-Id, Connection-Id, or Entity-Id. |
conflicting_target_selectors | 400 | The selectors you sent resolve to different organizations. Send one, or make them agree. |
multiple_organization_memberships | 400 | A signed-in session belongs to several organizations. Send Organization-Id to choose one. |
multiple_credentials | 400 | A credential-management endpoint received both an Api-Key and a bearer token. Send exactly one. |
stale_cursor | 400 | The pagination cursor was issued for a different query. Restart from the first page. |
unauthorized | 401 | Send a valid credential. |
forbidden | 403 | The credential cannot perform this operation. |
plan_upgrade_required | 403 | The action needs a paid plan or a premium entitlement. Upgrade the organization's plan. |
not_found | 404 | Check the ID and the organization you selected. |
method_not_allowed | 405 | Use a method from the Allow header. |
conflict | 409 | Read the message: the resource already exists or is in the wrong state. |
entity_resolution_ambiguous | 409 | Entity-Id matches more than one connection. Add Entity-Source or Connection-Id. |
gone | 410 | The link is finished. Ask whoever sent it for a new one. |
payload_too_large | 413 | Shrink the body, for example by compressing or splitting a file. |
rate_limited | 429 | Slow down, then retry. |
service_unavailable | 503 | Retry with backoff. The same request may succeed on a later attempt. |
error | 500 | Retry with backoff, and quote the requestId if it persists. |
Three further codes, missing_portfolio_selector, multiple_portfolio_memberships, and TEST_PARTNER_INACTIVE, apply only to partner portfolio credentials.
Common Error Messages
Authentication Errors
Validation Errors
Error Handling Best Practices
Read the envelope and handle each code appropriately:
import requests
response = requests.get(url, headers=headers)
if not response.ok:
error = response.json()
code = error["code"]
if code == "unauthorized":
# Handle authentication error
print("Invalid or missing credential")
elif code == "rate_limited":
# Handle rate limiting
print("Rate limited. Slow down and retry.")
elif code == "invalid_request":
# Handle validation errors
for field_error in error["errors"]:
print(f"{field_error['field']}: {field_error['message']}")
if not error["errors"]:
print(error["message"])
elif code == "stale_cursor":
# Restart pagination from the first page
print("Cursor no longer valid. Restarting from the first page.")For rate limiting and temporary errors, retry with exponential backoff:
import random
import time
import requests
# rate_limited, error, service_unavailable
RETRYABLE_STATUSES = {429, 500, 503}
def make_request_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.ok:
return response
if response.status_code not in RETRYABLE_STATUSES:
response.raise_for_status()
# Exponential backoff with jitter
wait_time = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait_time)
raise Exception("Max retries exceeded")Log errors with enough context to trace them, and always keep the requestId:
import logging
import requests
logger = logging.getLogger(__name__)
response = requests.get(url, headers=headers)
if not response.ok:
error = response.json()
logger.error(
"API request failed",
extra={
"status_code": response.status_code,
"url": url,
"code": error.get("code"),
"request_id": error.get("requestId"),
"message": error.get("message"),
}
)
response.raise_for_status()Transform error codes into messages your own users can act on:
def handle_api_error(error):
code = error.get("code")
if code == "unauthorized":
return "Please check your API key and try again."
elif code == "not_found":
return "The requested resource was not found."
elif code == "invalid_request":
return "Please check your input data and try again."
elif code == "rate_limited":
return "Too many requests. Please wait a moment and try again."
else:
return "An unexpected error occurred. Please try again later."