Keys

POST /1/keys, GET /1/keys, DELETE /1/keys/:id — create, list and revoke API keys.

Text
POST /1/keys
GET /1/keys
DELETE /1/keys/:id

Every route here requires an admin key. See API keys for what each key type can do, and Scoped tokens for the signing secret a search key returns.

Create a key

Text
POST /1/keys
TypeScript
type CreateKeyRequest = {
	name: string;
	type: 'admin' | 'write' | 'search';
	restrictions?: {
		indexes?: string[] | null;
		referrers?: string[] | null;
		ratePerMinute?: number | null;
	};
};

type CreateKeyResponse = {
	key: KeySummary;
	secret: string;
	signingSecret: string | null;
};

secret and signingSecret are both returned exactly once, at creation — there is no route that reveals either again. signingSecret is null for admin and write keys; only a search key gets one, since it is the key a signed scoped token narrows.

Referrer restrictions

referrers limits where a search key works. A search or click from a browser is allowed only when the hostname of its Origin matches one of the entries; leave it out, or send null, to allow any site.

You sendStored asMatches
docs.toread.appdocs.toread.apphttps://docs.toread.app, http://docs.toread.app:8080
https://docs.toread.app/guidedocs.toread.appthe same hostname, on any scheme or port
http://localhost:5173localhostlocal development on any port
*.toread.app*.toread.appdocs.toread.app, a.b.toread.app — not toread.app itself

Each entry is reduced to a lowercase hostname or a *. wildcard: the scheme, port, path and query are dropped, and duplicates are removed. An entry that isn't a hostname — a space, a * anywhere but the first label, a wildcard over a single label like *.com, a scheme other than http or https — fails the whole request with 400 bad_request and names the entry. To allow both a domain and its subdomains, list both: toread.app, *.toread.app.

Terminal
curl -X POST https://api.usesuo.com/1/keys \
  -H "x-suo-key: suo_admin_YOUR_KEY" \
  -H "content-type: application/json" \
  -d '{ "name": "docs widget", "type": "search", "restrictions": { "indexes": ["docs"] } }'
JSON
{
	"key": {
		"id": "suokey_8f2c1a94",
		"name": "docs widget",
		"type": "search",
		"display": "suo_search_…7f3a",
		"restrictions": { "indexes": ["docs"], "referrers": null, "ratePerMinute": null },
		"createdAt": 1732550401123,
		"lastUsedAt": null
	},
	"secret": "suo_search_YOUR_KEY",
	"signingSecret": "3q0F...base64url"
}

List keys

Text
GET /1/keys
TypeScript
type ListKeysResponse = {
	keys: KeySummary[];
};

type KeySummary = {
	id: string;
	name: string;
	type: 'admin' | 'write' | 'search';
	display: string;
	restrictions: { indexes: string[] | null; referrers: string[] | null; ratePerMinute: number | null };
	createdAt: number;
	lastUsedAt: number | null;
};

display is the prefix plus the last four characters (suo_search_…7f3a) — enough to identify the key, never enough to reconstruct it. Neither secret nor signingSecret is returned here.

Revoke a key

Text
DELETE /1/keys/:id

Returns 204 No Content on success. Revoking a key keeps its row for audit but evicts it from the lookup cache; the revocation can take up to 60 seconds to reach every edge location — see API keys.

Terminal
curl -X DELETE https://api.usesuo.com/1/keys/suokey_8f2c1a94 \
  -H "x-suo-key: suo_admin_YOUR_KEY"

From the CLI

Text
suo keys create --name "docs widget" --type search --index docs --admin-key suo_admin_YOUR_KEY --host https://api.usesuo.com
suo keys list --admin-key suo_admin_YOUR_KEY --host https://api.usesuo.com
suo keys revoke suokey_8f2c1a94 --admin-key suo_admin_YOUR_KEY --host https://api.usesuo.com

suo keys create prints secret with a "shown once" warning, but does not yet print signingSecret — call the route directly (as above) until the CLI is updated to surface it too.