Skip to main content

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

FieldAlways presentDescription
successYestrue for 2xx responses, false for 4xx/5xx
statusYesHTTP status code, mirrored in the body for convenience
messageYesHuman-readable summary of the result or error
dataOn successThe resource or array of resources
paginationOn list successtotal, limit, offset, hasMore
errorOn failurecode (machine-readable) + detail (human-readable) + optional fields array
metaYesrequestId, 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

StatusWhen it is returned
200 OKRequest succeeded. Resource returned or operation applied.
201 CreatedResource created successfully.
400 Bad RequestInvalid JSON body or a structurally malformed request.
401 UnauthorizedAPI key missing, invalid, revoked, or expired.
403 ForbiddenIP not in allowlist, business suspended, or key lacks the required scope.
404 Not FoundResource does not exist or does not belong to your business/environment.
409 ConflictResource state conflict (e.g. card already frozen, duplicate idempotency key with different body).
422 Unprocessable EntityRequest was understood but failed business validation rules.
429 Too Many RequestsRate limit exceeded.
500 Internal Server ErrorUnexpected error on FYATU’s end.

Error Code Catalogue

Authentication and Authorization

CodeHTTPDescription
API_KEY_INVALID401API key is malformed or does not exist in this environment
API_KEY_REVOKED401API key was explicitly revoked in the portal
API_KEY_EXPIRED401API key has passed its expiry date
IP_NOT_ALLOWED403Client IP is not in the key’s IP allowlist
BUSINESS_SUSPENDED403Business account is suspended
BUSINESS_CLOSED403Business account is permanently closed
INSUFFICIENT_SCOPE403Key lacks the required scope for this endpoint

Validation

CodeHTTPDescription
VALIDATION_ERROR422One or more request fields failed validation — see error.fields
INVALID_BODY400Request body is not valid JSON
IDEMPOTENCY_KEY_TOO_LONG422Idempotency-Key header exceeds 255 characters

General

CodeHTTPDescription
RESOURCE_NOT_FOUND404The requested resource does not exist or does not belong to your business
CONFLICT409General conflict — see message for the specific reason
RATE_LIMITED429Request rate exceeded — check X-RateLimit-Reset header
INTERNAL_ERROR500Unexpected server error — contact support with meta.requestId

Program Errors

CodeHTTPDescription
PROGRAM_NOT_FOUND404Program not found or belongs to another business or environment
PROGRAM_CLOSED409Program is closed and cannot accept new cardholders or cards
PROGRAM_INACTIVE422Program is paused — cards cannot be issued until it is resumed

Cardholder Errors

CodeHTTPDescription
CARDHOLDER_NOT_FOUND404Cardholder not found or belongs to another business
CARDHOLDER_EMAIL_EXISTS409Email already used by another cardholder in this environment
CARDHOLDER_UNDER_AGE422Cardholder must be at least 18 years old
CARDHOLDER_INACTIVE422Cardholder is suspended or terminated — operation not permitted
CARDHOLDER_TERMINATED409Cardholder is terminated and cannot be modified
KYC_FIELD_LOCKED409Field cannot be changed after KYC approval
ALREADY_SUSPENDED409Cardholder is already suspended
ALREADY_ACTIVE409Cardholder is already active

Card Errors

CodeHTTPDescription
CARD_NOT_FOUND404Card not found or belongs to another business or environment
CARD_FROZEN422Cannot fund or otherwise operate on a frozen card
CARD_ALREADY_TERMINATED409Card is already terminated
CARD_HAS_PENDING_TRANSACTIONS409Card has pending authorizations and cannot be terminated yet
CARDHOLDER_KYC_NOT_APPROVED422Card cannot be issued — cardholder KYC is not APPROVED
CARD_ALREADY_FROZEN409Card is already frozen
CARD_NOT_FROZEN409Cannot unfreeze — card is not frozen
CARD_STATUS_CONFLICT409Concurrent status change — wait and retry
CONFIRMATION_REQUIRED400Termination requires "confirm": "TERMINATE_CARD" in the body

Balance Errors

CodeHTTPDescription
INSUFFICIENT_PROGRAM_BALANCE422Program ledger balance too low to fund the card
INSUFFICIENT_CARD_BALANCE422Card balance too low to unload the requested amount
INVALID_AMOUNT422Amount must be greater than 0, or exceeds the program load cap

Withdrawal Errors

CodeHTTPDescription
WITHDRAWAL_ADDRESS_LOCKED422Withdrawal address was changed within the last 48 hours and is locked for security

Webhook Errors

CodeHTTPDescription
WEBHOOK_NOT_FOUND404Webhook not found or belongs to another business or environment
WEBHOOK_LIMIT_REACHED422Maximum of 10 webhook endpoints per environment
WEBHOOK_HTTPS_REQUIRED422LIVE environment requires an HTTPS webhook URL
INVALID_EVENT_TYPE422Unrecognised 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 classRetryableRecommended action
200 / 201Success — no retry needed
4xx (except 429)NoFix the request and resubmit
429YesWait until X-RateLimit-Reset, then retry with backoff
5xxYesExponential backoff — max 4 attempts
Node.js — retry helper
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));
  }
}
Python — retry helper
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

Node.js
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;
}
Python
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']

Using meta.requestId for Support

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:
  1. The meta.requestId from the failing response
  2. The HTTP status code
  3. 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

Error
{
  "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

Error
{
  "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

Error
{
  "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

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