Authentication
API keys, live versus test, and how to rotate safely.
Every request needs a key in the Authorization header.
curl https://api.cruxal.in/v1/filings \
-H "Authorization: Bearer ck_live_..."
Keys are server-side credentials
There is no CORS policy that allows browser calls, and that is deliberate. A key embedded in front-end JavaScript or in a mobile app binary is a published key — anyone who opens developer tools has it. Call this API from your own backend and proxy what your users need.
For the same reason, the key is never accepted in a query string. Query strings are written to server access logs, load-balancer logs and browser history. Header only.
Live and test keys
A test key is free, needs no plan, and you can create one the moment you sign up.
ck_live_ | ck_test_ | |
|---|---|---|
| Data | Real, current | Real, delayed 60 minutes |
| History | Full | Last 7 days |
| Quota | Your plan's | 1,000 requests/month |
| Rate limit | Your plan's | 10 requests/minute |
| Expires | Never — revoke when done | 30 days after creation |
Test keys are not a sandbox with fake data. They return the same shapes from the same tables, so anything you build against one runs unchanged against a live key. What differs is how much of each record you get:
| Field | ck_test_ returns |
|---|---|
headline, ticker, isin, company_name, filed_at, category | In full |
url | In full |
summary | First ~120 characters |
sentiment | In full |
impact_band | In full — low / medium / high |
impact_score | null (the 0-10 score is withheld) |
tags | null |
price_context | null |
logo_url | null |
expected_direction, conviction on /v1/events | null |
Note that no field changes type. Withholding nulls a value; it never swaps a number for a
string, because that is precisely what would break your client on upgrade. impact_band is
present on every plan, so impact filtering works the same everywhere — what a paid plan adds
is the score to rank on.
That is enough to write and test a real integration — every field a client parses is present, so nothing about the shape changes when you upgrade. It is not enough to run a product on, and it is not meant to be.
The environment is a property of the key recorded when it is created. It is not inferred from the prefix you send.
If a paid plan lapses, its live key is not disabled — it drops to these evaluation limits
and this payload, and keeps working. Your integration degrades rather than breaking, and
/v1/usage reports entitled: false so you can see why.
Storage and rotation
We store a SHA-256 hash of your key, never the key itself. The plaintext is shown exactly once, on the screen where you create it. It cannot be recovered — if you lose it, revoke it and create a new one.
To rotate without downtime:
- Create a second key.
- Deploy it to your service.
- Confirm traffic has moved (the Last used column on API Keys updates within a minute).
- Revoke the old key.
Revocation takes effect immediately: the very next request with that key gets a 401.
What a failure looks like
Missing, malformed, unknown, revoked and expired keys all return the same response:
{
"type": "https://www.cruxal.in/docs/errors#unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid or expired API key",
"instance": "/v1/filings"
}
They are deliberately indistinguishable. Telling you that a key exists but is revoked would also tell an attacker that their guess named a real key.