Programmatic access & API keys
This page covers how to drive CRTX from code today, and — importantly — a concrete design for the API-support features that make CRTX usable inside other products and your customers' environments: long-lived API keys, SDKs, webhooks, rate limiting, and an embeddable chat widget.
Status legend
- ✅ Available today — works against the current backend.
- 🧭 Proposed — a design/roadmap sketch with an implementation path. Not yet in the codebase; included so you can prioritize and build it.
#✅ What works today
The full REST + SSE API (reference) is callable from any language using a Supabase-issued JWT (authentication). You can already:
- Automate ingestion (upload files / submit URLs, poll jobs).
- Run streaming queries and consume SSE.
- Manage collections, sessions, shares, and read eval metrics.
- Generate a typed client from
Base URL/openapi.json.
A minimal automation loop:
import httpx, time
from supabase import create_client
sb = create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
token = sb.auth.sign_in_with_password({"email": E, "password": P}).session.access_token
api = httpx.Client(base_url=BASE_URL, headers={"Authorization": f"Bearer {token}"})
col = api.post("/collections/", json={"name": "Nightly Import"}).json()
job = api.post(f"/ingest/url", json={"url": SRC, "collection_id": col["id"]}).json()
while True: # poll until terminal
s = api.get(f"/ingest/jobs/{job['job_id']}").json()
if s["status"] in ("succeeded", "partial", "failed"):
break
time.sleep(2)
The gap: JWTs expire and represent a human. For headless integrations and customer-facing access you want API keys.
#🧭 API keys (the core of "API support")
Goal: issue long-lived, revocable, scoped keys that authenticate as a principal without a login flow — the standard way to let other systems and customers call your platform.
#Design
Key format. crtx_sk_<random> (secret, shown once) — plus a short public
prefix (crtx_sk_a1b2…) stored in cleartext for identification in dashboards.
Storage. A new api_keys table:
| column | purpose |
|---|---|
id | PK |
user_id | the owning principal (reuses existing ownership model) |
prefix | first 8 chars, for display/lookup |
key_hash | SHA-256 (or Argon2) of the full key — never store the raw key |
scopes | e.g. ["query", "ingest", "collections:read"] |
collection_id | optional: restrict the key to one collection |
expires_at, last_used_at, revoked_at | lifecycle |
Verification slots in beside the existing JWT dependency. The current
get_current_user returns a dict keyed by sub; a key-aware dependency returns
the same shape, so routers don't change:
# app/auth.py (proposed)
async def get_principal(request: Request,
creds: HTTPAuthorizationCredentials = Security(_bearer)) -> dict:
api_key = request.headers.get("X-API-Key")
if api_key:
row = lookup_api_key(sha256(api_key)) # constant-time compare
if not row or row["revoked_at"] or expired(row):
raise HTTPException(401, "Invalid API key")
touch_last_used(row["id"])
return {"sub": row["user_id"], "scopes": row["scopes"],
"collection_scope": row["collection_id"], "auth": "api_key"}
if creds: # fall back to JWT
return {**decode_token(creds.credentials), "scopes": ["*"], "auth": "jwt"}
raise HTTPException(401, "Not authenticated")
Swap Depends(get_current_user) → Depends(get_principal) in the routers, and add
a small require_scope("ingest") guard where mutations happen. Because access is
already enforced from user["sub"], a key inherits exactly its owner's collections
— optionally narrowed by collection_scope.
Management endpoints (JWT-authenticated, for the dashboard):
POST /keys/ { name, scopes, collection_id?, expires_at? } → { id, key } # key shown once
GET /keys/ → [{ id, name, prefix, scopes, last_used_at, ... }]
DELETE /keys/{id} # revoke
Usage (customer/integration side):
curl "$BASE_URL/query/" \
-H "X-API-Key: crtx_sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "question": "…", "collection_id": "…" }'
This unlocks: CI/cron ingestion, server-to-server queries from a customer's backend, and scoped read-only keys you can hand out safely.
#🧭 Rate limiting & usage metering
Once keys exist, meter them:
- Per-key rate limits via Redis token buckets (Redis is already a dependency).
A dependency increments
ratelimit:{key_id}:{window}and returns429withRetry-Afterwhen exceeded. - Usage records written per request (key id, endpoint, tokens, latency) — you
already log tokens per query, so extend
query_logs/ingest_logswith the key id. This gives you per-customer billing and quota dashboards. - Tiering: store limits on the key/plan and enforce in the same dependency.
#🧭 Webhooks (push, don't poll)
Ingestion is asynchronous, so today integrations poll /ingest/jobs/{id}.
Webhooks let CRTX call you instead — the natural fit for pipelines.
Design.
webhookstable:user_id,url,secret,events(ingest.succeeded,ingest.failed,ingest.partial, …),active.- The ingest worker already computes terminal status in
ingest_job.py— emit an event there. Sign the payload:X-CRTX-Signature: sha256=HMAC(secret, body). - Deliver via the Arq worker with retry/backoff (Tenacity is already used).
// POST to the customer's URL
{
"event": "ingest.succeeded",
"job_id": "…", "collection_id": "…", "source": "report.pdf",
"chunks_total": 512, "occurred_at": "2026-07-04T12:00:00Z"
}
Consumers verify the HMAC, then react (mark a document ready, trigger a re-index, notify a user). This is the listed roadmap item "webhook notifications on ingestion completion."
#🧭 Official SDKs
The API is small and regular — a thin SDK dramatically improves adoption. Two paths:
- Generate from the OpenAPI spec (
Base URL/openapi.json) withopenapi-typescript/openapi-python-clientfor the REST surface. - Hand-write the streaming helper — the SSE parsing for
/query/is the one part worth wrapping ergonomically:
// @crtx/sdk (proposed) — ergonomic streaming
const crtx = new CRTX({ apiKey: "crtx_sk_…", baseUrl });
for await (const evt of crtx.query({ question, collectionId })) {
if (evt.type === "metadata") renderSources(evt.sources);
if (evt.type === "token") append(evt.token);
}
# crtx-python (proposed)
crtx = CRTX(api_key="crtx_sk_…", base_url=BASE_URL)
for evt in crtx.query(question="…", collection_id="…"):
...
Ship a Python SDK (for data/ingestion pipelines) and a TS/JS SDK (for app integration and the widget below).
#🧭 Embeddable chat widget
To let customers drop CRTX into their website/app without building UI:
- Ship a small script that mounts an iframe/web-component chat bound to one collection.
- Authenticate it with a scoped, query-only, collection-restricted API key
(never a JWT, never an
ingestkey). - For untrusted browsers, mint short-lived widget tokens from the customer's backend (their server exchanges its secret key for a 15-minute widget token), so the long-lived key never ships to the client.
<script src="https://cdn.your-app.com/crtx-widget.js"
data-collection="COLLECTION_ID"
data-token-endpoint="https://customer.com/api/crtx-widget-token"></script>
This turns CRTX into an answer engine embeddable in any support page, docs site, or internal tool.
#Priority order
If you build the above, this sequence delivers value fastest:
- API keys — unblocks every other integration and all headless use.
- Rate limiting + usage metering — required before exposing keys to customers.
- Webhooks — removes polling; makes ingestion pipeline-friendly.
- SDKs — lower the integration bar (start with the SSE streaming helper).
- Embeddable widget — the fastest way for customers to adopt CRTX with no code.
For running CRTX itself inside a customer's infrastructure (single-tenant, VPC, on-prem, bring-your-own-keys, data residency), see Self-hosting & deploying in your environment.