//App Security API

App Security API

The App Security API covers the per-app credential surface: API key minting, OAuth provider configuration, WebAuthn relying-party configuration, the security audit log, and a small set of user-facing passkey-management endpoints. All admin endpoints under /api/v1/apps/:app_uuid/... require an authenticated admin JWT (a user with the admin role on the org that owns the app). The public auth endpoints under /api/v1/auth/... are anonymous — apps call them by supplying credentials directly in the request body or query string.

Endpoint Summary

| Method | Path | Purpose | |---|---|---| | POST | /api/v1/auth/api-key/exchange | Trade a raw API key (or refresh token) for an access JWT + refresh token pair | | GET | /api/v1/auth/oauth/{provider}/start | Begin a hosted OAuth flow (302 redirect to provider) | | GET | /api/v1/apps/{app_uuid}/oauth-configs | List OAuth provider configs for an app | | POST | /api/v1/apps/{app_uuid}/oauth-configs | Register a new OAuth provider config | | PATCH | /api/v1/apps/{app_uuid}/oauth-configs/{provider} | Partially update an OAuth provider config | | DELETE | /api/v1/apps/{app_uuid}/oauth-configs/{provider} | Remove an OAuth provider config | | GET | /api/v1/apps/{app_uuid}/api-keys | List API keys for an app (no plaintext) | | POST | /api/v1/apps/{app_uuid}/api-keys | Mint a new API key (plaintext returned once) | | DELETE | /api/v1/apps/{app_uuid}/api-keys/{key_uuid} | Revoke an API key | | POST | /api/v1/apps/{app_uuid}/api-keys/{key_uuid}/rotate | Rotate an API key (issue replacement) | | GET | /api/v1/apps/{app_uuid}/audit-log | Read recent security audit rows for an app | | GET | /api/v1/apps/{app_uuid}/rp-config | Read the per-app WebAuthn relying-party config | | PATCH | /api/v1/apps/{app_uuid}/rp-config | Partially update the per-app WebAuthn RP config | | GET | /api/v1/me/passkeys | List the signed-in user's enrolled passkeys | | DELETE | /api/v1/me/passkeys/{credential_uuid} | Revoke one of the signed-in user's passkeys |

Endpoints

POST /api/v1/auth/api-key/exchange

Anonymous endpoint. Accepts either a raw API key (wb_live_… / wb_test_…) for initial exchange, OR an opaque refresh token to rotate an existing pair. The presented refresh token is one-time-use: it is revoked as a side effect of a successful refresh.

Input (initial exchange): ~~~json { "api_key": "wb_live_3kP2nQ8x..." } ~~~

Input (refresh): ~~~json { "refresh_token": "rt_4f8a9c1e..." } ~~~

Output: ~~~json { "access_token": "eyJhbGciOiJSUzI1NiIs...", "refresh_token": "rt_4f8a9c1e...", "token_type": "Bearer", "access_expires_at": "2026-05-24T12:00:00Z", "refresh_expires_at": "2026-08-22T12:00:00Z" } ~~~

GET /api/v1/auth/oauth/{provider}/start

Begins a hosted OAuth flow for one of the supported providers (google, microsoft, github, apple). The server responds with a 302 to the provider's authorize URL, with a signed state carrying app_uuid + return_to. SDKs expose this as a URL builder — callers redirect the browser to the returned URL rather than performing an HTTP call themselves.

Input (query): ~~~json { "app_uuid": "550e8400-e29b-41d4-a716-446655440000", "return_to": "https://app.example.com/auth/callback" } ~~~

Output: 302 Found to the provider's authorize URL. No JSON body.

GET /api/v1/apps/{app_uuid}/oauth-configs

Lists every OAuth provider config registered for an app. client_secret is never returned — only metadata.

Input: (no body)

Output: ~~~json [ { "provider": "google", "client_id": "1234567890.apps.googleusercontent.com", "redirect_uris": ["https://app.example.com/auth/callback"], "scopes": ["openid", "email", "profile"], "enabled": true, "created_at": "2026-05-20T09:15:00Z", "updated_at": "2026-05-22T11:30:00Z" } ] ~~~

POST /api/v1/apps/{app_uuid}/oauth-configs

Registers a new OAuth provider config for the app. Every redirect_uris entry must be https://… or http://localhost… — plain HTTP to non-localhost is rejected. provider_extras carries provider-specific configuration: Apple expects {team_id, key_id, private_key}; the private_key PEM is encrypted at rest under a per-app DEK (envelope encryption via the platform vault).

