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

# Authentication

> X-API-Key header, trial minting, rotation, revocation, and the typed error envelope. Everything you need to manage credentials in production.

## The header

Every authenticated request carries your key in the `X-API-Key` header.

```bash theme={null}
curl https://intel-v1.exorde.io/v1/topics/global/trending \
  -H "X-API-Key: exd_trial_QLocUNNcjQ7TXxTgJZ2DWww4QxjlLBgc"
```

No cookies, no OAuth, no signed requests. Keys are secrets — treat them like passwords.

<Warning>
  **Never commit a key to git, embed it in a public SPA bundle, or email it in plaintext.** If a key is exposed, rotate it immediately (see below) — rotation is atomic and the old key dies the same instant the new one is born.
</Warning>

## Key tiers and prefixes

Each key carries a prefix that hints at its tier. The prefix is **cosmetic** — the real tier is stored server-side and returned by `GET /v1/keys/current`.

| Prefix       | Tier  | Typical source                                                   |
| ------------ | ----- | ---------------------------------------------------------------- |
| `exd_trial_` | Watch | `POST /v1/keys/trial` (free, 7 days)                             |
| `exd_watch_` | Watch | Paid Watch subscription                                          |
| `exd_see_`   | See   | Paid See subscription                                            |
| `exd_know_`  | Know  | Paid Know subscription                                           |
| `exd_test_`  | Test  | Internal QA, integrator plumbing tests (not issued to customers) |

