api keys LIVE
Authentication for the shinnslist api: get a key, send it on every call, and read exactly what the server returns when something goes wrong. Everything below describes code that is actually deployed — anything still planned is marked DRAFT.
functions/api/keys.js, functions/api/keys/usage.js, functions/lib/api-keys.js). Where the code has a seam for a future feature but no endpoint yet (rotation, revocation, pro issuance), the section says so — we don't document endpoints that don't exist.getting a key — POST /api/keys
Keys are free and self-service. One request, no signup, no card:
curl -s -X POST "https://shinnslist.com/api/keys" | jq '{ok, tier, limits, note}'
A successful response looks like this (the raw key is shown exactly once — it is never stored in readable form, so save it now):
{
"ok": true,
"key": "sk_live_<43-char-secret>",
"tier": "free",
"limits": {
"intel_per_day": 25,
"requests_per_minute": 10,
"v1_calls_per_month": 1000,
"v1_price_per_call_usd": 0.003
},
"note": "This key is shown only once and is never stored in readable form. Send it as the x-api-key header, ?key=, or Authorization: Bearer. v1 signal APIs: POST /v1/signals/{signal}/evaluate."
}
Details, all verified in code:
- raw key:
sk_live_+ 32 random bytes (base64url, 256 bits of entropy). The server keeps only the SHA-256 hash of the key. - every key issues at the
freetier today.proexists in the config but issuance is reserved — see tiers below. - issuance is rate-limited: 3 keys per IP per hour. Over the line you get
429witherror: "issuance_rate_limit_exceeded"and aRetry-Afterheader. - the server can also fail closed with
503 {"ok": false, "error": "key store unavailable, try again"}if the key store is down.
sending the key
On /api/intel and the rest of the REST surface, send the key as the x-api-key header (preferred) or as the ?key= query parameter. The v1 signal APIs (POST /v1/signals/{signal}/evaluate) accept the same two plus Authorization: Bearer.
curl -s "https://shinnslist.com/api/intel?q=Richmond,VA" \ -H "x-api-key: sk_live_YOUR_KEY_HERE" | jq '.signals | keys'
No key still works — you fall back to the anonymous tier (5 parcel queries/day per IP). A wrong key is rejected, not demoted: 401, see error responses.
tiers & quotas
| tier | signals | intel / day | req / min | how issued |
|---|---|---|---|---|
anon (no key) | all | 5 per IP | 2 per IP | automatic |
free | all | 25 | 10 | POST /api/keys |
pro | all | 1000 | 60 | reserved — payments track only |
Quota is the gate for free today: every key carries entitlements: ["*"], so any signal works up to the quota. v1 signal APIs have their own monthly seam: 1,000 calls/month free, then $0.003 per call with a hard cap — no surprise charges.
known limitation: the daily quota counter is not atomic (Cloudflare KV has no transactions), so bursts can overshoot the cap slightly. Acceptable for freemium gating; hard metering moves to a transactional store later.
checking usage — GET /api/keys/usage
Requires a valid key (x-api-key header or ?key=). Anonymous callers get 401 {"ok": false, "error": "api key required for /api/keys/usage"}. The response never reveals the key hash:
{
"ok": true,
"tier": "free",
"entitlements": ["*"],
"created": "2026-09-28T14:02:11.000Z",
"quota": { "used": 3, "limit": 25, "remaining": 22, "resets_at": "2026-09-29T00:00:00.000Z" },
"rate": { "per_minute": 10 },
"v1_monthly": { "limit": 1000, "used": 41, "remaining": 959, "price_per_call_usd": 0.003 },
"note": "metering seam: counters feed future metered billing; no charges today"
}
error responses
Every error is JSON with an ok: false envelope. These are the exact shapes the code returns — copied, not paraphrased.
- 401 — invalid key
{"ok": false, "error": "invalid api key", "hint": "get a free key: POST /api/keys"} - 429 — rate limit (carries a
Retry-Afterheader){"ok": false, "error": "rate_limit_exceeded", "retry_after_seconds": 34} - 429 — daily quota exceeded (
Retry-Afteruntil midnight UTC){"ok": false, "error": "daily_quota_exceeded", "reason": "daily_quota_exceeded", "quota": {"limit": 25, "resets_at": "2026-09-29T00:00:00.000Z"}, "hint": "quota resets at 00:00 UTC; paid tiers raise the cap"} - 403 — signal not entitled (paid-tier hook; never fires today — every tier's entitlements are
["*"]){"ok": false, "error": "signal_not_entitled", "reason": "signal_not_entitled", "denied": [{"signal_id": "openfema_flood_buyout", "reason": "requires pro tier"}], "hint": "this signal requires a higher tier"} - 503 — key store unavailable
{"ok": false, "error": "key store unavailable, try again"}
key security
- the raw key is returned once and never stored, logged, or retrievable. The server stores only the SHA-256 hash (
key:<hash>). - search logs carry only the 6-character key prefix (
sk_live) — enough to identify a key in support, not enough to use it. - key records live in KV with a 1-year TTL; when the D1 binding is present, D1 is the authoritative store and lookups reject revoked keys.
planned: key rotation DRAFT
There is no rotation endpoint today — no POST /api/keys/rotate, no DELETE /api/keys, no self-service revocation. If a key is compromised, the only honest move is to mint a new key via POST /api/keys and stop using the old one. Endpoint names and flows here are not final; the real rotation docs land on this page the day the endpoints ship.
planned: revocation DRAFT
The D1 key store already has a revoked_at column and lookups reject revoked keys — a revoked key behaves exactly like an invalid key (401). But there is no endpoint that sets it: revocation is currently an operator-side action, not self-service. Details and the real endpoint ship here when they exist.
planned: usage dashboard DRAFT
Until a dashboard ships, GET /api/keys/usage is the usage interface: poll it for quota.used / quota.remaining / v1_monthly. No dashboard page, no emailed quota alerts, and no usage history endpoint exist yet — all not final.
planned: pro tier issuance DRAFT
pro (1000 intel/day, 60 req/min) exists in the tier config but is marked reserved: true: POST /api/keys never issues it, and there is no other issuance path. Pro issuance ships with the payments track. The price-per-call seam ($0.003/call on v1) is the only paid-shaped number live today — and it's modeled as hard-capped free, not a charge.
next: getting started · api reference · docs hub