Input: ~~~json { "provider": "google", "client_id": "1234567890.apps.googleusercontent.com", "client_secret": "GOCSPX-...", "redirect_uris": ["https://app.example.com/auth/callback"], "scopes": ["openid", "email", "profile"], "enabled": true, "provider_extras": {} } ~~~

Output: ~~~json { "provider": "google", "client_id": "1234567890.apps.googleusercontent.com", "redirect_uris": ["https://app.example.com/auth/callback"], "scopes": ["openid", "email", "profile"], "enabled": true, "created_at": "2026-05-24T09:15:00Z", "updated_at": "2026-05-24T09:15:00Z" } ~~~

PATCH /api/v1/apps/{app_uuid}/oauth-configs/{provider}

Partial update. Send only the fields you want to change. client_secret only rotates when sent as a non-empty value — sending "" or omitting it leaves the stored ciphertext untouched.

Input: ~~~json { "client_secret": "GOCSPX-newvalue...", "scopes": ["openid", "email", "profile", "https://www.googleapis.com/auth/calendar.readonly"], "enabled": false } ~~~

Output: OAuthConfigSummary (same shape as the list endpoint, no secret).

DELETE /api/v1/apps/{app_uuid}/oauth-configs/{provider}

Removes the OAuth provider config for the app. Existing sessions issued through this provider are unaffected.

Input: (no body)

Output: 204 No Content.

GET /api/v1/apps/{app_uuid}/api-keys

Lists every API key issued for the app — including revoked ones — without any plaintext key material. Each row is an ApiKeySummary carrying a key_prefix (visible identifier) and a last_four-style display fragment.

Input: (no body)

Output: ~~~json [ { "key_uuid": "8c1f...", "app_uuid": "550e8400-...", "name": "production-server", "key_prefix": "wb_live_3kP2", "key_type": "permanent", "last_four": "x7Qz", "expires_at": null, "last_used_at": "2026-05-23T18:42:11Z", "revoked_at": null, "created_at": "2026-05-20T09:15:00Z" } ] ~~~

POST /api/v1/apps/{app_uuid}/api-keys

Mints a new API key. The response includes raw_key — this is the only time the plaintext key is ever returned. The server stores only its SHA-256 hash; if the caller doesn't persist raw_key immediately it cannot be recovered, only rotated. For key_type: "expiring" provide an expiry of either {"absolute": ""} or {"in_days": N}.

Input: ~~~json { "name": "production-server", "env": "live", "key_type": "expiring", "expiry": { "in_days": 90 } } ~~~

Output: ~~~json { "key_uuid": "8c1f...", "raw_key": "wb_live_3kP2nQ8x...", "key_prefix": "wb_live_3kP2", "key_type": "expiring", "expires_at": "2026-08-22T09:15:00Z" } ~~~

DELETE /api/v1/apps/{app_uuid}/api-keys/{key_uuid}

Revokes an API key. Idempotent — revoking an already-revoked key is a no-op. Also invalidates every refresh token issued against this key.

Input: (no body)

Output: 204 No Content.

POST /api/v1/apps/{app_uuid}/api-keys/{key_uuid}/rotate

Issues a replacement key with the same name (suffixed (rotated)), type, and env as the original, then revokes the original. The backend honors a short grace window during which the old key continues to authenticate, letting callers redeploy without an instantaneous outage. Returns the same shape as create — raw_key shown once.

Input: (no body)

Output: ~~~json { "key_uuid": "f4d9...", "raw_key": "wb_live_9zM7vR2y...", "key_prefix": "wb_live_9zM7", "key_type": "expiring", "expires_at": "2026-08-22T09:15:00Z" } ~~~

GET /api/v1/apps/{app_uuid}/audit-log

Reads recent rows from the per-app security audit log, newest first. The backend defaults limit to 200 and caps it at 1000. Use action (server-side accepts an action_prefix parameter) to narrow by event family — e.g. api_key. matches api_key.created, api_key.revoked, api_key.rotated. Action strings emitted today: api_key.created, api_key.revoked, api_key.rotated, oauth_config.created, oauth_config.updated, oauth_config.deleted, passkey.revoked.

Input (query): ~~~json { "since": "2026-05-20T00:00:00Z", "limit": 100, "action_prefix": "api_key." } ~~~

