Authentication
Secret keys, publishable keys, end-user session tokens, scopes, rotation and SSO.
Three credential types, each for a different place in the stack.
| Credential | Format | Lives | Can do |
|---|---|---|---|
| Secret key | wb_sk_live_… | Server only | Everything in its scope set |
| Publishable key | wb_pk_live_… | Browser, mobile | Mint session tokens, nothing else |
| Session token | wb_st_… | An end user's client | Stream to one session, until it expires |
All three go in the same header:
Authorization: Bearer wb_sk_live_4kP9xQ2mR7tL5nW8vB3cY6jH1dF0gS
Secret keys
Created per project, scoped explicitly, shown once.
curl -sS "$WB_BASE_URL/api-keys" \
-H "Authorization: Bearer $WB_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "proj_7fTn21",
"name": "agent-backend",
"scopes": ["sessions:write", "runs:write", "fs:write", "usage:read"],
"expires_at": "2027-10-06T00:00:00Z"
}'
{
"id": "key_4Jd8Pq",
"kind": "secret",
"prefix": "wb_sk_live_4k",
"key": "wb_sk_live_4kP9xQ2mR7tL5nW8vB3cY6jH1dF0gS",
"scopes": ["sessions:write", "runs:write", "fs:write", "usage:read"],
"expires_at": "2027-10-06T00:00:00Z"
}
key appears in this response and never again. Only an Argon2id hash is stored. A lost key is
revoked and replaced, never recovered.
Scopes
| Scope | Grants |
|---|---|
sessions:read | Read and list sessions |
sessions:write | Create, mutate, delete sessions |
runs:read / runs:write | Read / create and cancel runs |
fs:read / fs:write | Read / write session filesystems |
browser:write | Browser actions and CDP |
desktop:write | Desktop actions and live view |
tools:invoke | Invoke tier 0 tools |
volumes:*, snapshots:*, templates:* | Their resources |
policies:write | Change policies — treat as privileged |
usage:read | Usage and cost data |
billing:* | Invoices, credits, payment methods |
admin:write | Members, roles, keys, org settings |
Grant the narrowest set that works. An agent backend almost never needs policies:write or
admin:write; a key with those is a path from a prompt injection to a disabled egress policy.
Rotation
- Create the replacement with the same scopes and a new name.
- Deploy it alongside the old key. Both work.
- Watch
last_used_aton the old key until it stops moving. - Revoke the old key. Revocation is immediate; the gateway's 60-second cache is invalidated on write.
Setting expires_at on every key makes rotation a calendar event rather than a thing nobody does.
Expiry warnings fire at 30, 7 and 1 days as api_key.expiring webhooks.
Leak response
Keys carry the wb_ marker so secret scanners recognise them. A key found in a public repository is
revoked automatically, the organization's owners are notified, and an audit entry is written. There
is no opt-out: an exposed key is a live credential to someone else's compute, billed to you.
Publishable keys
For code you ship to a client. A publishable key can do exactly one thing: mint a session token.
// browser — safe
const res = await fetch('https://api.workspaces.build/v1/auth/session-tokens', {
method: 'POST',
headers: {
Authorization: `Bearer ${PUBLISHABLE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ session_id: sessionId, scopes: ['desktop:view'] }),
})
The session must already exist and must have been created by your backend. A publishable key cannot create a session, so a stolen one buys an attacker nothing — it can only mint tokens for sessions they do not know the IDs of.
Session tokens
Short-lived, scoped to one session, safe to hand to an end user's browser. This is how a customer's own customer gets a live terminal or a desktop view without your backend proxying every frame.
curl -sS "$WB_BASE_URL/auth/session-tokens" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{
"session_id": "sess_2kQ9xwR4",
"end_user_id": "eu_customer_4821",
"scopes": ["pty", "fs:read"],
"ttl_s": 600
}'
{
"token": "wb_st_8Pq2Vd4Lm9Kx…",
"expires_at": "2026-10-06T09:31:12Z",
"session_id": "sess_2kQ9xwR4",
"scopes": ["pty", "fs:read"]
}
| Token scope | Grants |
|---|---|
exec | Run commands in the session |
pty | Interactive terminal |
fs:read / fs:write | Filesystem access |
cdp | Browser control |
desktop:view | Watch the desktop, no input |
desktop:control | Watch and control the desktop |
Default TTL is 600 seconds, maximum 3600. Tokens cannot be refreshed — mint a new one, which forces the backend to re-authorize the end user periodically rather than handing out a long-lived credential once.
Tokens are revoked when the session ends, when the issuing key is revoked, or explicitly via
DELETE /auth/session-tokens/{jti}.
A desktop:control token lets the holder drive a full desktop that may be logged into the
customer's systems. Treat it like a remote-access session: short TTL, one end user, and an audit
entry for every grant.
End-user identity
end_user_id is an attribution tag, not an authentication mechanism. Passing it does not prove the
caller is that end user — your backend authenticates your users and then tells us who they are.
The platform uses it for usage attribution, quotas, spend caps and reporting. It never uses it to authorize.
Dashboard authentication
Humans use OIDC, not API keys.
| Plan | Methods |
|---|---|
| All | Email with a one-time code, Google, GitHub |
| Team and above | Any OIDC provider, enforced MFA |
| Enterprise | SAML, SCIM provisioning, domain capture, session policy |
Dashboard sessions are separate from API credentials. A dashboard session cannot be exchanged for an API key, and reading a key's value is impossible for anyone including the platform.
Authorization model
Every object check is: does the principal's project own this object, and does the principal's scope
set permit this operation. Both must pass. There is no cross-project access from a project-scoped
key — an organization-wide operation needs an org-scoped key with admin:write.
Roles
| Role | Sessions | Policies | Keys | Billing | Members |
|---|---|---|---|---|---|
owner | Full | Full | Full | Full | Full |
admin | Full | Full | Full | Read | Full |
developer | Full | Read | Own keys | — | — |
operator | Read, terminate | Read | — | — | — |
billing | Read | — | — | Full | — |
viewer | Read | Read | — | Read | Read |
operator exists for on-call: enough to see and stop a runaway session, not enough to change what
runs or who can run it.
Failure responses
| Condition | Status | code |
|---|---|---|
| Missing or malformed header | 401 | unauthenticated |
| Unknown, revoked or expired key | 401 | invalid_credentials |
| Valid key, missing scope | 403 | insufficient_scope |
| Valid key, object in another project | 404 | not_found |
| Organization suspended | 403 | account_suspended |
| Token expired | 401 | token_expired |
Cross-project access returns 404, not 403. Confirming that an object exists in a project you
cannot reach is an information leak.