> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fyatu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Unified response envelope, HTTP status codes, complete error code catalogue, retry strategies, and how to use requestId for support — FYATU CaaS API v3.20.

# 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

```json theme={null}
{
  "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

```json theme={null}
{
  "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

```json theme={null}
{
  "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:

```json theme={null}
{
  "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`                                            |

<Tip>
  Use `error.code` in your `switch` statements for programmatic handling. Use `message` for displaying to end-users. Use `meta.requestId` when contacting support.
</Tip>

## 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                                                                                                                                                                       |
| `CARD_CREATION_UNAVAILABLE`     | 503  | Card creation is temporarily unavailable (paused by Fyatu, or a transient issue on our side) — **retryable**                                                                                                                         |
| `CARD_FUNDING_UNAVAILABLE`      | 503  | Card funding is temporarily unavailable (paused by Fyatu, or a transient processing issue on our side). Your program balance is **not** affected — **retryable**. Distinct from `INSUFFICIENT_PROGRAM_BALANCE`, which needs a top-up |
| `CARD_WITHDRAWAL_UNAVAILABLE`   | 503  | Card withdrawal/unload is temporarily unavailable (paused by Fyatu, or a transient issue on our side) — **retryable**                                                                                                                |

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

```javascript Node.js — retry helper theme={null}
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 Python — retry helper theme={null}
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

```javascript Node.js theme={null}
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 Python theme={null}
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

```json Error theme={null}
{
  "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

```json Error theme={null}
{
  "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

```json Error theme={null}
{
  "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

```json Error theme={null}
{
  "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.
