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

# Authentication

> Every Kynva API request is authenticated. There are two ways to do it: API keys (server-to-server) and short-lived JWTs (browser / on-behalf-of).

The Kynva API authenticates every request with a bearer token in the `Authorization` header:

```http theme={null}
Authorization: Bearer <token>
```

There are two kinds of token. Pick based on **who is making the call**:

| Use case                                     | Token type                                    | Lifetime                   |
| -------------------------------------------- | --------------------------------------------- | -------------------------- |
| Server-to-server (your backend, CI, scripts) | **API key** (`kyn_live_...` / `kyn_test_...`) | Long-lived — until rotated |
| End-user request from your frontend          | **JWT** (issued by your auth provider)        | Short — minutes            |

<Warning>
  **Never put an API key in browser, mobile, or desktop client code.** API keys grant full access to your workspace. If you need to call Kynva from a user's device, mint a JWT on your backend and forward that.
</Warning>

## Method 1 — API key (Bearer)

API keys live in [Settings → API Keys](https://www.kynva.ai/api-keys). Each key has:

| Field                   | Notes                                                                                                           |
| ----------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Prefix**              | `kyn_live_` (charges credits) or `kyn_test_` (free, sandbox). Legacy `rf_live_` / `rf_test_` keys remain valid. |
| **Scopes**              | What the key can do (`render:write`, `brands:read`, etc.). Scope down whenever possible.                        |
| **Created / Last used** | Visible in the dashboard. Use this to find stale keys.                                                          |

### Send the key

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.kynva.ai/api/v1/facade/health \
    -H "Authorization: Bearer kyn_live_abc123..."
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch("https://api.kynva.ai/api/v1/facade/health", {
    headers: {
      Authorization: `Bearer ${process.env.KYNVA_API_KEY}`,
    },
  });
  ```

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

  res = requests.get(
      "https://api.kynva.ai/api/v1/facade/health",
      headers={"Authorization": f"Bearer {os.environ['KYNVA_API_KEY']}"},
  )
  res.raise_for_status()
  ```
</CodeGroup>

### Rotate a key

1. In the dashboard, click **Rotate** on the key you want to replace.
2. The new key is shown once. Update your secrets store immediately.
3. The old key keeps working for a 24-hour grace window. After that it returns [`UNAUTHORIZED`](/errors/codes#unauthorized).

<Tip>
  Schedule a quarterly rotation. Kynva sends an email reminder at the 80-day mark for any key older than 90 days.
</Tip>

### Store keys safely

* **Backend**: load from your secret manager (AWS Secrets Manager, GCP Secret Manager, Doppler, 1Password Connect). Never commit to git.
* **Local dev**: use a `.env` file ignored by git, or your shell's keychain.
* **CI**: provision as a masked secret in GitHub Actions / GitLab CI / etc.

If a key leaks, **rotate immediately**. The dashboard shows last-used timestamp and source IP — use both to audit blast radius.

## Method 2 — JWT (end-user, short-lived)

If your product makes Kynva calls **on behalf of an end user** (e.g., a browser app where the user pays per render), use JWTs instead of API keys.

### How it works

```
[Your frontend]  ──► [Your backend]  ──► [Kynva]
                          │                  │
                          │ mints JWT        │ verifies JWT
                          │ (signs with      │ via JWKS
                          │  your private    │
                          │  key)            │
```

1. Configure your **JWT issuer** in [Settings → Auth](https://www.kynva.ai/settings) — issuer URL + JWKS endpoint.
2. Your backend mints a JWT for the requesting user. Include:
   * `sub` — the user's stable ID in your system
   * `aud: "kynva"`
   * `exp` — short. 5–15 minutes is typical.
3. Pass the JWT as the bearer token.

### Send the JWT

<CodeGroup>
  ```bash curl theme={null}
  # Any authenticated endpoint works the same way — listing brands here;
  # rendering uses POST /api/v1/facade/generate (see the quickstart).
  curl https://api.kynva.ai/api/v1/facade/brands \
    -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
    -H "X-API-Version: 2026-01-01"
  ```

  ```typescript TypeScript (Node, JWT minted with jose) theme={null}
  import { SignJWT } from "jose";
  import { randomUUID } from "node:crypto";

  // On your backend — never ship the private key to clients
  const privateKey = await loadPrivateKey(); // your secrets store

  async function mintUserJwt(userId: string) {
    return await new SignJWT({})
      .setProtectedHeader({ alg: "RS256", kid: "kynva-2026-01" })
      .setIssuer("https://your-app.example.com")
      .setAudience("kynva")
      .setSubject(userId)
      .setIssuedAt()
      .setExpirationTime("10m")
      .sign(privateKey);
  }

  const jwt = await mintUserJwt("user_42");

  // Any authenticated endpoint works the same way — rendering uses
  // POST /api/v1/facade/generate (see the quickstart).
  await fetch("https://api.kynva.ai/api/v1/facade/brands", {
    headers: {
      Authorization: `Bearer ${jwt}`,
      "X-API-Version": "2026-01-01",
    },
  });
  ```

  ```python Python (JWT minted with PyJWT) theme={null}
  import os, uuid, time, jwt, requests

  # On your backend — load your RSA private key from secrets
  with open("/secrets/kynva-jwt-private.pem", "rb") as f:
      private_key = f.read()

  def mint_user_jwt(user_id: str) -> str:
      return jwt.encode(
          {
              "iss": "https://your-app.example.com",
              "aud": "kynva",
              "sub": user_id,
              "iat": int(time.time()),
              "exp": int(time.time()) + 600,  # 10 minutes
          },
          private_key,
          algorithm="RS256",
          headers={"kid": "kynva-2026-01"},
      )

  token = mint_user_jwt("user_42")

  # Any authenticated endpoint works the same way — rendering uses
  # POST /api/v1/facade/generate (see the quickstart).
  res = requests.get(
      "https://api.kynva.ai/api/v1/facade/brands",
      headers={
          "Authorization": f"Bearer {token}",
          "X-API-Version": "2026-01-01",
      },
  )
  res.raise_for_status()
  ```
</CodeGroup>

### JWT requirements

| Claim | Required    | Notes                                                                 |
| ----- | ----------- | --------------------------------------------------------------------- |
| `iss` | yes         | Must match the issuer URL configured in your workspace.               |
| `aud` | yes         | Must be `kynva`.                                                      |
| `sub` | yes         | Stable user identifier. Used for usage attribution and rate limits.   |
| `exp` | yes         | Must be in the future and within 24h of `iat`. We recommend ≤ 15 min. |
| `iat` | recommended | Issued-at timestamp.                                                  |
| `nbf` | optional    | If set, must be ≤ now.                                                |

Tokens with missing or invalid claims are rejected with [`UNAUTHORIZED`](/errors/codes#unauthorized).

## Verifying a request succeeded

A successful authenticated request includes:

```http theme={null}
HTTP/1.1 200 OK
X-Request-Id: req_2k4j8h9d
X-RateLimit-Remaining: 119
```

A failed authentication request returns:

```http theme={null}
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication required",
    "request_id": "req_2k4j8h9d"
  }
}
```

See [error codes](/errors/codes) for the full taxonomy.

## Frequently asked

<AccordionGroup>
  <Accordion title="My legacy rf_live_ key — do I need to migrate?">
    No. Legacy `rf_*` keys keep working indefinitely. New keys are issued with the `kyn_` prefix. Nothing about the wire protocol changed.
  </Accordion>

  <Accordion title="Can I scope a key to a single brand?">
    Yes. In the dashboard, edit a key and add `brand:brand_abc` to its scope. The key will return [`FORBIDDEN`](/errors/codes#forbidden) if used against any other brand.
  </Accordion>

  <Accordion title="Do test keys (kyn_test_) talk to the same backend?">
    Yes. Same backend, same code, but renders made with a test key are watermarked and don't consume credits.
  </Accordion>

  <Accordion title="What's the rate limit per key vs per workspace?">
    Limits apply per workspace, not per key. See [Rate limits](/concepts/rate-limits) for the per-category caps.
  </Accordion>
</AccordionGroup>