Test-mode keys (`exd_test_*`) bypass the database and serve **fixture data** for deterministic plumbing tests. Production data requires a real key. See [Test mode](#test-mode) below.

## Minting a trial key

Public endpoint, IP-rate-limited, **idempotent per email** within the key's active window. Call it twice with the same email and you get the same key back.

```bash curl theme={null}
curl -X POST https://intel-v1.exorde.io/v1/keys/trial \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com"}'
```

```python Python theme={null}
import httpx

r = httpx.post(
    "https://intel-v1.exorde.io/v1/keys/trial",
    json={"email": "you@example.com"},
)
r.raise_for_status()
data = r.json()
print(data["api_key"], "reused:", data["reused"])
```

A successful response (HTTP 201 first time, HTTP 200 if replayed):

```json theme={null}
{
  "api_key": "exd_trial_QLocUNNcjQ7TXxTgJZ2DWww4QxjlLBgc",
  "client_id": "trial_3c91be7f",
  "tier": "watch",
  "topics": ["global"],
  "webhook_limit": 0,
  "rate_limit_rpm": 30,
  "monthly_call_quota": 5000,
  "active": true,
  "created_at": "2026-05-19T13:00:00Z",
  "expires_at": "2026-05-26T13:00:00Z",
  "reused": false
}
```

If you call this endpoint again with the same email **while a valid key still exists**, you get the same key back with `reused: true` and HTTP 200. No duplicate keys, no silent reissuance.

If the previous key for that email has expired or been revoked, a fresh key is minted (HTTP 201, `reused: false`).

## Inspecting the current key

Use this at runtime to discover what your key can do — tier, topics, expiry, rate limit, webhook quota.

```bash theme={null}
curl https://intel-v1.exorde.io/v1/keys/current \
  -H "X-API-Key: $EXORDE_API_KEY"
```

```json theme={null}
{
  "api_key": "exd_trial_QLocUNNcjQ7TXxTgJZ2DWww4QxjlLBgc",
  "client_id": "trial_3c91be7f",
  "tier": "watch",
  "topics": ["global"],
  "webhook_limit": 0,
  "rate_limit_rpm": 30,
  "monthly_call_quota": 5000,
  "active": true,
  "expires_at": "2026-05-26T13:00:00Z"
}
```

Same shape as the trial response, without `reused`. Build tier-aware UIs from this — read once at app start, refresh on 401/403.

## Rotating a key

Rotation issues a new key with the **same tier, topics, limits, and expiry**. The old key is deactivated atomically — switch your clients to the new key immediately.

```bash theme={null}
curl -X POST https://intel-v1.exorde.io/v1/keys/rotate \
  -H "X-API-Key: $EXORDE_API_KEY"
```

```json theme={null}
{
  "old_api_key": "exd_trial_QLocUNNcjQ7TXxTgJZ2DWww4QxjlLBgc",
  "new_api_key": "exd_trial_r3qiDtHjRNXp8wEYCPRH8L9Wv2nCNkqA",
  "client_id": "trial_3c91be7f",
  "tier": "watch",
  "topics": ["global"],
  "webhook_limit": 0,
  "rate_limit_rpm": 30,
  "expires_at": "2026-05-26T13:00:00Z"
}
```

<Tip>
  **Expiry is not extended** by rotation. Rotation is for credential hygiene, not lifetime extension. Replay protection: every subsequent call with the **old** key returns `401 invalid_api_key`.
</Tip>

## Revoking a key

Permanent. Idempotent. Once revoked, the key cannot be reactivated — a future POST to `/v1/keys/trial` with the same email will mint a brand-new key.

```bash theme={null}
curl -X DELETE https://intel-v1.exorde.io/v1/keys/current \
  -H "X-API-Key: $EXORDE_API_KEY"
```

```json theme={null}
{
  "api_key": "exd_trial_r3qiDtHjRNXp8wEYCPRH8L9Wv2nCNkqA",
  "revoked": true,
  "already_inactive": false,
  "revoked_at": "2026-05-19T13:42:11Z"
}
```

A second call with the same key returns `already_inactive: true` and HTTP 200 (idempotent, not an error).

## Who am I

`GET /v1/me` returns the caller's identity and **full entitlements** — tier, topics, rate limit, webhook + watchlist quotas, monthly usage. Ideal for building a tier-aware dashboard header or settings page.

```bash theme={null}
curl https://intel-v1.exorde.io/v1/me \
  -H "X-API-Key: $EXORDE_API_KEY"
```

```json theme={null}
{
  "client_id": "trial_3c91be7f",
  "tier": "watch",
  "topics": ["global"],
  "limits": {
    "rate_limit_rpm": 30,
    "monthly_call_quota": 5000,
    "webhook_limit": 0,
    "watchlist_limit": 0,
    "watchlist_term_limit": 0
  },
  "usage": {
    "api_calls_this_month": 127,
    "period": "2026-05",
    "webhooks_active": 0,
    "watchlists_active": 0
  },
  "expires_at": "2026-05-26T13:00:00Z",
  "trace_id": "8b3a47ce91d04f17"
}
```

## The typed error envelope

Every non-2xx response follows the same shape. Match on the `error` enum, show `message` to users, log `trace_id` for support.

```json theme={null}
{
  "error": "upgrade_required",
  "message": "This endpoint requires the 'see' tier",
  "feature": "clusters",
  "current_tier": "watch",
  "required_tier": "see",
  "upgrade": true,
  "trace_id": "8b3a47ce91d04f17"
}
```

The four authentication-specific errors:

| Status | `error`           | When                                            | Action                      |
| ------ | ----------------- | ----------------------------------------------- | --------------------------- |
| 401    | `missing_api_key` | Header not sent                                 | Add `X-API-Key`             |
| 401    | `invalid_api_key` | Unknown / rotated / revoked key                 | Mint or rotate              |
| 403    | `key_expired`     | Past `expires_at`                               | Upgrade or mint a new trial |
| 403    | `topic_denied`    | Key valid but not scoped to the requested topic | Upgrade or change topic     |

Full list across all routers: [Errors](/errors).

<Note>
  **Trace IDs are 16-hex strings** present on every response (success or error) in the `X-Exorde-Trace-Id` header and inside the JSON envelope on errors. Quote the trace\_id when you email support — we resolve in one round trip.
</Note>

## Test mode

Keys with the `exd_test_` prefix bypass the database, return a synthetic `know`-tier shape, and serve **fixture data** on every analytics endpoint. Use them to wire integrations (HTTP layer, JSON parsing, error handling) without burning real quota or polluting analytics.

```bash theme={null}
# Always returns the same fixture alert envelope, with detected_at rotated to "now"
curl "https://intel-v1.exorde.io/v1/topics/cyber/alerts?hours=24" \
  -H "X-API-Key: exd_test_smoke"
```

How to tell apart a fixture alert from a live alert:

| Field                    | Test fixture                                         | Live data                        |
| ------------------------ | ---------------------------------------------------- | -------------------------------- |
| `alert_id`               | starts with `sig_test_`                              | random UUID, no `_test_` infix   |
| `detected_at`            | rotated to "now-ish"                                 | actual detection time            |
| `keyword`                | always `iran` for cyber, `bitcoin` for finance, etc. | whatever is actually spiking     |
| `sample_posts[].preview` | templated string                                     | first \~160 chars of a real post |

Test keys are not issued to customers. They exist for our internal QA suite and for integrator partners who request a deterministic fixture path during onboarding.

## Operational guidance

* **Store keys server-side only.** Never in a public SPA bundle, git history, or a client-side env var shipped to users.
* **Rotate on suspicion of leak.** Rotation is free, atomic, and preserves all tier/topic/limit settings.
* **Use `/v1/me` at startup** to detect tier changes (upgrades, downgrades, expiries) without polling the billing system.
* **Handle 401 as "get a new key"** and **403 as "ask the user to upgrade or change topic"** — the codes are distinct for a reason.
* **Always log `trace_id`** alongside any user-facing error message. It is the single most useful piece of evidence in a support ticket.
* **Set a watchdog on `expires_at`.** Trial keys expire silently after 7 days; surface a "renew/upgrade" prompt when within 24h of expiry.

## Lifecycle example: full mint → rotate → revoke

```python theme={null}
import httpx, time

BASE = "https://intel-v1.exorde.io"
EMAIL = "ops@example.com"

# 1. Mint
r = httpx.post(f"{BASE}/v1/keys/trial", json={"email": EMAIL}).raise_for_status()
key = r.json()["api_key"]
print("minted:", key)

# 2. Use it
me = httpx.get(f"{BASE}/v1/me", headers={"X-API-Key": key}).json()
print("tier:", me["tier"], "topics:", me["topics"])

# 3. Rotate (suspected leak)
r = httpx.post(f"{BASE}/v1/keys/rotate", headers={"X-API-Key": key}).json()
new_key = r["new_api_key"]
print("rotated:", key, "->", new_key)

# Old key now dead
assert httpx.get(f"{BASE}/v1/me", headers={"X-API-Key": key}).status_code == 401

# 4. Revoke (when done)
r = httpx.delete(f"{BASE}/v1/keys/current", headers={"X-API-Key": new_key}).json()
assert r["revoked"] is True
print("revoked:", new_key)
```

Every step in this lifecycle is exercised by our QA suite (205 scenarios, currently 100% PASS). See [Changelog](/changelog) for release-by-release detail.

<Note>Last reviewed: 2026-05-19. API version 1.2.8.</Note>
