Managing keys
Every key operation in the portal is also reachable from the SDK β they hit the same /api/v1/connect-keys/* backend, share the same validation, the same audit log, the same cache invalidation. Pick whichever fits your workflow.
What a key carries
A connect key is a sk_* bearer plus the metadata below. The first two come from WorkOS (the secret store); the rest live in our mirror and can be edited without rotating:
| Field | Editable? | Notes |
|---|---|---|
name | via mirror (label-only β WorkOS-side stays original) | Human-readable. |
permissions | no β mint a replacement key | WorkOS doesn't expose PATCH on api_keys today; rotation inherits the parent's list verbatim. |
workspace_id | yes | Pin the key to one workspace in a multi-workspace org. null = earliest workspace (default). |
rate_limit_per_minute | yes | Wins over org override and default. 0 blocks all requests; null clears the override. |
description | yes | Free-text label visible on the portal. |
environment | yes | sandbox or live. Portal listing filter. |
Minting a key
Portal
- Open Dashboard β Keys.
- Click New key.
- Name + permissions are required. Workspace pin / rate-limit / description / environment hide behind the Advanced disclosure β the basic flow stays fast.
- Click Create key. The full
sk_β¦value appears once with copy + multi-language code samples. WorkOS will not show it again.
SDK
from heyarchie import Archie
client = Archie() # picks up ARCHIE_API_KEY env var
key = client.keys.create(
name="ci-deploys",
permissions=["ask:execute", "search:read"],
workspace_id="b0000000-0000-0000-0000-000000000001", # optional
rate_limit_per_minute=30, # optional
description="staging deploy bot", # optional
environment="sandbox", # default
)
# Surface key.value to the operator immediately and discard.
# WorkOS will not return it again.
print(key.value)On a successful create the backend INSERTs the mirror row + the seed populates the per-key rate-limit cache, so any cap you set is enforced from the very first request through the new key.
Permission slugs must come from the current 11-scope catalog β the same taxonomy the scopes table documents, derived live from the skill registry. A retired or unknown slug (e.g. the old messages:create) fails the create with 400 unknown_permissions, and the response lists the valid slugs. Keys minted under the old vocabulary still authenticate, but a retired slug no longer matches any skill's required permission β mint a replacement key with current scopes and revoke the old one.
Editing per-key metadata
Portal
Click the chevron on the key row to expand its detail panel. The three editable fields (workspace pin, rate limit, description) auto-save 600ms after you stop typing; a small saved pill confirms.
SDK
# Pin to a workspace
client.keys.update(key.id, workspace_id="b0000000-β¦")
# Set per-key rate limit (None to clear, 0 to block)
client.keys.update(key.id, rate_limit_per_minute=30)
# Update multiple fields in one call
client.keys.update(
key.id,
description="staging deploy bot β updated",
rate_limit_per_minute=60,
)Trying to PATCH permissions returns 501 β WorkOS doesn't expose a PATCH on api_keys today β and rotate copies the parent's permission list verbatim. To change a key's permissions, create a replacement key with the scopes you want and revoke the old one.
Rotating
Rotation mints a replacement key inheriting name / permissions / workspace_id / rate_limit_per_minute / description / environment from the parent, and stamps expires_at = now() + grace_period_hours on the parent. A backend reaper job revokes the parent after expiry.
Portal
Hover the key row, click Rotate. The new value appears once.
SDK
# Default: parent dies after 24h
new_key = client.keys.rotate(key.id)
# Or shorter grace window
new_key = client.keys.rotate(key.id, grace_period_hours=1)
print(new_key.value) # one-time replacement sk_β¦
print(new_key.rotated_from_key_id) # = old key.idRevoking
Revoke is immediate. The backend deletes the WorkOS key, stamps revoked_at on the mirror row, and flushes both caches (the WorkOS validator + the per-key rate-limit cache) so any in-flight cached token gets 401 on the very next request β no 5-minute staleness window.
Portal
Hover the row, click Revoke, confirm.
SDK
client.keys.revoke(key.id)
# Subsequent calls through this key 401 with auth_required.Listing + inspecting
page = client.keys.list()
for k in page.data:
print(k.id, k.obfuscated_value, k.workspace_id, k.rate_limit_per_minute)
# Single key, merged shape:
key = client.keys.get("api_key_01KRVGBβ¦")
print(key.description, key.environment, key.revoked_at)What never leaves the backend
- Your
WORKOS_API_KEYβ only ours, server-side. - The full
sk_*value after the one-time reveal β WorkOS never returns it again. - The org-level WorkOS admin token used by the Management API.
Browser sees only the merged response shape with obfuscated_value (the sk_β¦Ozgi preview). Open dev-tools on /dashboard/keys and confirm β zero calls to api.workos.com.
Next
- Authentication β scopes table + the JWT / OAuth alternatives.
- Rate limits β the override resolution order in detail.
- Python SDK β every
client.keys.*method.