Skip to main content

Error Handling

The TrustGate API uses a consistent error response format across all endpoints. Every error response follows the same JSON envelope structure, making it straightforward to build reliable error handling in your integration.

Error Response Format

All API errors return a JSON object with an error key containing a structured error detail:

{
"error": {
"code": "MACHINE_READABLE_CODE",
"message": "Human-readable description of what went wrong",
"status": 400,
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}
FieldTypeDescription
codestringA machine-readable error code (e.g., UNAUTHORIZED, VALIDATION_ERROR). Use this for programmatic error handling.
messagestringA human-readable description of the error. Suitable for logging but not guaranteed to be stable across versions.
statusintegerThe HTTP status code, mirrored inside the error body for convenience.
request_idstring or nullA unique identifier for the request. Include this when contacting support for faster debugging.

The request_id is assigned by TrustGate middleware on every incoming request and is included in all error responses. It is also available in the X-Request-ID response header.

Error Codes

TrustGate uses the following machine-readable error codes:

CodeHTTP StatusDescription
UNAUTHORIZED401Missing or invalid authentication credentials.
INVALID_API_KEY401The provided API key does not match any active key.
EXPIRED_API_KEY401The API key has passed its expiration date.
PERMISSION_DENIED403The authenticated user or API key lacks the required permission.
NOT_FOUND404The requested resource does not exist or is not accessible to your tenant.
CONFLICT409The request conflicts with existing state (e.g., duplicate resource).
IDEMPOTENCY_MISMATCH409An idempotency key was reused with different request parameters.
VALIDATION_ERROR422The request body failed validation. The message field contains field-level details.
RATE_LIMITED429Too many requests. Back off and retry after the duration indicated in the Retry-After header.
INTERNAL_ERROR500An unexpected server error occurred.
CIRCUIT_BREAKER_OPEN503A downstream provider is temporarily unavailable. The circuit breaker has been tripped.
PRIMITIVE_NOT_FOUND404The requested primitive does not exist.
PAYMENT_REQUIRED402Usage limit reached or premium feature requires a plan upgrade.

HTTP Status Codes

400 Bad Request

The request is malformed or contains invalid parameters that do not fall under validation (422). Examples include invalid permission names when creating an API key or invalid webhook event types.

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid permissions: ['nonexistent:permission']",
"status": 400,
"request_id": "a1b2c3d4-..."
}
}

401 Unauthorized

Authentication failed. The request either has no credentials or the credentials are invalid.

Missing authentication:

{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing authentication. Provide Bearer token or X-API-Key header.",
"status": 401,
"request_id": "a1b2c3d4-..."
}
}

Invalid API key:

{
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid API key",
"status": 401,
"request_id": "a1b2c3d4-..."
}
}

Expired API key:

{
"error": {
"code": "EXPIRED_API_KEY",
"message": "API key has expired",
"status": 401,
"request_id": "a1b2c3d4-..."
}
}

Invalid JWT token:

{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid token: Signature has expired.",
"status": 401,
"request_id": "a1b2c3d4-..."
}
}

402 Payment Required

The request was authenticated but the tenant has exceeded their usage limits or is attempting to use a feature that requires a plan upgrade.

{
"error": {
"code": "PAYMENT_REQUIRED",
"message": "Monthly usage limit reached. Upgrade your plan to continue.",
"status": 402,
"request_id": "a1b2c3d4-..."
}
}

403 Forbidden

The request was authenticated, but the user or API key does not have the required permission or role.

Missing permission:

{
"error": {
"code": "PERMISSION_DENIED",
"message": "API key lacks permission to invoke primitive: screening.individual",
"status": 403,
"request_id": "a1b2c3d4-..."
}
}

Insufficient role:

{
"error": {
"code": "PERMISSION_DENIED",
"message": "Requires one of roles: admin, manager",
"status": 403,
"request_id": "a1b2c3d4-..."
}
}

Plan-gated feature:

{
"error": {
"code": "PERMISSION_DENIED",
"message": "'workflow_builder' requires business plan or higher. Current plan: developer",
"status": 403,
"request_id": "a1b2c3d4-..."
}
}

404 Not Found

The requested resource does not exist, or it belongs to a different tenant and is not accessible.

{
"error": {
"code": "NOT_FOUND",
"message": "API key not found",
"status": 404,
"request_id": "a1b2c3d4-..."
}
}

Due to tenant isolation, a 404 is returned even if the resource exists under a different tenant. This prevents information leakage about other tenants' data.

409 Conflict

The request conflicts with the current state of a resource.

Idempotency conflict:

{
"error": {
"code": "IDEMPOTENCY_MISMATCH",
"message": "Idempotency key reused with different request parameters",
"status": 409,
"request_id": "a1b2c3d4-..."
}
}

