Manage API keys for programmatic access. See the API keys guide for usage details.
const TOKEN = "hv_..."; // your API keyconst headers = { Authorization: `Bearer ${TOKEN}` };const TOKEN = "hv_..."; // your API keyconst headers = { Authorization: `Bearer ${TOKEN}` };let token = "hv_..."; // your API keyconst token = "hv_..." // your API key# All examples assume $TOKEN is an API key (hv_...)AUTH="Authorization: Bearer $TOKEN"Create an API key
Section titled “Create an API key”POST /admin/api-keysRequires api-keys:create permission.
interface ApiKey { id: string; name: string; key: string; key_prefix: string; permissions: string[];}
const response = await fetch("http://127.0.0.1:3000/admin/api-keys", { method: "POST", headers: { ...headers, "Content-Type": "application/json", }, body: JSON.stringify({ name: "CI Deploy", permissions: ["lexicons:read", "lexicons:create", "backfill:create"], }),});const data: ApiKey = await response.json();const response = await fetch("http://127.0.0.1:3000/admin/api-keys", { method: "POST", headers: { ...headers, "Content-Type": "application/json", }, body: JSON.stringify({ name: "CI Deploy", permissions: ["lexicons:read", "lexicons:create", "backfill:create"], }),});const data = await response.json();let client = reqwest::Client::new();let response = client .post("http://127.0.0.1:3000/admin/api-keys") .bearer_auth(token) .json(&serde_json::json!({ "name": "CI Deploy", "permissions": ["lexicons:read", "lexicons:create", "backfill:create"] })) .send() .await?;let data: serde_json::Value = response.json().await?;body := bytes.NewBufferString(`{ "name": "CI Deploy", "permissions": ["lexicons:read", "lexicons:create", "backfill:create"]}`)req, _ := http.NewRequest("POST", "http://127.0.0.1:3000/admin/api-keys", body)req.Header.Set("Authorization", "Bearer "+token)req.Header.Set("Content-Type", "application/json")resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl -X POST http://127.0.0.1:3000/admin/api-keys \ -H "$AUTH" \ -H "Content-Type: application/json" \ -d '{ "name": "CI Deploy", "permissions": ["lexicons:read", "lexicons:create", "backfill:create"] }'| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | A label to identify this key’s usage |
permissions | string[] | yes | Permissions to grant the key. A permission you don’t hold yourself returns 403 Forbidden. A string that names no permission is stored but grants nothing |
Response: 201 Created
{ "id": "550e8400-e29b-41d4-a716-446655440000", "name": "CI Deploy", "key": "hv_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "key_prefix": "hv_a1b2c3d4", "permissions": ["lexicons:read", "lexicons:create", "backfill:create"]}The key field contains the full API key. It is only returned in this response, so store it securely. The key’s effective permissions are the intersection of the permissions specified here and the creating operator’s permissions at the time of each request. A key never carries the superadmin privilege, even when its operator holds it, so a superadmin-only endpoint such as POST /admin/users/transfer-super refuses every API key.
List API keys
Section titled “List API keys”GET /admin/api-keysRequires api-keys:read permission.
interface ApiKeySummary { id: string; name: string; key_prefix: string; permissions: string[]; created_at: string; last_used_at: string | null; revoked_at: string | null;}
const response = await fetch("http://127.0.0.1:3000/admin/api-keys", { headers,});const data: ApiKeySummary[] = await response.json();const response = await fetch("http://127.0.0.1:3000/admin/api-keys", { headers,});const data = await response.json();let client = reqwest::Client::new();let response = client .get("http://127.0.0.1:3000/admin/api-keys") .bearer_auth(token) .send() .await?;let data: serde_json::Value = response.json().await?;req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/api-keys", nil)req.Header.Set("Authorization", "Bearer "+token)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl http://127.0.0.1:3000/admin/api-keys -H "$AUTH"Response: 200 OK
[ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "CI Deploy", "key_prefix": "hv_a1b2c3d4", "permissions": ["lexicons:read", "lexicons:create", "backfill:create"], "created_at": "2026-03-01T00:00:00Z", "last_used_at": "2026-03-06T12:00:00Z", "revoked_at": null }]Only returns keys belonging to the caller. The full key is never included, only the prefix.
Revoke an API key
Section titled “Revoke an API key”DELETE /admin/api-keys/{id}Requires api-keys:delete permission.
const response = await fetch( "http://127.0.0.1:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000", { method: "DELETE", headers },);const response = await fetch( "http://127.0.0.1:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000", { method: "DELETE", headers },);let client = reqwest::Client::new();client .delete("http://127.0.0.1:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000") .bearer_auth(token) .send() .await?;req, _ := http.NewRequest("DELETE", "http://127.0.0.1:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000", nil)req.Header.Set("Authorization", "Bearer "+token)resp, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer resp.Body.Close()curl -X DELETE http://127.0.0.1:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000 \ -H "$AUTH"Sets revoked_at on the key. The key remains in the database for audit purposes but can no longer authenticate.
Response: 204 No Content. Returns 404 Not Found if the caller has no unrevoked key with that id.
© HappyProto