Output: ~~~json [ { "id": 4821, "app_uuid": "550e8400-...", "actor_user_uuid": "1a2b3c4d-...", "action": "api_key.rotated", "target_id": "8c1f...", "details": { "previous_key_uuid": "8c1f...", "new_key_uuid": "f4d9..." }, "ip": "203.0.113.42", "user_agent": "ButtrbaseDashboard/1.0", "created_at": "2026-05-23T18:42:11Z" } ] ~~~

GET /api/v1/apps/{app_uuid}/rp-config

Reads the per-app WebAuthn relying-party config. rp_id is null when the app has no override and falls back to the deployment-wide BUTTRBASE_WEBAUTHN_RP_ID env var. rp_origins is the explicit allow-list of origins that may participate in passkey ceremonies for this app.

Input: (no body)

Output: ~~~json { "app_uuid": "550e8400-...", "rp_id": "app.example.com", "rp_origins": [ "https://app.example.com", "https://staging.example.com", "http://localhost:3001" ] } ~~~

PATCH /api/v1/apps/{app_uuid}/rp-config

Partial update — omit a field to leave it unchanged. Known SDK limitation: the typed SDK patch inputs (Python UpdateAppRpConfigRequest, Node UpdateAppRpConfigInput, Go UpdateAppRpConfigInput, Rust UpdateAppRpConfigRequest) can set* rp_id but cannot *clear it back to null. To revert to the env-var fallback you must send a raw {"rp_id": null} body — drop down to the underlying HTTP client or Client::request helper for that one-off.

Input: ~~~json { "rp_id": "app.example.com", "rp_origins": ["https://app.example.com", "http://localhost:3001"] } ~~~

Output: AppRpConfig (same shape as GET).

GET /api/v1/me/passkeys

Lists the signed-in user's enrolled passkeys across every app they have access to, in descending created_at order. Requires a user bearer token (no admin scope). Each row carries a credential_uuid (for revocation) and a 12-char credential_id_prefix for display.

Input: (no body)

Output: ~~~json [ { "credential_uuid": "9b8a7c6d-...", "credential_id_prefix": "AQIDBAUGBwgJ", "app_uuid": "550e8400-...", "nickname": "MacBook Pro Touch ID", "last_used_at": "2026-05-23T08:11:00Z", "created_at": "2026-05-15T14:22:00Z" } ] ~~~

DELETE /api/v1/me/passkeys/{credential_uuid}

Revokes one of the signed-in user's enrolled passkeys. The owner check is enforced on the backend — passing a UUID that belongs to another user returns 404 (not 403, to avoid leaking existence).

Input: (no body)

Output: ~~~json { "status": "revoked" } ~~~

SDK Examples

POST /api/v1/auth/api-key/exchange

#### Initial exchange (api_key)

##### Python ~~~python from buttrbase import ButtrbaseClient

client = ButtrbaseClient(api_key="bb_live_admin_token") resp = client.exchange_api_key("wb_live_3kP2nQ8x...") print(resp["access_token"], resp["refresh_token"]) ~~~

##### Node.js / TypeScript ~~~typescript import { ButtrbaseClient } from '@buttrbase/sdk';

const client = new ButtrbaseClient({ apiKey: 'bb_live_admin_token' }); const resp = await client.exchangeApiKey('wb_live_3kP2nQ8x...'); console.log(resp.access_token, resp.refresh_token); ~~~

##### Go ~~~go c := buttrbase.New("bb_live_admin_token") resp, err := c.ExchangeAPIKey(ctx, "wb_live_3kP2nQ8x...") if err != nil { return err } fmt.Println(resp.AccessToken, resp.RefreshToken) ~~~

##### Rust ~~~rust let client = ButtrBaseClient::new_with_credentials("bb_live_admin_token", "..."); let resp = client.exchange_api_key("wb_live_3kP2nQ8x...").await?; println!("{} {}", resp.access_token, resp.refresh_token); ~~~

#### Refresh exchange (refresh_token)

##### Python ~~~python resp = client.exchange_refresh_token("rt_4f8a9c1e...") print(resp["access_token"]) ~~~

##### Node.js / TypeScript ~~~typescript const resp = await client.exchangeRefreshToken('rt_4f8a9c1e...'); console.log(resp.access_token); ~~~

##### Go ~~~go resp, err := c.ExchangeRefreshToken(ctx, "rt_4f8a9c1e...") ~~~

##### Rust ~~~rust let resp = client.exchange_refresh_token("rt_4f8a9c1e...").await?; ~~~

GET /api/v1/auth/oauth/{provider}/start

