API Keys
Endpoints under the API Keys tag manage long-lived bearer tokens for automation (scoped service accounts). The active org is implied by the calling principal’s session or API key — there is no separate X-Org selector on these routes. Shapes follow ApiKeyListItem, CreateApiKeyRequest, CreateApiKeyResponse, and UpdateApiKeyRequest in the API reference.
For how credentials are sent on the wire, see Authentication. POST /api-keys returns CreateApiKeyResponse including the full secret token once — store it immediately; later GET /api-keys only returns token_suffix and metadata.
Operations vs wire routes
Section titled “Operations vs wire routes”| Concern | HTTP | operationId | Notes |
|---|---|---|---|
| List | GET /api-keys | list_api_keys | Array of ApiKeyListItem. |
| Mint | POST /api-keys | create_api_key | Body CreateApiKeyRequest: required name, optional expires_at (RFC3339) or expires_in (duration string), scoped_roles, scoped_entity_ids. 201 + CreateApiKeyResponse. |
| Rename / expiry | PATCH /api-keys/{id} | update_api_key | Body UpdateApiKeyRequest. 204 empty body on success. |
| Revoke | DELETE /api-keys/{id} | revoke_api_key | Soft revoke — 204. |
| Restore | POST /api-keys/{id}/restore | restore_api_key | 204 when un-revoking. |
| Purge | DELETE /api-keys/{id}/purge | purge_api_key | Hard delete — 409 if the key is not revoked first. |
SDK coverage
Section titled “SDK coverage”| Capability | Python | Rust (openapp_sdk) | Go | TypeScript (AsyncClient) |
|---|---|---|---|---|
| Full lifecycle | client.api_keys — list, create, update, revoke, restore, purge | client.api_keys() — same method names | APIKeysAPI (generated) | Not on façade — use transport / another SDK |
Python create accepts convenience kwargs (label, scopes) plus any CreateApiKeyRequest fields via extra keywords — e.g. name, expires_in, scoped_roles — merged into the JSON body.
Typical errors
Section titled “Typical errors”401 when not authenticated.403 on create without api_keys:create (see server roles).404 on unknown key ids.409 when purge runs on an active key.400 on invalid expiry / scope payloads — see Errors & retries.
Examples
Section titled “Examples”List API keys
Section titled “List API keys”keys = await client.api_keys.list()keys, httpResp, err := client.APIKeysAPI.ListApiKeys(ctx).Execute()if err != nil { return err}defer httpResp.Body.Close()_ = keysuse openapp_sdk::Client;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let keys = client.api_keys().list().await?;const keys = await fetch("https://api.openapp.house/api/v1/api-keys", { headers: { authorization: "Bearer v1_openapp_YOUR_SECRET" },}).then( (r) => r.json() as Promise< Array<{ id: string; name: string; token_suffix: string }> >,);The active org is implied by the calling principal — no X-Org required. List items only carry token_suffix (the last few characters); the full token is only ever returned by POST /api-keys. Replace with AsyncClient.listApiKeys once the Node façade exposes it.
Mint an API key
Section titled “Mint an API key”created = await client.api_keys.create( name="Automation key", expires_in="90d", scoped_roles=["devices:list"],)token = created["token"]Persist token immediately; it is not returned again on later list calls.
req := openapiclient.NewCreateApiKeyRequest("Automation key")req.SetExpiresIn("90d")req.SetScopedRoles([]string{"devices:list"})resp, httpResp, err := client.APIKeysAPI.CreateApiKey(ctx).CreateApiKeyRequest(*req).Execute()if err != nil { return err}defer httpResp.Body.Close()_ = resp.GetToken()use openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let created = client .api_keys() .create(&json!({ "name": "Automation key", "expires_in": "90d", "scoped_roles": ["devices:list"] })) .await?;const created = await fetch("https://api.openapp.house/api/v1/api-keys", { method: "POST", headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "content-type": "application/json", }, body: JSON.stringify({ name: "Automation key", expires_in: "90d", scoped_roles: ["devices:list"], }),}).then( (r) => r.json() as Promise<{ id: string; token: string; token_suffix: string }>,);
const apiKey = created.token;Persist created.token immediately — it is only returned on the 201 mint response, never again on GET /api-keys. expires_at (RFC3339) and expires_in (duration string) are mutually exclusive. Replace with AsyncClient.createApiKey once the Node façade exposes it.
Rename or extend expiry (PATCH /api-keys/{id})
Section titled “Rename or extend expiry (PATCH /api-keys/{id})”Body UpdateApiKeyRequest — required name, optional expires_at (RFC3339) xor expires_in (duration string). Server returns 204 with an empty body.
await client.api_keys.update( key_id, name="Automation key (rotated)", expires_in="180d",)import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
upd := openapiclient.NewUpdateApiKeyRequest("Automation key (rotated)")upd.SetExpiresIn("180d")httpResp, err := client.APIKeysAPI.UpdateApiKey(context.Background(), keyID). UpdateApiKeyRequest(*upd). Execute()if err != nil { return err}defer httpResp.Body.Close()use openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
client .api_keys() .update( key_id, &json!({ "name": "Automation key (rotated)", "expires_in": "180d", }), ) .await?;await fetch(`https://api.openapp.house/api/v1/api-keys/${keyId}`, { method: "PATCH", headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "content-type": "application/json", }, body: JSON.stringify({ name: "Automation key (rotated)", expires_in: "180d", }),});Body matches UpdateApiKeyRequest — name is required; pass expires_at xor expires_in, never both. Server returns 204 with an empty body, so don’t call r.json(). Replace with AsyncClient.updateApiKey once the Node façade exposes it.
Revoke, restore, purge (DELETE / POST lifecycle)
Section titled “Revoke, restore, purge (DELETE / POST lifecycle)”revoke_api_key soft-revokes (the row stays for audit/restore); restore_api_key un-revokes; purge_api_key hard-deletes and returns 409 if the key has not been revoked first. All return 204 on success.
await client.api_keys.revoke(key_id)restored = await client.api_keys.restore(key_id)await client.api_keys.purge(key_id)httpResp, err := client.APIKeysAPI.RevokeApiKey(ctx, keyID).Execute()if err != nil { return err}defer httpResp.Body.Close()
httpResp2, err := client.APIKeysAPI.RestoreApiKey(ctx, keyID).Execute()if err != nil { return err}defer httpResp2.Body.Close()
httpResp3, err := client.APIKeysAPI.PurgeApiKey(ctx, keyID).Execute()if err != nil { return err}defer httpResp3.Body.Close()Quick path: api_keys().revoke / restore / purge send only the path and auth headers. If your gateway still expects org scope via X-Org (or returns 403 without it), call transport() + RequestSpec with extra_headers — same escape hatch as Devices lifecycle:
use openapp_sdk::Client;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
client.api_keys().revoke(key_id).await?;let restored = client.api_keys().restore(key_id).await?;client.api_keys().purge(key_id).await?;use openapp_sdk::{Client, transport::RequestSpec};use reqwest::Method;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let org_id = "01HORG00000000000000000000".to_string();let revoke_path = format!("/api-keys/{key_id}");client .transport() .request_json::<(), ()>(RequestSpec { method: Method::DELETE, path: &revoke_path, extra_headers: &[("X-Org", org_id.clone())], ..Default::default() }) .await?;
let restore_path = format!("/api-keys/{key_id}/restore");client .transport() .request_json::<(), ()>(RequestSpec { method: Method::POST, path: &restore_path, extra_headers: &[("X-Org", org_id.clone())], ..Default::default() }) .await?;
let purge_path = format!("/api-keys/{key_id}/purge");client .transport() .request_json::<(), ()>(RequestSpec { method: Method::DELETE, path: &purge_path, extra_headers: &[("X-Org", org_id)], ..Default::default() }) .await?;Purging an active key is rejected with 409 — call revoke first.
const headers = { authorization: "Bearer v1_openapp_YOUR_SECRET" };
await fetch(`https://api.openapp.house/api/v1/api-keys/${keyId}`, { method: "DELETE", headers,});
await fetch(`https://api.openapp.house/api/v1/api-keys/${keyId}/restore`, { method: "POST", headers,});
const purgeRes = await fetch( `https://api.openapp.house/api/v1/api-keys/${keyId}/purge`, { method: "DELETE", headers },);if (purgeRes.status === 409) { // Key is still active — call DELETE /api-keys/{id} (revoke) first.}All three endpoints return 204 on success — no body to decode. /purge rejects with 409 when the key has not been revoked yet, so always revoke (or check is_revoked from the list response) before purging. Replace with AsyncClient lifecycle helpers once the Node façade exposes them.