Every authenticated request carries your key in the X-API-Key header.
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.
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.
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 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.
curl -X POST https://intel-v1.exorde.io/v1/keys/trial \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'
import httpx
r = httpx.post(
"https://intel-v1.exorde.io/v1/keys/trial",
json={"email": "[email protected]"},
)
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):
{
"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.
curl https://intel-v1.exorde.io/v1/keys/current \
-H "X-API-Key: $EXORDE_API_KEY"
{
"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.
curl -X POST https://intel-v1.exorde.io/v1/keys/rotate \
-H "X-API-Key: $EXORDE_API_KEY"
{
"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"
}
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.
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.
curl -X DELETE https://intel-v1.exorde.io/v1/keys/current \
-H "X-API-Key: $EXORDE_API_KEY"
{
"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.
curl https://intel-v1.exorde.io/v1/me \
-H "X-API-Key: $EXORDE_API_KEY"
{
"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.
{
"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.
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.
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.
# 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
import httpx, time
BASE = "https://intel-v1.exorde.io"
EMAIL = "[email protected]"
# 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 for release-by-release detail.
Last reviewed: 2026-05-19. API version 1.2.8.