The SDKs expose this as a URL builder, not an HTTP call — the caller redirects the browser to the returned URL and the backend responds with a 302 to the upstream provider.

#### Python ~~~python url = client.oauth_start_url( "google", "550e8400-e29b-41d4-a716-446655440000", "https://app.example.com/auth/callback", )

redirect the browser to url

~~~

#### Node.js / TypeScript ~~~typescript const url = client.oauthStartUrl( 'google', '550e8400-e29b-41d4-a716-446655440000', 'https://app.example.com/auth/callback', ); window.location.href = url; ~~~

#### Go ~~~go url := c.OAuthStartURL(buttrbase.OAuthProviderGoogle, "550e8400-e29b-41d4-a716-446655440000", "https://app.example.com/auth/callback") // 302 the user-agent to url ~~~

#### Rust ~~~rust let url = client.oauth_start_url( OAuthProvider::Google, Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000")?, "https://app.example.com/auth/callback", ); // redirect browser to url ~~~

GET /api/v1/apps/{app_uuid}/oauth-configs

#### Python ~~~python configs = client.list_oauth_configs("550e8400-...") for c in configs: print(c["provider"], c["enabled"]) ~~~

#### Node.js / TypeScript ~~~typescript const configs = await client.listOAuthConfigs('550e8400-...'); configs.forEach((c) => console.log(c.provider, c.enabled)); ~~~

#### Go ~~~go configs, err := c.ListOAuthConfigs(ctx, "550e8400-...") for _, cfg := range configs { fmt.Println(cfg.Provider, cfg.Enabled) } ~~~

#### Rust ~~~rust let configs = client.list_oauth_configs(app_uuid).await?; for cfg in &configs { println!("{} {}", cfg.provider, cfg.enabled); } ~~~

POST /api/v1/apps/{app_uuid}/oauth-configs

#### Python ~~~python config = client.create_oauth_config( "550e8400-...", { "provider": "google", "client_id": "1234567890.apps.googleusercontent.com", "client_secret": "GOCSPX-...", "redirect_uris": ["https://app.example.com/auth/callback"], "scopes": ["openid", "email", "profile"], "enabled": True, "provider_extras": {}, }, ) ~~~

#### Node.js / TypeScript ~~~typescript const config = await client.createOAuthConfig('550e8400-...', { provider: 'google', client_id: '1234567890.apps.googleusercontent.com', client_secret: 'GOCSPX-...', redirect_uris: ['https://app.example.com/auth/callback'], scopes: ['openid', 'email', 'profile'], enabled: true, }); ~~~

#### Go ~~~go cfg, err := c.CreateOAuthConfig(ctx, "550e8400-...", buttrbase.CreateOAuthConfigInput{ Provider: "google", ClientID: "1234567890.apps.googleusercontent.com", ClientSecret: "GOCSPX-...", RedirectURIs: []string{"https://app.example.com/auth/callback"}, Scopes: []string{"openid", "email", "profile"}, Enabled: true, }) ~~~

#### Rust ~~~rust let cfg = client .create_oauth_config(app_uuid, &CreateOAuthConfigRequest { provider: "google".into(), client_id: "1234567890.apps.googleusercontent.com".into(), client_secret: "GOCSPX-...".into(), redirect_uris: vec!["https://app.example.com/auth/callback".into()], scopes: vec!["openid".into(), "email".into(), "profile".into()], enabled: true, }) .await?; ~~~

PATCH /api/v1/apps/{app_uuid}/oauth-configs/{provider}

#### Python ~~~python updated = client.update_oauth_config( "550e8400-...", "google", {"client_secret": "GOCSPX-rotated...", "enabled": False}, ) ~~~

#### Node.js / TypeScript ~~~typescript const updated = await client.updateOAuthConfig('550e8400-...', 'google', { client_secret: 'GOCSPX-rotated...', enabled: false, }); ~~~

#### Go ~~~go secret := "GOCSPX-rotated..." enabled := false updated, err := c.UpdateOAuthConfig(ctx, "550e8400-...", "google", buttrbase.UpdateOAuthConfigInput{ClientSecret: &secret, Enabled: &enabled}) ~~~

#### Rust ~~~rust let patch = UpdateOAuthConfigRequest { client_secret: Some("GOCSPX-rotated...".into()), enabled: Some(false), ..Default::default() }; let updated = client.update_oauth_config(app_uuid, "google", &patch).await?; ~~~

DELETE /api/v1/apps/{app_uuid}/oauth-configs/{provider}

