KintsugiKintsugi

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": []
}
FieldWhat it carries
codeA stable, machine-readable error code. Branch on this rather than on the message or the HTTP status.
messageA human-readable description of the failure. Show it or log it, but do not parse it.
requestIdThe identifier for this request. Quote it when you report a failure so it can be traced.
errorsOne 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 codeMeaningMessage
missingA required field was not sent.This field is required.
invalid_typeThe value is the wrong type, such as text where a number belongs.The value is not of the type this field accepts.
invalid_valueThe 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_rangeThe 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

Client errors4xx · 10 codesFix the request, then retry.
400Bad Request
The request was invalid.
Default code
invalid_request
401Unauthorized
Authentication failed or was missing.
Default code
unauthorized
403Forbidden
The credential is not permitted for this request.
Default code
forbidden
404Not Found
The resource does not exist, or belongs to an organization your credential cannot reach. The two are deliberately identical.
Default code
not_found
405Method Not Allowed
The path exists but does not accept that method. The Allow header lists the methods it does accept.
Default code
method_not_allowed
409Conflict
The request conflicts with existing state, such as a duplicate create or a resource in the wrong state for the change.
Default code
conflict
410Gone
A link that was valid no longer accepts requests. Used by the public certificate-upload links.
Default code
gone
413Payload Too Large
The request body is larger than the endpoint accepts.
Default code
payload_too_large
422Unprocessable Content
The request failed validation, and the errors list names each field.
Default code
invalid_request
429Too Many Requests
The caller is over a rate or usage limit.
Default code
rate_limited
Server errors5xx · 2 codesRetry with backoff.
500Internal Server Error
An unexpected error prevented the request from completing.
Default code
error
503Service Unavailable
A service the request depends on was unavailable. Retry the request.
Default code
service_unavailable

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:

CodeStatusWhat to do
invalid_request400, 422Fix the request. On a 422, errors names each field.
invalid_api_version400The Api-Version header is not a date. Send YYYY-MM-DD, or omit the header.
unsupported_api_version400The Api-Version date is older than the earliest release. Send 2026-07-21 or later, or omit the header.
missing_target_selector400Your credential reaches more than one organization. Send Organization-Id, Connection-Id, or Entity-Id.
conflicting_target_selectors400The selectors you sent resolve to different organizations. Send one, or make them agree.
multiple_organization_memberships400A signed-in session belongs to several organizations. Send Organization-Id to choose one.
multiple_credentials400A credential-management endpoint received both an Api-Key and a bearer token. Send exactly one.
stale_cursor400The pagination cursor was issued for a different query. Restart from the first page.
unauthorized401Send a valid credential.
forbidden403The credential cannot perform this operation.
plan_upgrade_required403The action needs a paid plan or a premium entitlement. Upgrade the organization's plan.
not_found404Check the ID and the organization you selected.
method_not_allowed405Use a method from the Allow header.
conflict409Read the message: the resource already exists or is in the wrong state.
entity_resolution_ambiguous409Entity-Id matches more than one connection. Add Entity-Source or Connection-Id.
gone410The link is finished. Ask whoever sent it for a new one.
payload_too_large413Shrink the body, for example by compressing or splitting a file.
rate_limited429Slow down, then retry.
service_unavailable503Retry with backoff. The same request may succeed on a later attempt.
error500Retry 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

Branch on the Error Code

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.")
Implement Exponential Backoff

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 Appropriately

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()
Provide User-Friendly Error Messages

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

Troubleshooting Common Issues