Skip to main content
Two kinds of key, and they are not interchangeable.

Secret keys

uk_sk_…, your server’s credential. Created in the panel, shown once, stored as a SHA-256 hash.
201
Requires api_keys:write.
key is the only time the value is readable. What is stored is its hash, so a database dump hands out no working credentials — and a lost key is replaced, never recovered.
A key is born in an environment and never leaves it. The environment is chosen here, minted into the prefix, and resolved from the stored row on every request. Nothing the caller sends later can change it.

Listing

Requires api_keys:read. Only the display prefix comes back — enough to recognize a key in a list, never enough to use one. last_used_at is stamped at minute granularity, best-effort: writing on every request would turn a read path into a write on a hot row.

Revoking

204. The row survives with revoked_at stamped, so the audit trail of which key existed does not disappear with the key. Revoked keys stop authenticating immediately.

Rotating

There is no rotate endpoint, because rotation is two calls and a deploy:
1

Create the replacement

Same environment, a name that says why it exists.
2

Deploy it

Both keys work at once — nothing is exclusive.
3

Revoke the old one

Check last_used_at first: if it is still moving, something is still holding it.

Publishable keys

uk_pk_…, one per environment, seeded when the organization is created. You never create or delete them; you configure them.
Returned in plaintext, unlike every secret: the value is public by definition — it sits in your page’s HTML — and the panel is where a developer copies it from. A publishable key identifies and authorizes almost nothing. It opens /v1/boot and the /v1/contact-auth/* flows, and nothing else.

The allowed-origins list

This list is the actual security boundary of a key that is public by design.
Requires api_keys:write. Entries are scheme, host and optionally port. No path, no query, no fragment — an Origin header never carries one, and an entry that did would never match anything. Malformed entries answer 400. Matching is exact on the normalized origin. No wildcards, no prefixes: the list is short, and the ambiguity of anything cleverer is where bypasses live.
An empty list allows any origin. That is the getting-started state so nobody’s first ten minutes are spent on a CORS screen. Fill it in before you go live.
With a non-empty list, a request with no Origin header is refused as well. Browsers always send one cross-origin, so “no Origin” means “not a browser” — exactly the caller the list exists to stop. A rejected origin answers:
403

The other guard

Boot is throttled per publishable key — 240 calls a minute — on top of the per-IP limit. Bots and landing-page traffic mint visitor rows forever otherwise, and the key is the tenant boundary a flood arrives through.

Which key goes where

A uk_sk_ key in a browser bundle is a uk_sk_ key in every visitor’s hands. Ship publishable keys to clients; keep secret keys on servers.

If a key leaks

1

Revoke it

DELETE /v1/organization/api-keys/{id}. It stops working on the next request.
2

Create a replacement and deploy

Same environment.
3

If it was federated, rotate the identity secret too

A leaked uk_is_… is worse than a leaked API key: it mints verified sessions for any user id. See Federated identity.
Every credential carries a recognizable prefix precisely so a repository scan or a log grep can find it before somebody else does.