#### Python ~~~python client.delete_oauth_config("550e8400-...", "google") ~~~

#### Node.js / TypeScript ~~~typescript await client.deleteOAuthConfig('550e8400-...', 'google'); ~~~

#### Go ~~~go err := c.DeleteOAuthConfig(ctx, "550e8400-...", "google") ~~~

#### Rust ~~~rust client.delete_oauth_config(app_uuid, "google").await?; ~~~

GET /api/v1/apps/{app_uuid}/api-keys

#### Python ~~~python keys = client.list_app_api_keys("550e8400-...") for k in keys: print(k["name"], k["key_prefix"], k["revoked_at"]) ~~~

#### Node.js / TypeScript ~~~typescript const keys = await client.listAppApiKeys('550e8400-...'); keys.forEach((k) => console.log(k.name, k.key_prefix, k.revoked_at)); ~~~

#### Go ~~~go keys, err := c.ListAppAPIKeys(ctx, "550e8400-...") for _, k := range keys { fmt.Println(k.Name, k.KeyPrefix, k.RevokedAt) } ~~~

#### Rust ~~~rust let keys = client.list_app_api_keys(app_uuid).await?; for k in &keys { println!("{} {} {:?}", k.name, k.key_prefix, k.revoked_at); } ~~~

POST /api/v1/apps/{app_uuid}/api-keys

Plaintext raw_key is returned exactly once — persist it before doing anything else with the response.

#### Python ~~~python created = client.create_app_api_key( "550e8400-...", {"name": "production-server", "env": "live", "key_type": "expiring", "expiry": {"in_days": 90}}, ) save_secret(created["raw_key"]) ~~~

#### Node.js / TypeScript ~~~typescript const created = await client.createAppApiKey('550e8400-...', { name: 'production-server', env: 'live', key_type: 'expiring', expiry: { in_days: 90 }, }); await saveSecret(created.raw_key); ~~~

#### Go ~~~go created, err := c.CreateAppAPIKey(ctx, "550e8400-...", buttrbase.CreateAPIKeyInput{ Name: "production-server", Env: "live", KeyType: "expiring", Expiry: &buttrbase.ExpiryInput{InDays: 90}, }) saveSecret(created.RawKey) ~~~

#### Rust ~~~rust let created = client .create_app_api_key(app_uuid, &CreateApiKeyRequest { name: "production-server".into(), env: "live".into(), key_type: KeyType::Expiring(ExpiryInput::InDays(90)), }) .await?; save_secret(&created.raw_key); ~~~

DELETE /api/v1/apps/{app_uuid}/api-keys/{key_uuid}

#### Python ~~~python client.revoke_app_api_key("550e8400-...", "8c1f...") ~~~

#### Node.js / TypeScript ~~~typescript await client.revokeAppApiKey('550e8400-...', '8c1f...'); ~~~

#### Go ~~~go err := c.RevokeAppAPIKey(ctx, "550e8400-...", "8c1f...") ~~~

#### Rust ~~~rust client.revoke_app_api_key(app_uuid, key_uuid).await?; ~~~

POST /api/v1/apps/{app_uuid}/api-keys/{key_uuid}/rotate

Returns a new raw_key; the original is revoked but honors a brief grace window so callers can redeploy without an instantaneous outage.

#### Python ~~~python rotated = client.rotate_app_api_key("550e8400-...", "8c1f...") save_secret(rotated["raw_key"]) ~~~

#### Node.js / TypeScript ~~~typescript const rotated = await client.rotateAppApiKey('550e8400-...', '8c1f...'); await saveSecret(rotated.raw_key); ~~~

#### Go ~~~go rotated, err := c.RotateAppAPIKey(ctx, "550e8400-...", "8c1f...") saveSecret(rotated.RawKey) ~~~

#### Rust ~~~rust let rotated = client.rotate_app_api_key(app_uuid, key_uuid).await?; save_secret(&rotated.raw_key); ~~~

GET /api/v1/apps/{app_uuid}/audit-log

#### Python ~~~python rows = client.read_audit_log( "550e8400-...", limit=100, action_prefix="api_key.", ) for r in rows: print(r["created_at"], r["action"], r["actor_user_uuid"]) ~~~

#### Node.js / TypeScript ~~~typescript const rows = await client.readAuditLog('550e8400-...', { limit: 100, action_prefix: 'api_key.', }); rows.forEach((r) => console.log(r.created_at, r.action, r.actor_user_uuid)); ~~~

