Reading the keys…
ChoonDevelopAPI keys
demoThis build is a demo. The catalog and detections are a fixed sample.Scopereading…Sound is used for one thing: a short tone when a long scan finishes while this tab is in the background. The A/B listening check plays audio either way, because there the sound is the evidence.Reading the keys…
Which keys exist for this account, which of them is nobody using, and what can a key actually do?
Keys authenticate requests to the Choon API. Anyone with a key can identify audio and read your detections.
Sandbox keys are minted with the chn_test_ prefix (monitor/store.py:735) — §13.8 writes it chn_sand_, and the code is what appears in your terminal.
No route verifies a key yet.
Keys are minted, hashed and listed, and nothing in service/api.py or either router checks one on the way in. So a key is a real record of an intent to authenticate and not yet a credential that gates anything — which is a different statement from “keys work” and from “there are no keys”.
Reading the keys…
Requests (7d) has no field to read.
§13.8 asks for a per-key request count with seven day-bars. The row the count would come from is monitor_request_log, and nothing in the service writes it — see the request log. Drawing seven bars off a table nothing writes would render an unused key and a busy one identically, in a column headed with a number.
The secret is returned once, in the response to the call that mints it, and is never recoverable — only a hash is stored. Have somewhere to put it before you run this.
curl -s -X POST "$CHOON/api/develop/keys" \
-H 'content-type: application/json' \
-d '{"label":"Ingest worker (staging)","environment":"sandbox"}'
# → {"key_id":"key_…","key_prefix":"chn_test_…","secret":"chn_test_…",
# "warning":"this secret is not recoverable; store it now"}secret appears in that response and nowhere else, ever. The server keeps a SHA-256 and a six-character prefix; there is no endpoint that can return the value again, and that is a property of the store rather than a policy of the UI.
This page cannot mint a key.
The reason is no longer transport. Until 2026-08-24 it was: minting is a POST and the proxies were GET-only. They are not — lib/backend-proxy.ts forwards POST and DELETE, so both POST /api/develop/keys and DELETE /api/develop/keys/:key_id are reachable from this browser. What is missing is this page: no control here issues either call. That gap is worth naming separately for revocation, which is a security control and was withheld by a rule aimed at something else entirely. The calls printed here are the real ones.
Revoking Ingest worker (staging) makes requests using that key fail immediately. It cannot be undone. There is deliberately no regenerate: it reads as a refresh and behaves as a revocation.
curl -s -X DELETE "$CHOON/api/develop/keys/key_9f4c11a70b2d6e88"
The row stays and revoked_at is set (monitor/store.py:752), so a revoked key remains visible in the table above rather than disappearing — a key that vanishes cannot be distinguished from one that never existed.
§13.8 specifies both. Neither is implemented: create_api_key takes a label and an environment, and no route reads a scope.
| Scope | What it would let someone do | State |
|---|---|---|
| identify | Submit audio and get matches | not implemented |
| watermark:embed | Embed payloads (metered) | not implemented |
| watermark:detect | Read payloads | not implemented |
| monitor:read | Read detections and evidence | not implemented |
| catalog:write | Add or remove references | not implemented |
A scope picker is not rendered, on purpose.
A checkbox list that silently grants everything is the most dangerous fiction available on this screen: it reads as a restriction and behaves as none, and the reader would only discover the difference by being breached. The expiry choices §13.8 names — 24 hours · 30 days · 90 days · Never — are missing for the same reason; when they land, choosing Never states in the disputed hue that such a key can only be revoked manually.