State conflict:

{
"error": {
"code": "CONFLICT",
"message": "API key already revoked",
"status": 409,
"request_id": "a1b2c3d4-..."
}
}

422 Unprocessable Entity

The request body failed schema validation. The message field contains field-level error details joined by semicolons.

{
"error": {
"code": "VALIDATION_ERROR",
"message": "body.email: value is not a valid email address; body.first_name: Field required",
"status": 422,
"request_id": "a1b2c3d4-..."
}
}

The message format is {location}.{field}: {error_description} for each invalid field. Multiple field errors are separated by semicolons.

429 Too Many Requests

The request was rate-limited. Wait for the duration specified in the Retry-After response header before making another request.

{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded",
"status": 429,
"request_id": "a1b2c3d4-..."
}
}

Rate limit headers are included in the response:

HeaderDescription
Retry-AfterNumber of seconds to wait before retrying
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining before the limit is hit
X-RateLimit-ResetUnix timestamp when the rate limit window resets

500 Internal Server Error

An unexpected error occurred on the server. In production, the message is generic to prevent information leakage. The request_id can be provided to TrustGate support for investigation.

{
"error": {
"code": "INTERNAL_ERROR",
"message": "Internal server error",
"status": 500,
"request_id": "a1b2c3d4-..."
}
}

503 Service Unavailable

A downstream provider (e.g., sanctions screening service, document verification provider) is temporarily unavailable. The circuit breaker has been tripped to prevent cascading failures.

{
"error": {
"code": "CIRCUIT_BREAKER_OPEN",
"message": "Provider temporarily unavailable. Please retry shortly.",
"status": 503,
"request_id": "a1b2c3d4-..."
}
}

Error Handling Best Practices

Retry Strategy

Not all errors should be retried. Use the following guide:

Status CodeRetry?Strategy
400NoFix the request. The parameters are invalid.
401NoCheck your API key or token. Re-authenticate if using JWTs.
402NoUpgrade your plan or wait for the billing cycle to reset.
403NoThe API key or user lacks the required permission. Update permissions.
404NoThe resource does not exist. Verify the ID and your tenant scope.
409NoResolve the conflict. For idempotency mismatches, use a new idempotency key.
422NoFix the request body. Review the field-level errors in the message.
429YesWait for the Retry-After duration, then retry.
500YesRetry with exponential backoff. If persistent, contact support with the request_id.
503YesRetry with exponential backoff. The downstream provider should recover shortly.

Exponential Backoff

For retryable errors (429, 500, 503), use exponential backoff with jitter:

Python:

import time
import random
import requests

def make_request_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries + 1):
response = requests.get(url, headers=headers)

if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
time.sleep(retry_after)
continue

if response.status_code in (500, 503):
if attempt < max_retries:
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
continue

return response

return response # Return last response after exhausting retries

JavaScript:

async function makeRequestWithRetry(url, headers, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, { headers });

if (response.status === 429) {
const retryAfter = parseInt(response.headers.get("Retry-After") || "5", 10);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
continue;
}

if ([500, 503].includes(response.status) && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
await new Promise((r) => setTimeout(r, delay));
continue;
}

return response;
}
}

Parsing Error Responses

Always check for the error envelope in non-2xx responses:

response = requests.post(url, headers=headers, json=payload)

if not response.ok:
error_data = response.json().get("error", {})
error_code = error_data.get("code")
error_message = error_data.get("message")
request_id = error_data.get("request_id")

if error_code == "INVALID_API_KEY":
# Handle invalid key: check configuration
pass
elif error_code == "PERMISSION_DENIED":
# Handle missing permission: check API key scopes
pass
elif error_code == "RATE_LIMITED":
# Handle rate limit: implement backoff
pass
else:
# Log for debugging
print(f"API error [{error_code}]: {error_message} (request_id: {request_id})")

Using the Request ID

Every API response includes a request_id (in both the error body and the X-Request-ID response header). When contacting TrustGate support about an error, always include the request_id -- it allows the team to trace the exact request through server logs.

Idempotency Keys

For operations that should not be repeated (such as invoking a primitive), TrustGate supports idempotency keys via the Idempotency-Key header. If you replay a request with the same idempotency key and the same parameters, TrustGate returns the cached result. If you reuse the key with different parameters, you receive a 409 IDEMPOTENCY_MISMATCH error.

curl -X POST https://api.bytrustgate.com/api/v1/primitives/screening.individual/invoke \
-H "X-API-Key: sk_live_your_api_key_here" \
-H "Idempotency-Key: unique-request-id-12345" \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "date_of_birth": "1990-01-15"}'

Next Steps