#### Go ~~~go rows, err := c.ReadAuditLog(ctx, "550e8400-...", buttrbase.AuditLogQuery{ Limit: 100, ActionPrefix: "api_key.", }) for _, r := range rows { fmt.Println(r.CreatedAt, r.Action, r.ActorUserUUID) } ~~~

#### Rust ~~~rust let rows = client .read_audit_log(app_uuid, AuditLogQuery { limit: Some(100), action_prefix: Some("api_key.".into()), }) .await?; for r in &rows { println!("{} {} {:?}", r.created_at, r.action, r.actor_user_uuid); } ~~~

GET /api/v1/apps/{app_uuid}/rp-config

#### Python ~~~python rp = client.get_app_rp_config("550e8400-...") print(rp["rp_id"], rp["rp_origins"]) ~~~

#### Node.js / TypeScript ~~~typescript const rp = await client.getAppRpConfig('550e8400-...'); console.log(rp.rp_id, rp.rp_origins); ~~~

#### Go ~~~go rp, err := c.GetAppRPConfig(ctx, "550e8400-...") fmt.Println(rp.RPID, rp.RPOrigins) ~~~

#### Rust ~~~rust let rp = client.get_app_rp_config(app_uuid).await?; println!("{:?} {:?}", rp.rp_id, rp.rp_origins); ~~~

PATCH /api/v1/apps/{app_uuid}/rp-config

#### Python ~~~python updated = client.update_app_rp_config( "550e8400-...", {"rp_id": "app.example.com", "rp_origins": ["https://app.example.com", "http://localhost:3001"]}, ) ~~~

#### Node.js / TypeScript ~~~typescript const updated = await client.updateAppRpConfig('550e8400-...', { rp_id: 'app.example.com', rp_origins: ['https://app.example.com', 'http://localhost:3001'], }); ~~~

#### Go ~~~go rpID := "app.example.com" origins := []string{"https://app.example.com", "http://localhost:3001"} updated, err := c.UpdateAppRPConfig(ctx, "550e8400-...", buttrbase.UpdateAppRpConfigInput{ RPID: &rpID, RPOrigins: &origins, }) ~~~

#### Rust ~~~rust let patch = UpdateAppRpConfigRequest { rp_id: Some("app.example.com".into()), rp_origins: Some(vec![ "https://app.example.com".into(), "http://localhost:3001".into(), ]), }; let updated = client.update_app_rp_config(app_uuid, &patch).await?; ~~~

GET /api/v1/me/passkeys

#### Python ~~~python passkeys = client.list_my_passkeys() for p in passkeys: print(p["credential_uuid"], p["nickname"], p["last_used_at"]) ~~~

#### Node.js / TypeScript ~~~typescript const passkeys = await client.listMyPasskeys(); passkeys.forEach((p) => console.log(p.credential_uuid, p.nickname, p.last_used_at)); ~~~

#### Go ~~~go passkeys, err := c.ListMyPasskeys(ctx) for _, p := range passkeys { fmt.Println(p.CredentialUUID, p.Nickname, p.LastUsedAt) } ~~~

#### Rust ~~~rust let passkeys = client.list_my_passkeys().await?; for p in &passkeys { println!("{} {:?} {:?}", p.credential_uuid, p.nickname, p.last_used_at); } ~~~

DELETE /api/v1/me/passkeys/{credential_uuid}

#### Python ~~~python client.delete_my_passkey("9b8a7c6d-...") ~~~

#### Node.js / TypeScript ~~~typescript const res = await client.deleteMyPasskey('9b8a7c6d-...'); console.log(res.status); // "revoked" ~~~

#### Go ~~~go err := c.DeleteMyPasskey(ctx, "9b8a7c6d-...") ~~~

#### Rust ~~~rust client.delete_my_passkey(credential_uuid).await?; ~~~

Error Reference

| Status | Code | Meaning | |---|---|---| | 400 | bad_request | Malformed payload, unknown / unsupported provider, invalid redirect URI scheme, or unknown key_type | | 401 | unauthorized | Missing bearer token, malformed JWT, or token expired / revoked | | 403 | forbidden | Authenticated but caller does not hold the admin role on the app's org | | 404 | not_found | App UUID, key UUID, OAuth provider config, or passkey credential UUID does not exist (or is not owned by the caller) | | 409 | conflict | Duplicate resource — e.g. an OAuth config for that provider already exists on the app | | 500 | internal | Envelope encryption misconfigured (DEK unavailable), platform vault unreachable, or database failure |