# Error Handling (2026-07-21)

> Learn how to handle API errors and implement robust error handling

Source: https://docs.trykintsugi.com/docs/2026-07-21/advanced/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:

## Error Response Format

| 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

Client errors 4xx · 10 codes Fix the request, then retry.

400 Bad Request

The request was invalid.

Default code

invalid_request

401 Unauthorized

Authentication failed or was missing.

Default code

unauthorized

403 Forbidden

The credential is not permitted for this request.

Default code

forbidden

404 Not Found

The resource does not exist, or belongs to an organization your credential cannot reach. The two are deliberately identical.

Default code

not_found

405 Method 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

409 Conflict

The request conflicts with existing state, such as a duplicate create or a resource in the wrong state for the change.

Default code

conflict

410 Gone

A link that was valid no longer accepts requests. Used by the public certificate-upload links.

Default code

gone

413 Payload Too Large

The request body is larger than the endpoint accepts.

Default code

payload_too_large

422 Unprocessable Content

The request failed validation, and the errors list names each field.

Default code

invalid_request

429 Too Many Requests

The caller is over a rate or usage limit.

Default code

rate_limited

Server errors 5xx · 2 codes Retry with backoff.

500 Internal Server Error

An unexpected error prevented the request from completing.

Default code

error

503 Service 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:

## Error Codes

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

## Error Codes

Three further codes, missing_portfolio_selector, multiple_portfolio_memberships, and TEST_PARTNER_INACTIVE, apply only to partner portfolio credentials.

## Common Error Messages

Authentication Errors

Missing or Invalid API Key

Error Response

{
"code": "unauthorized",
"message": "Invalid API key.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": []
}

Solution

Send your API key in the Api-Key header:

curl -H "Api-Key: $KINTSUGI_API_KEY" \
https://api.trykintsugi.com/products/categories

Verify the key is correct and complete

Check it has not expired or been deleted

Check the header is named Api-Key

The message varies with the cause, so branch on code: "unauthorized" rather than on the text.

Session Token Required

Error Response

{
"code": "unauthorized",
"message": "Provide a bearer token.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": []
}

Solution

The Users and API Keys endpoints manage people and credentials, and they do not accept an organization API key. Send a signed-in user's session token as Authorization: Bearer <token>. See Endpoints That Take a Session Token.

Organization Not Found

Error Response

{
"code": "not_found",
"message": "The requested resource was not found.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": []
}

Solution

An Organization-Id, Connection-Id, or Entity-Id that names an organization your credential cannot reach returns the same 404 as one that does not exist. A key created in the app acts on its own organization and needs no selector, so remove the header or correct it:

curl -H "Api-Key: $KINTSUGI_API_KEY" \
-H "Organization-Id: $KINTSUGI_ORG_ID" \
https://api.trykintsugi.com/products

List organizations returns every organization your credential can access, with the ID to use.

Not Permitted

Error Response

{
"code": "forbidden",
"message": "Your credential is not permitted to perform this operation.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": []
}

Solution

## Common Error Messages

Your credential reached the organization but cannot perform this operation. Managing users and API keys, for example, takes a signed-in user with the Owner or Admin role.

Plan Upgrade Required

Error Response

{
"code": "plan_upgrade_required",
"message": "A paid plan is required to view a billing estimate.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": []
}

Solution

The request is valid and authenticated, but the organization's current plan does not include the action. Upgrade the plan, then retry. Branch on the code, not the message, to offer an upgrade in your own product.

Validation Errors

Missing Required Fields

{
"code": "invalid_request",
"message": "The request could not be validated.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": [
{
"field": "externalId",
"code": "missing",
"message": "This field is required."
}
]
}

Solution: Include every required field. The reference marks each one, and field names the one that is missing.

Invalid Field Values

{
"code": "invalid_request",
"message": "The request could not be validated.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": [
{
"field": "addresses[0].type",
"code": "invalid_value",
"message": "The value is not one this field accepts."
}
]
}

Solution: Use a value the field accepts. The reference lists the members of every enum.

Business Logic Violations

{
"code": "invalid_request",
"message": "originalTransactionId may only be sent when type is a credit-note type.",
"requestId": "req_8f3k2mNpQr7Ls",
"errors": []
}

Solution: A rule that spans more than one field comes back as a 400 with the rule in message and an empty errors list. Adjust the request so the fields agree.

## 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__)

## Error Handling Best Practices

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

Authentication Issues

Missing or Invalid API Key

Error: 401 with code unauthorized

Solution: Send your key in the Api-Key header, and check it has not expired or been deleted:

curl -H "Api-Key: $KINTSUGI_API_KEY" \
https://api.trykintsugi.com/products/categories

The Tenanted API uses Api-Key, not the v1 x-api-key header, and a key created in the app needs no organization header.

Session Token Required

Error: Provide a bearer token.

Solution: Users and API Keys endpoints do not accept an organization API key. Send a signed-in user's session token as Authorization: Bearer <token>.

Validation Issues

Missing Required Fields

Error: This field is required. on a field with code missing

Solution: Check the reference for required fields and include each one in your request body. field names the one that is missing.

Invalid Data Types

Error: The value is not of the type this field accepts. or The value is not one this field accepts.

Solution: Use the type and enum values the reference lists for the field. Money amounts are decimal strings, and timestamps are RFC 3339 UTC ending in Z.

Resource Issues

Resource Not Found

Error: The requested resource was not found.

Solution:

Verify the resource ID is correct

Check the resource belongs to an organization your credential can reach

Check any Organization-Id, Connection-Id, or Entity-Id you sent

Choosing an Organization

Error: Provide one of Organization-Id, Connection-Id, or Entity-Id.

Solution: Your credential reaches more than one organization, so a write has to name one. Send Organization-Id with the ID from List organizations.

## Related Resources

Getting Started

Authenticating Your Requests

API Reference

Support

---

Index of every page: https://docs.trykintsugi.com/llms.txt
