Error Handling
Every error response from the API uses the same JSON envelope, regardless of
the endpoint. Check the HTTP status code first, then switch on code
for programmatic branching.
The error envelope
| Field | Type | Description |
|---|---|---|
error |
string | Human-readable summary of what went wrong. Safe to display. |
code |
string | Machine-readable error code - stable across releases, safe to switch on. |
request_id |
string | UUID that correlates to server-side logs. Always include this when contacting support. |
errors |
object | Field-level validation messages keyed by field name. Only present on validation_error. |
details |
object | Extra context to help you act on the error. Shape varies by error type; present when there is actionable context beyond error and code. See the reference table below. |
Minimal error (401)
{
"error": "Authentication credentials were not provided.",
"code": "authentication_failed",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
With field errors (400)
{
"error": "Validation failed.",
"code": "validation_error",
"request_id": "550e8400-e29b-41d4-a716-446655440001",
"errors": {
"legal_name": ["This field is required."],
"entity_type": ["Must be 'company' or 'individual'."]
}
}
With details (409)
{
"error": "A check is already in progress.",
"code": "conflict",
"request_id": "550e8400-e29b-41d4-a716-446655440002",
"details": {
"existing_check_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
"status": "pending"
}
}
Error code reference
All possible code values, the HTTP status they always appear on, and the details shape when present.
| HTTP | code | When it occurs | details keys |
|---|---|---|---|
| 400 | validation_error |
Request body failed field validation - check errors for per-field messages. |
- |
| 400 | invalid_request |
Request is structurally valid but logically invalid - e.g. wrong workflow state, unsupported operation. | current_status (when a status transition is rejected) |
| 401 | authentication_failed |
Missing, malformed, or revoked API key. | - |
| 402 | insufficient_credits |
The tenant has no remaining credits for this feature. | - |
| 403 | permission_denied |
Valid key but not authorised to access this resource. | - |
| 404 | not_found |
Resource does not exist or is not accessible to your tenant. | - |
| 409 | conflict |
Action conflicts with current state - e.g. a duplicate screening or check already in progress. | existing_check_id, status |
| 409 | idempotency_conflict |
The same Idempotency-Key header was reused with a different request body. |
- |
| 429 | rate_limited |
Too many requests. Back off and retry | - |
| 500 | server_error |
Unexpected internal error on KYC Genie's side. Report request_id to support. |
- |
| 502 | upstream_error |
An external service (e.g. a webhook endpoint) returned a non-2xx response or was unreachable. | http_status_code (remote status, or absent if unreachable) |
Invalid request (400) - with details
{
"error": "This response has already been submitted.",
"code": "invalid_request",
"request_id": "550e8400-e29b-41d4-a716-446655440003",
"details": {
"current_status": "submitted"
}
}
Insufficient credits (402)
{
"error": "Insufficient credits to perform this action.",
"code": "insufficient_credits",
"request_id": "550e8400-e29b-41d4-a716-446655440004"
}
Upstream error (502)
{
"error": "Webhook endpoint returned HTTP 405",
"code": "upstream_error",
"request_id": "6d078d05-c86c-4728-9ae9-89ab22b0e1d2",
"details": {
"http_status_code": 405
}
}
Which errors to retry
Not all errors are worth retrying. The table below gives the recommended strategy for each code.
| code | Retry? | Strategy |
|---|---|---|
validation_error |
No | Fix the request body first - retrying without changes will get the same error. |
invalid_request |
No | The request is logically invalid for the current state. Check details.current_status. |
authentication_failed |
No | Check your API key. Rotating and retrying once is fine. |
insufficient_credits |
No | Top up credits in the dashboard before retrying. |
permission_denied |
No | Your key doesn't have access to this resource. |
not_found |
No | Resource does not exist. Verify the ID. |
conflict |
Sometimes | Wait for the in-progress operation to complete, then retry if needed. Check details for the existing resource. |
idempotency_conflict |
No | Generate a new Idempotency-Key if you genuinely want a second request. |
rate_limited |
Yes | Exponential back-off starting at 1 s. The Python SDK retries 429s automatically. |
server_error |
Yes | Retry up to 3 times with exponential back-off. Include request_id if you report the issue. |
upstream_error |
Sometimes | The remote endpoint is outside KYC Genie's control. Fix the remote service or URL, then retry. |
Rate limited (429)
{
"error": "Request was throttled. Expected available in 1 second.",
"code": "rate_limited",
"request_id": "550e8400-e29b-41d4-a716-446655440005"
}
Python SDK - exception hierarchy
Every API error raises a typed exception. Catch specific subclasses for precise handling,
or catch the base KYCGenieAPIError for a catch-all.
The exception class is determined by code in the response body - not the HTTP status code -
so the same status can produce different exception types.
| code | Exception class | Extra attribute |
|---|---|---|
validation_error |
KYCGenieValidationError |
e.data.errors - field-level messages |
invalid_request |
KYCGenieValidationError |
e.data.details |
authentication_failed |
KYCGenieAuthenticationError |
- |
insufficient_credits |
KYCGenieInsufficientCreditsError |
- |
permission_denied |
KYCGeniePermissionError |
- |
not_found |
KYCGenieNotFoundError |
- |
conflict |
KYCGenieConflictError |
e.data.details |
rate_limited |
KYCGenieRateLimitError |
- |
upstream_error |
KYCGenieUpstreamError |
e.remote_status_code - HTTP status returned by the remote, or None if unreachable |
server_error |
KYCGenieServerError |
- |
All exception classes share these attributes:
| Attribute | Type | Description |
|---|---|---|
e.status_code |
int | HTTP status code |
e.message |
str | Human description from the API docs |
e.data.error |
str | Human-readable error from the response body |
e.data.code |
str | Machine-readable error code |
e.data.request_id |
str | Request ID for support escalation |
e.data.errors |
dict | Field-level errors - only on validation_error |
e.data.details |
dict | Extra context - varies by error type |
Recommended catch pattern
from kycgenie import KYCGenie
from kycgenie.errors import (
KYCGenieValidationError,
KYCGenieNotFoundError,
KYCGenieInsufficientCreditsError,
KYCGenieConflictError,
KYCGenieRateLimitError,
KYCGenieUpstreamError,
KYCGenieServerError,
KYCGenieAPIError,
)
client = KYCGenie(api_key="YOUR_API_KEY")
try:
entity = client.entities.create(legal_name="Acme Ltd")
except KYCGenieValidationError as e:
# 400 - fix the request
print(f"Validation failed: {e.data.errors}")
except KYCGenieInsufficientCreditsError:
# 402 - top up credits
print("No credits remaining")
except KYCGenieConflictError as e:
# 409 - resource already exists or in-progress
existing_id = (e.data.details or {}).get("existing_check_id")
print(f"Conflict - existing resource: {existing_id}")
except KYCGenieRateLimitError:
# 429 - SDK retries automatically, but you can catch manually
print("Rate limit hit, back off and retry")
except KYCGenieServerError as e:
# 500 - report to support with request_id
print(f"Server error, request_id: {e.data.request_id}")
except KYCGenieAPIError as e:
# catch-all for anything else
print(f"API error {e.status_code}: {e.data.code} - {e.data.error}")
Upstream error with details
from kycgenie.errors import KYCGenieUpstreamError
try:
result = client.webhooks.test_delivery(
webhook_url="https://example.com/webhook",
)
print(f"Delivered - status: {result.http_status_code}")
except KYCGenieUpstreamError as e:
remote = e.remote_status_code # int or None
if remote:
print(f"Endpoint returned HTTP {remote}")
else:
print("Endpoint was unreachable")
print(f"Request ID: {e.data.request_id}")
Accessing field-level errors
from kycgenie.errors import KYCGenieValidationError
try:
client.entities.create(legal_name="", entity_type="unknown")
except KYCGenieValidationError as e:
for field, messages in (e.data.errors or {}).items():
for msg in messages:
print(f" {field}: {msg}")
# legal_name: This field may not be blank.
# entity_type: Must be 'company' or 'individual'.