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

# Authentication

> API key authentication for the FYATU CaaS API v3.20. Generate keys in the portal, pass them as Bearer tokens, scope them to exactly the permissions you need, and use Idempotency-Key to safely retry write operations.

# Authentication

The FYATU CaaS API v3.20 uses **API key authentication** — no tokens to exchange, no JWTs to decode. Every request must include a valid API key, and the key's scopes determine what the request is allowed to do.

## How It Works

```
┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│   Your server                FYATU CaaS API                        │
│                                                                     │
│   GET /cardholders  ──────────────────────────────────────────────► │
│   Authorization: Bearer fyatu_live_abc123...                        │
│                                                                     │
│                       ① Validate key (SHA-256 hash lookup)         │
│                       ② Check key status (ACTIVE / REVOKED)        │
│                       ③ Check IP allowlist (if configured)         │
│                       ④ Check business status (ACTIVE)             │
│                       ⑤ Sliding-window rate limit (1,000/min)      │
│                       ⑥ Enforce scope (cardholders:read)           │
│                                                                     │
│                ◄──────────────────────────────────────────────────  │
│   200 OK + cardholder list                                          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
```

## Getting Your API Key

<Steps>
  <Step title="Open the CaaS portal">
    Go to [platform.fyatu.com](https://platform.fyatu.com) and log in to your CaaS account.
  </Step>

  <Step title="Navigate to API Keys">
    Click **Developer** → **API Keys** in the left sidebar.
  </Step>

  <Step title="Create an API key">
    Click **Create API Key**. Name the key, choose its environment (`LIVE` or `SANDBOX`), and select the scopes it needs.
  </Step>

  <Step title="Copy the key immediately">
    The full key is shown **once** at creation time. Copy it to your secrets manager now — it cannot be retrieved later.
  </Step>
</Steps>

<Warning>
  API keys are shown **once** at creation. If you lose a key, revoke it and create a new one. Never log keys or commit them to version control.
</Warning>

## Sending the API Key

Pass your key in the `Authorization` header as a Bearer token. This is the only supported method.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET https://api.fyatu.com/api/v3.20/cardholders \
    -H "Authorization: Bearer fyatu_live_abc123def456ghi789jkl012mno345pqr678stu901vwx234"
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.fyatu.com/api/v3.20/cardholders', {
    headers: {
      'Authorization': `Bearer ${process.env.FYATU_API_KEY}`,
      'Content-Type': 'application/json'
    }
  });
  ```

  ```python Python theme={null}
  import os, requests

  resp = requests.get(
      'https://api.fyatu.com/api/v3.20/cardholders',
      headers={'Authorization': f'Bearer {os.environ["FYATU_API_KEY"]}'}
  )
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("GET", "https://api.fyatu.com/api/v3.20/cardholders", nil)
  req.Header.Set("Authorization", "Bearer "+os.Getenv("FYATU_API_KEY"))
  resp, _ := http.DefaultClient.Do(req)
  ```

  ```php PHP theme={null}
  $ch = curl_init('https://api.fyatu.com/api/v3.20/cardholders');
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          'Authorization: Bearer ' . getenv('FYATU_API_KEY'),
          'Content-Type: application/json',
      ],
  ]);
  $resp = json_decode(curl_exec($ch), true);
  ```
</CodeGroup>

## Authentication Failure Response

When authentication fails, the API returns an error using the standard response envelope:

```json 401 — Invalid API key theme={null}
{
  "success": false,
  "status": 401,
  "message": "API key is invalid or does not exist",
  "error": {
    "code": "API_KEY_INVALID",
    "detail": "The provided API key was not found in this environment"
  },
  "meta": {
    "requestId": "req_01HXY123456ABCDEF",
    "platform": "Fyatu CaaS",
    "timestamp": "2026-05-22T10:00:00Z"
  }
}
```

```json 403 — Insufficient scope theme={null}
{
  "success": false,
  "status": 403,
  "message": "API key does not have the required scope",
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "detail": "This endpoint requires the cards:write scope"
  },
  "meta": {
    "requestId": "req_01HXY123456ABCDEF",
    "platform": "Fyatu CaaS",
    "timestamp": "2026-05-22T10:00:00Z"
  }
}
```

## Environment Isolation

Each API key is bound to a single environment. Keys issued for `SANDBOX` only work against sandbox data; `LIVE` keys only against live data. The environment is enforced server-side — you cannot mix data between environments and attempting to do so returns `API_KEY_INVALID`.

| Environment | Purpose                                                                   |
| ----------- | ------------------------------------------------------------------------- |
| `SANDBOX`   | Development and testing. No real card issuance. KYC completes in seconds. |
| `LIVE`      | Production. Real cardholders, real cards, real transactions.              |

<Tip>
  Always build and test against `SANDBOX` before switching to `LIVE`. The two environments have separate programs, cardholders, and cards — nothing carries over.
</Tip>

## Scopes

API keys are scoped to specific resource permissions. A key without a scope cannot call endpoints that require it — it receives a `403 INSUFFICIENT_SCOPE` response. Assign only the scopes your integration actually needs.

| Scope               | What It Grants                                                                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cardholders:read`  | `GET /cardholders`, `GET /cardholders/{id}`                                                                                                             |
| `cardholders:write` | `POST /cardholders`, `PATCH /cardholders/{id}`, `POST /cardholders/{id}/suspend`, `POST /cardholders/{id}/reactivate`                                   |
| `cards:read`        | `GET /cards`, `GET /cards/{id}`                                                                                                                         |
| `cards:write`       | `POST /cards`, `POST /cards/{id}/freeze`, `POST /cards/{id}/unfreeze`, `POST /cards/{id}/terminate`, `POST /cards/{id}/fund`, `POST /cards/{id}/unload` |
| `transactions:read` | `GET /transactions`, `GET /transactions/{id}`                                                                                                           |
| `webhooks:read`     | `GET /webhooks`, `GET /webhooks/{id}`                                                                                                                   |
| `webhooks:write`    | `POST /webhooks`, `PATCH /webhooks/{id}`, `DELETE /webhooks/{id}`                                                                                       |

