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

# Authentication

> How to authenticate with the Finvera API from browsers and from servers.

<Tip>
  Create your account and provision keys at the [API Key Dashboard](https://dashboard.finvera.ai/sign-up).
</Tip>

Finvera supports two authentication models. Pick the one that matches where your code runs:

| Key type            | Prefix               | Where it runs                           | How it authenticates                                      |
| ------------------- | -------------------- | --------------------------------------- | --------------------------------------------------------- |
| **Publishable key** | `pk_…`               | Browser / mobile / any client-side code | Exchanged for a short‑lived session token at the edge     |
| **Secret key**      | `sk_…` / `finvera_…` | Trusted servers only                    | Sent directly on every request as `Authorization: Bearer` |

The base URL for all requests is `https://api.finvera.news`.

***

## Client-side: publishable keys + session tokens

Publishable keys are safe to ship in client bundles, but on their own they cannot call data endpoints. A client first exchanges its `pk_` for a **session token** at the Finvera edge, then uses that session token as a bearer credential on every subsequent call.

Each session is bound to:

* the **publishable key** it was minted for,
* the browser **Origin** that minted it (must match the key's allowlist), and
* the caller's **network prefix** (IPv4 /24 or IPv6 /48).

A leaked session token therefore cannot be replayed from a different site or a different network, and any session can be revoked instantly by revoking its parent `pk_`.

### Flow at a glance

```
Browser                               Finvera Edge
  │                                        │
  │  1. Solve Turnstile challenge          │
  │  ─────────────────────────────────────►│
  │                                        │
  │  2. POST /v1/session                   │
  │     x-api-key: pk_…                    │
  │     cf-turnstile-token: …              │
  │     Origin: https://yourapp.com        │
  │  ─────────────────────────────────────►│  verify Turnstile,
  │                                        │  check pk + origin,
  │                                        │  rate-limit,
  │  ◄─────────────────────────────────────│  mint session JWT
  │     { token, expires_in, … }           │
  │                                        │
  │  3. GET /kms/api/v1/…                  │
  │     Authorization: Bearer <token>      │
  │  ─────────────────────────────────────►│  verify session,
  │  ◄─────────────────────────────────────│  forward to origin
  │     200 OK + JSON                      │
```

### 1. Solve a Turnstile challenge

The mint endpoint is protected by [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/). Render the widget on the page that will mint the session and pass the resulting token along with the mint request.

```html theme={null}
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<div class="cf-turnstile"
     data-sitekey="YOUR_TURNSTILE_SITE_KEY"
     data-action="mint_session"
     data-cdata="SHA256_OF_YOUR_PK"
     data-callback="onTurnstileToken"></div>
```

Required widget configuration:

* `data-action` **must** equal `mint_session`. Tokens issued for any other action are rejected.
* `data-cdata` should be the lowercase hex SHA-256 of your publishable key. This binds the challenge to the specific `pk_` so a token harvested under one key on the same site cannot be reused with another. (Only enforced on interactive widgets; invisible widgets may omit it.)
* The widget's hostname must match the `Origin` of the mint request — Turnstile is bound to the page that served it.

The widget invokes your callback with a single-use token. Send that token to `/v1/session` immediately.

### 2. Mint a session token

```bash theme={null}
curl -X POST https://api.finvera.news/v1/session \
  -H "x-api-key: pk_live_xxxxxxxxxxxxxxxx" \
  -H "Origin: https://yourapp.com" \
  -H "cf-turnstile-token: <token-from-widget>"
```

**Response (200):**

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…",
  "token_type": "Bearer",
  "expires_in": 900,
  "expires_at": 1747058400,
  "refresh_window_seconds": 28800,
  "action": "mint_session"
}
```

* `token` — opaque JWT to send on subsequent requests.
* `expires_in` — seconds until this token expires (default 15 min).
* `refresh_window_seconds` — the maximum lifetime of a session chain. Once this much time has passed since the original mint, you must mint a fresh session from `pk_` again.

### 3. Call API endpoints with the session token

Every other Finvera endpoint accepts the session token as a standard bearer credential:

```bash theme={null}
curl https://api.finvera.news/kms/api/v1/press-releases \
  -H "Authorization: Bearer <session-token>"
```

The request must come from the same Origin and the same network prefix the session was minted on. Sending a `pk_` directly to a data endpoint (without first exchanging it) returns `401 session_required`.

### 4. Sliding refresh (zero-effort token rotation)

When a request is served past the halfway point of the session's TTL — and the session chain is still within `refresh_window_seconds` — the edge mints a fresh token and returns it on the response:

```
x-session-token: eyJhbGciOi…       ← replace your in-memory token with this
x-session-expires-at: 1747059300
```

These headers are exposed via CORS (`Access-Control-Expose-Headers`), so browsers can read them. Clients should:

1. Inspect every response for `x-session-token`.
2. If present, replace the in-memory token with it (and update `expires_at`).
3. When the refresh window is exhausted, restart from step 1 — solve Turnstile and call `/v1/session` again.

### Error responses

`/v1/session` returns JSON `{"error":"<reason>"}` with these status codes:

| Status | `error`                                                      | Meaning                                     |
| ------ | ------------------------------------------------------------ | ------------------------------------------- |
| 401    | `publishable_key_required`                                   | `x-api-key` is missing or not a `pk_`       |
| 401    | `unknown_key`                                                | `pk_` is not recognised                     |
| 401    | `key_revoked`                                                | `pk_` was revoked                           |
| 403    | `origin_required` / `origin_malformed`                       | `Origin` header missing or unparseable      |
| 403    | `origin_not_allowed`                                         | `Origin` is not in this key's allowlist     |
| 403    | `turnstile_token_missing`                                    | No `cf-turnstile-token` header              |
| 403    | `turnstile_verify_failed`                                    | Turnstile siteverify rejected the token     |
| 403    | `turnstile_hostname_mismatch`                                | Widget hostname doesn't match `Origin`      |
| 403    | `turnstile_action_mismatch`                                  | Widget action wasn't `mint_session`         |
| 403    | `turnstile_cdata_mismatch`                                   | Widget cdata didn't match `sha256(pk_)`     |
| 429    | `rate_limited_ip` / `rate_limited_pk` / `rate_limited_pk_ip` | Mint rate limit hit                         |
| 503    | `snapshot_unavailable`                                       | Edge couldn't load the key snapshot — retry |

Data-plane endpoints (with `Authorization: Bearer`) add:

| Status | `error`                                       | Meaning                                                        |
| ------ | --------------------------------------------- | -------------------------------------------------------------- |
| 401    | `session_expired`                             | Token TTL elapsed — refresh it                                 |
| 401    | `session_bad_signature` / `session_malformed` | Token was tampered with                                        |
| 401    | `session_mint_window_exceeded`                | Session chain hit `refresh_window_seconds`; re-mint from `pk_` |
| 401    | `session_revoked`                             | Underlying `pk_` was revoked                                   |
| 401    | `session_required`                            | You sent a `pk_` directly instead of a session token           |
| 403    | `session_origin_mismatch`                     | Request `Origin` differs from the one in the token             |
| 403    | `session_network_mismatch`                    | Caller's network prefix differs from the one in the token      |

***

## Browser implementation (JavaScript / TypeScript)

```javascript theme={null}
const API_BASE = "https://api.finvera.news";
const PUBLISHABLE_KEY = "pk_live_xxxxxxxxxxxxxxxx";

class FinveraClient {
  constructor() {
    this.token = null;
    this.expiresAt = 0;
  }

  // Called once after the Turnstile widget produces a token.
  async mintSession(turnstileToken) {
    const res = await fetch(`${API_BASE}/v1/session`, {
      method: "POST",
      headers: {
        "x-api-key": PUBLISHABLE_KEY,
        "cf-turnstile-token": turnstileToken,
      },
    });
    if (!res.ok) {
      const { error } = await res.json().catch(() => ({}));
      throw new Error(`session mint failed: ${error ?? res.status}`);
    }
    const body = await res.json();
    this.token = body.token;
    this.expiresAt = body.expires_at * 1000;
  }

  async request(path, init = {}) {
    if (!this.token || Date.now() >= this.expiresAt) {
      throw new Error("no valid session — re-solve Turnstile and call mintSession()");
    }
    const res = await fetch(`${API_BASE}${path}`, {
      ...init,
      headers: {
        ...(init.headers ?? {}),
        Authorization: `Bearer ${this.token}`,
      },
    });

    // Sliding refresh — pick up the rotated token if the edge issued one.
    const refreshed = res.headers.get("x-session-token");
    if (refreshed) {
      this.token = refreshed;
      this.expiresAt = Number(res.headers.get("x-session-expires-at")) * 1000;
    }
    return res;
  }
}
```

A few practical notes:

* Keep the session token in memory only. Never persist it to `localStorage` — its short TTL plus origin/network binding is the protection.
* The `Origin` header is set automatically by the browser; don't try to override it.
* If you get `session_network_mismatch` you've roamed networks (e.g. Wi-Fi → cellular). Mint a fresh session.

***

## Server-to-server: secret keys

For backend services, batch jobs, or any code running outside a browser, use a **secret key** (`sk_…` or `finvera_…`). Secret keys do **not** go through the session flow — they are sent directly on every request and validated at the origin.

```bash theme={null}
curl https://api.finvera.news/kms/api/v1/press-releases \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx"
```

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

API_BASE = "https://api.finvera.news"

class FinveraClient:
    def __init__(self, secret_key: str):
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {secret_key}"

    def get(self, path: str, **kwargs):
        return self.session.get(f"{API_BASE}{path}", **kwargs)


client = FinveraClient(os.environ["FINVERA_SECRET_KEY"])
resp = client.get("/kms/api/v1/press-releases")
resp.raise_for_status()
print(resp.json())
```

Treat secret keys like any other credential:

* Store them in your secret manager (Vault, AWS Secrets Manager, Doppler, …), never in source control.
* Rotate them from the dashboard if you suspect exposure — revocation propagates to the edge within \~70 seconds.
* Do not ship secret keys to browsers, mobile apps, or any user-controlled environment. Use a publishable key there instead.

***

## Choosing between `pk_` and `sk_`

| Question                                                                    | Use `pk_` + session | Use `sk_` |
| --------------------------------------------------------------------------- | ------------------- | --------- |
| Will the code run in a browser, mobile app, or other untrusted environment? | ✅                   | ❌         |
| Do you need per-Origin and per-network binding?                             | ✅                   | ❌         |
| Is the caller a server you control?                                         | ❌                   | ✅         |
| Do you need long-lived, non-interactive access (cron, ETL)?                 | ❌                   | ✅         |

If you need both — a public web app *and* a backend that calls Finvera — provision one of each from the dashboard.
