Error Handling
The FYATU CaaS API v3.20 uses a unified response envelope for every response — success and error alike. The top-level shape is always the same; only the fields that are populated change.
Response Envelope
Success — single resource
{
"success": true,
"status": 200,
"message": "Card retrieved",
"data": {
"cardId": "crd_01HXYZ5555ABCDEF1111",
"status": "ACTIVE"
},
"meta": {
"requestId": "req_01HXY123456ABCDEF",
"platform": "Fyatu CaaS",
"timestamp": "2026-05-22T10:00:00Z"
}
}
Success — paginated list
{
"success": true,
"status": 200,
"message": "Cards retrieved",
"data": [ ... ],
"pagination": {
"total": 47,
"limit": 20,
"offset": 0,
"hasMore": true
},
"meta": {
"requestId": "req_01HXY123456ABCDEF",
"platform": "Fyatu CaaS",
"timestamp": "2026-05-22T10:00:00Z"
}
}
Error
{
"success": false,
"status": 422,
"message": "Amount must be greater than zero",
"error": {
"code": "INVALID_AMOUNT",
"detail": "The amount field must be a positive integer representing cents"
},
"meta": {
"requestId": "req_01HXY123456ABCDEF",
"platform": "Fyatu CaaS",
"timestamp": "2026-05-22T10:00:00Z"
}
}
Validation error (multiple fields)
When a request fails validation, the error object includes a fields array:
{
"success": false,
"status": 422,
"message": "Validation failed",
"error": {
"code": "VALIDATION_ERROR",
"detail": "One or more request fields are invalid",
"fields": [
{ "field": "dateOfBirth", "message": "dateOfBirth must be in YYYY-MM-DD format" },
{ "field": "email", "message": "email must be a valid email address" }
]
},
"meta": {
"requestId": "req_01HXY123456ABCDEF",
"platform": "Fyatu CaaS",
"timestamp": "2026-05-22T10:00:00Z"
}
}
Envelope Fields
| Field | Always present | Description |
|---|
success | Yes | true for 2xx responses, false for 4xx/5xx |
status | Yes | HTTP status code, mirrored in the body for convenience |
message | Yes | Human-readable summary of the result or error |
data | On success | The resource or array of resources |
pagination | On list success | total, limit, offset, hasMore |
error | On failure | code (machine-readable) + detail (human-readable) + optional fields array |
meta | Yes | requestId, platform, timestamp |
Use error.code in your switch statements for programmatic handling. Use message for displaying to end-users. Use meta.requestId when contacting support.
HTTP Status Codes
| Status | When it is returned |
|---|
200 OK | Request succeeded. Resource returned or operation applied. |
201 Created | Resource created successfully. |
400 Bad Request | Invalid JSON body or a structurally malformed request. |
401 Unauthorized | API key missing, invalid, revoked, or expired. |
403 Forbidden | IP not in allowlist, business suspended, or key lacks the required scope. |
404 Not Found | Resource does not exist or does not belong to your business/environment. |
409 Conflict | Resource state conflict (e.g. card already frozen, duplicate idempotency key with different body). |
422 Unprocessable Entity | Request was understood but failed business validation rules. |
429 Too Many Requests | Rate limit exceeded. |
500 Internal Server Error | Unexpected error on FYATU’s end. |
Error Code Catalogue
Authentication and Authorization
| Code | HTTP | Description |
|---|
API_KEY_INVALID | 401 | API key is malformed or does not exist in this environment |
API_KEY_REVOKED | 401 | API key was explicitly revoked in the portal |
API_KEY_EXPIRED | 401 | API key has passed its expiry date |
IP_NOT_ALLOWED | 403 | Client IP is not in the key’s IP allowlist |
BUSINESS_SUSPENDED | 403 | Business account is suspended |
BUSINESS_CLOSED | 403 | Business account is permanently closed |
INSUFFICIENT_SCOPE | 403 | Key lacks the required scope for this endpoint |
Validation
| Code | HTTP | Description |
|---|
VALIDATION_ERROR | 422 | One or more request fields failed validation — see error.fields |
INVALID_BODY | 400 | Request body is not valid JSON |
IDEMPOTENCY_KEY_TOO_LONG | 422 | Idempotency-Key header exceeds 255 characters |
General
| Code | HTTP | Description |
|---|
RESOURCE_NOT_FOUND | 404 | The requested resource does not exist or does not belong to your business |
CONFLICT | 409 | General conflict — see message for the specific reason |
RATE_LIMITED | 429 | Request rate exceeded — check X-RateLimit-Reset header |
INTERNAL_ERROR | 500 | Unexpected server error — contact support with meta.requestId |
Program Errors
| Code | HTTP | Description |
|---|
PROGRAM_NOT_FOUND | 404 | Program not found or belongs to another business or environment |
PROGRAM_CLOSED | 409 | Program is closed and cannot accept new cardholders or cards |
PROGRAM_INACTIVE | 422 | Program is paused — cards cannot be issued until it is resumed |
Cardholder Errors
| Code | HTTP | Description |
|---|
CARDHOLDER_NOT_FOUND | 404 | Cardholder not found or belongs to another business |
CARDHOLDER_EMAIL_EXISTS | 409 | Email already used by another cardholder in this environment |
CARDHOLDER_UNDER_AGE | 422 | Cardholder must be at least 18 years old |
CARDHOLDER_INACTIVE | 422 | Cardholder is suspended or terminated — operation not permitted |
CARDHOLDER_TERMINATED | 409 | Cardholder is terminated and cannot be modified |
KYC_FIELD_LOCKED | 409 | Field cannot be changed after KYC approval |
ALREADY_SUSPENDED | 409 | Cardholder is already suspended |
ALREADY_ACTIVE | 409 | Cardholder is already active |
Card Errors
| Code | HTTP | Description |
|---|
CARD_NOT_FOUND | 404 | Card not found or belongs to another business or environment |
CARD_FROZEN | 422 | Cannot fund or otherwise operate on a frozen card |
CARD_ALREADY_TERMINATED | 409 | Card is already terminated |
CARD_HAS_PENDING_TRANSACTIONS | 409 | Card has pending authorizations and cannot be terminated yet |
CARDHOLDER_KYC_NOT_APPROVED | 422 | Card cannot be issued — cardholder KYC is not APPROVED |
CARD_ALREADY_FROZEN | 409 | Card is already frozen |
CARD_NOT_FROZEN | 409 | Cannot unfreeze — card is not frozen |
CARD_STATUS_CONFLICT | 409 | Concurrent status change — wait and retry |
CONFIRMATION_REQUIRED | 400 | Termination requires "confirm": "TERMINATE_CARD" in the body |
Balance Errors
| Code | HTTP | Description |
|---|
INSUFFICIENT_PROGRAM_BALANCE | 422 | Program ledger balance too low to fund the card |
INSUFFICIENT_CARD_BALANCE | 422 | Card balance too low to unload the requested amount |
INVALID_AMOUNT | 422 | Amount must be greater than 0, or exceeds the program load cap |
Withdrawal Errors
| Code | HTTP | Description |
|---|
WITHDRAWAL_ADDRESS_LOCKED | 422 | Withdrawal address was changed within the last 48 hours and is locked for security |
Webhook Errors
| Code | HTTP | Description |
|---|
WEBHOOK_NOT_FOUND | 404 | Webhook not found or belongs to another business or environment |
WEBHOOK_LIMIT_REACHED | 422 | Maximum of 10 webhook endpoints per environment |
WEBHOOK_HTTPS_REQUIRED | 422 | LIVE environment requires an HTTPS webhook URL |
INVALID_EVENT_TYPE | 422 | Unrecognised event type in the events array |
Retry Strategy
Only 429 and 5xx errors should be retried. 4xx errors (except 429) indicate a problem with the request — retrying without fixing it will not succeed.
| Error class | Retryable | Recommended action |
|---|
200 / 201 | — | Success — no retry needed |
4xx (except 429) | No | Fix the request and resubmit |
429 | Yes | Wait until X-RateLimit-Reset, then retry with backoff |
5xx | Yes | Exponential backoff — max 4 attempts |
async function withRetry(fn, maxAttempts = 4) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = await fn();
const body = await res.clone().json();
if (res.ok) return body;
const isRetryable = res.status === 429 || res.status >= 500;
if (!isRetryable || attempt === maxAttempts) {
throw Object.assign(new Error(body.message), {
code: body.error?.code,
requestId: body.meta?.requestId,
status: res.status,
});
}
// On 429, wait for the rate limit window to reset
const resetAt = res.headers.get('X-RateLimit-Reset');
const waitMs = resetAt
? Math.max(Number(resetAt) * 1000 - Date.now(), 1000)
: Math.min(2 ** attempt * 500, 30000);
await new Promise(r => setTimeout(r, waitMs));
}
}
import time, os, requests
def with_retry(fn, max_attempts=4):
for attempt in range(1, max_attempts + 1):
resp = fn()
body = resp.json()
if resp.ok:
return body
retryable = resp.status_code == 429 or resp.status_code >= 500
if not retryable or attempt == max_attempts:
raise RuntimeError(
f"[{body.get('error', {}).get('code', 'UNKNOWN')}] "
f"{body.get('message')} "
f"(requestId: {body.get('meta', {}).get('requestId')})"
)
if resp.status_code == 429:
reset_at = resp.headers.get('X-RateLimit-Reset')
wait = max(int(reset_at) - time.time(), 1) if reset_at else 2 ** attempt
else:
wait = min(2 ** attempt * 0.5, 30)
time.sleep(wait)
Error Handling Patterns
Inspect error.code for programmatic handling
async function issueCard(payload) {
const res = await fetch('https://api.fyatu.com/api/v3.20/cards', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FYATU_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const body = await res.json();
if (!body.success) {
switch (body.error?.code) {
case 'CARDHOLDER_KYC_NOT_APPROVED':
throw new Error('Cardholder KYC is still pending — wait for CARDHOLDER_KYC_APPROVED webhook');
case 'INSUFFICIENT_PROGRAM_BALANCE':
throw new Error('Top up your program balance before issuing cards');
case 'INSUFFICIENT_SCOPE':
throw new Error('API key is missing the cards:write scope');
case 'RATE_LIMITED':
throw Object.assign(new Error('Rate limited'), { resetAt: res.headers.get('X-RateLimit-Reset') });
default:
throw new Error(`${body.error?.code}: ${body.message} (requestId: ${body.meta?.requestId})`);
}
}
return body.data;
}
def issue_card(payload):
resp = requests.post(
'https://api.fyatu.com/api/v3.20/cards',
headers={'Authorization': f'Bearer {os.environ["FYATU_API_KEY"]}'},
json=payload,
)
body = resp.json()
if not body['success']:
code = body.get('error', {}).get('code', 'UNKNOWN')
message = body.get('message', '')
requestId = body.get('meta', {}).get('requestId', '')
raise RuntimeError(f'{code}: {message} (requestId: {requestId})')
return body['data']
Every response — success or error — includes a meta.requestId. This ID uniquely identifies the request in FYATU’s systems and allows the support team to trace exactly what happened.
When contacting support about an unexpected error, always include:
- The
meta.requestId from the failing response
- The HTTP status code
- The
error.code value
Hi FYATU support,
I'm getting an unexpected 500 INTERNAL_ERROR on POST /cards.
requestId: req_01HXY123456ABCDEF
Common Errors by Scenario
Issuing a card — cardholder KYC not yet approved
{
"success": false,
"status": 422,
"message": "Cardholder KYC is not approved",
"error": {
"code": "CARDHOLDER_KYC_NOT_APPROVED",
"detail": "The cardholder must have kycStatus APPROVED before a card can be issued"
},
"meta": { "requestId": "req_01HXY...", "platform": "Fyatu CaaS", "timestamp": "2026-05-22T10:00:00Z" }
}
Fix: Subscribe to the CARDHOLDER_KYC_APPROVED webhook. Issue the card only after the event fires.
Funding a card with insufficient program balance
{
"success": false,
"status": 422,
"message": "Insufficient program balance",
"error": {
"code": "INSUFFICIENT_PROGRAM_BALANCE",
"detail": "Program balance is $0.00. Required: $50.00"
},
"meta": { "requestId": "req_01HXY...", "platform": "Fyatu CaaS", "timestamp": "2026-05-22T10:00:00Z" }
}
Fix: Deposit funds to your program via the CaaS portal or the deposit workflow, then retry.
Terminating a card with pending transactions
{
"success": false,
"status": 409,
"message": "Card has pending transactions and cannot be terminated",
"error": {
"code": "CARD_HAS_PENDING_TRANSACTIONS",
"detail": "Wait for all pending authorizations to clear before terminating"
},
"meta": { "requestId": "req_01HXY...", "platform": "Fyatu CaaS", "timestamp": "2026-05-22T10:00:00Z" }
}
Fix: Wait for pending authorizations to clear (typically 24–72 hours), then retry.
Missing termination confirmation
{
"success": false,
"status": 400,
"message": "Termination requires explicit confirmation",
"error": {
"code": "CONFIRMATION_REQUIRED",
"detail": "Include \"confirm\": \"TERMINATE_CARD\" in the request body"
},
"meta": { "requestId": "req_01HXY...", "platform": "Fyatu CaaS", "timestamp": "2026-05-22T10:00:00Z" }
}
Fix: Include "confirm": "TERMINATE_CARD" in the request body.