<Tip>
  Create separate keys for separate services. A read-only reporting service should only have `*:read` scopes. A compromised minimal-scope key limits the blast radius.
</Tip>

## Idempotency-Key Header

For write operations (`POST`), you can supply an `Idempotency-Key` header to safely retry requests without risking duplicate operations.

```bash theme={null}
curl -X POST https://api.fyatu.com/api/v3.20/cards/crd_01HXYZ.../fund \
  -H "Authorization: Bearer $FYATU_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: fund-card-crd01HXYZ-order-9991" \
  -d '{ "amount": 50.00 }'
```

**How it works:**

* If a request with the same `Idempotency-Key` is received within **24 hours**, the API replays the original response without re-executing the operation.
* Replayed responses include the header `Idempotency-Replayed: true`.
* Keys must be unique strings of up to 255 characters. A UUID or a deterministic hash of your operation parameters works well.
* After 24 hours, the key expires and a request with the same key is treated as a new operation.

```bash theme={null}
# Replayed response headers
Idempotency-Replayed: true
```

<Warning>
  If you send the same `Idempotency-Key` with a different request body, the API returns `422 IDEMPOTENCY_KEY_TOO_LONG` if the key is malformed, or `409 CONFLICT` if the body does not match the original request.
</Warning>

**When to use it:** Fund card, create cardholder, issue card, and any write operation that should not be duplicated if your network times out and you retry.

## Rate Limits

The CaaS API enforces a **sliding-window rate limit** of **1,000 requests per minute** per API key. Every response includes rate-limit headers:

| Header                  | Description                              |
| ----------------------- | ---------------------------------------- |
| `X-RateLimit-Limit`     | Your key's request cap per minute        |
| `X-RateLimit-Remaining` | Requests remaining in the current window |
| `X-RateLimit-Reset`     | Unix timestamp when the window resets    |

When you exceed the limit, the API returns `429 RATE_LIMITED`. Implement exponential backoff:

```javascript theme={null}
async function withBackoff(fn, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fn();
    if (res.status !== 429) return res;
    const resetAt = res.headers.get('X-RateLimit-Reset');
    const wait = resetAt
      ? (Number(resetAt) * 1000 - Date.now())
      : (2 ** attempt * 1000);
    await new Promise(r => setTimeout(r, Math.max(wait, 1000)));
  }
  throw new Error('Rate limit retries exhausted');
}
```

## IP Allowlisting

API keys can optionally restrict which IP addresses may use them. If you configure an IP allowlist in the portal and a request arrives from an unlisted IP, the API returns `403 IP_NOT_ALLOWED`.

This is strongly recommended for server-to-server `LIVE` integrations. Leave the allowlist empty during development or when your server IPs are dynamic.

## Authentication Error Reference

| Code                 | HTTP | Cause                                                  |
| -------------------- | ---- | ------------------------------------------------------ |
| `API_KEY_INVALID`    | 401  | Key does not exist in this environment or is malformed |
| `API_KEY_REVOKED`    | 401  | Key was explicitly revoked in the portal               |
| `API_KEY_EXPIRED`    | 401  | Key passed its expiry date                             |
| `IP_NOT_ALLOWED`     | 403  | Client IP is not in the key's allowlist                |
| `BUSINESS_SUSPENDED` | 403  | Business account is suspended                          |
| `BUSINESS_CLOSED`    | 403  | Business account is permanently closed                 |
| `INSUFFICIENT_SCOPE` | 403  | Key does not have the required scope                   |
| `RATE_LIMITED`       | 429  | Request rate exceeded                                  |

## Best Practices

<AccordionGroup>
  <Accordion title="Store keys in secrets managers">
    Never hard-code API keys in source files. Use environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler, etc.). Rotate keys at least every 90 days.
  </Accordion>

  <Accordion title="Use separate keys per service">
    Issue one key per microservice or integration, each with the minimal required scopes. This lets you revoke a single key without disrupting other services.
  </Accordion>

  <Accordion title="Configure IP allowlisting for LIVE">
    For production workloads, set an IP allowlist on your LIVE API keys. This adds a layer of defense even if a key is leaked — the attacker cannot use it from an unlisted IP.
  </Accordion>

  <Accordion title="Use Idempotency-Key on all write operations">
    Network failures happen. Always supply `Idempotency-Key` on fund, issue, and create requests so a retry never double-charges your program balance.
  </Accordion>

  <Accordion title="Monitor your rate limit headers">
    Log `X-RateLimit-Remaining` on every response. If it consistently approaches zero, batch requests, cache reads, or contact support to request a higher rate limit.
  </Accordion>
</AccordionGroup>
