API key
The kinds of API key and their scopes, and how to issue and revoke them with a web login
An API key is a Bearer token for calling the /v1 session API. Send it with every request as Authorization: Bearer <key>. What a key can do is decided by its scope. Management APIs such as agents, members, and keys cannot be called with a key (403); they are called only with the web login cookie.
scope
| scope | What it does |
|---|---|
sessions:read | Read sessions, turns, events, usage, and receipts |
sessions:write | Create sessions, send messages, and request pushes |
sessions:control | interrupt, pause, resume, close, terminate, and change the profile |
sessions:approve | Answer permission requests and questions, approve or reject push requests, open PRs and comment on them |
sessions:recover | Recovery decisions. Not included in write or control |
A request without the scope gets 403 FORBIDDEN before the body is read. Both personal and agent keys can be issued with sessions:approve. No key approves requests of a session it started (403 self_approval); another key or a person approves them.
Kinds
| Kind | Issued by | How it behaves |
|---|---|---|
Personal key (personal) | Any member, their own only | Acts as the person who created it. Its scopes are the intersection of the key's scopes and the scopes that person's current role allows. If that person is deactivated, 401 |
Agent key (agent + agent_id) | owner, admin, and that agent's administrator | Acts as the key itself, not as a person. Creates and sees only sessions created from that agent's release. It keeps working after the person who issued it leaves. With sessions:approve it approves requests and answers questions in that agent's sessions, except sessions it started itself |
There is no key without an agent, that is, no key that reaches the whole workspace. Sending kind: "agent" without agent_id gets 400 BAD_REQUEST.
A key can carry a name (1 to 64 characters after trimming, not required) so you can tell it apart in the list. Without one it is null.
Without expires_at, a key stays valid until you revoke it. The web console's new agent key screen fills in an expiry date 90 days out; clear it to issue a key with no expiry.
Organization admin keys, owned by the organization and tied to no person (for automation such as Terraform, CI or SCIM), do not exist yet. The web console's API key screen shows their place as "Coming soon", to owners and admins only. Once they exist, each one will need an expiry date.
Issue a key
Issue it in the web console, or call POST /v1/api-keys with the web login cookie. The example below uses the WEB header from Create. The secret (secret) appears only once, in this response. Keep it in a variable and do not print it.
issued=$(curl -sS "$API/v1/api-keys" "${WEB[@]}" \
-d '{"kind": "personal", "name": "laptop", "scopes": ["sessions:read", "sessions:write"]}')
MY_KEY=$(echo "$issued" | jq -r .secret)
KEY_ID=$(echo "$issued" | jq -r .key.id)
echo "$issued" | jq '{kind: .key.kind, name: .key.name, scopes: .key.scopes, receipt: (.receipt_id != null)}'
curl -sS -w '\n' "$API/v1" -H "Authorization: Bearer $MY_KEY"{
"kind": "personal",
"name": "laptop",
"scopes": [
"sessions:read",
"sessions:write"
],
"receipt": true
}
{"status":"ok","owner_id":"…"}Requesting a scope beyond the issuer's own scopes returns 403 SCOPE_NOT_GRANTABLE.
List and revoke
The list shows only your own personal keys and the keys of the agents you manage. Secrets are not shown.
last_used_at is when the key last authenticated a request. It is recorded at most once a minute per key, and a use made while another request is writing a session with the same key is skipped and recorded on the next use, so it can lag by more than a minute. null means no use has been recorded since last-use recording began, not that the key was never used. created_by_user is the issuer's id and current display name. The name stays after the issuer leaves the workspace or their account is disabled. It is null on a key with no recorded issuer (one made with the operator CLI).
curl -sS "$API/v1/api-keys" "${WEB[@]}" | jq -c --arg id "$KEY_ID" '.items[] | select(.id == $id) | {kind, name, scopes, revoked_at, used: (.last_used_at != null), issuer: .created_by_user.display_name}'
curl -sS -X DELETE "$API/v1/api-keys/$KEY_ID" "${WEB[@]}" | jq '{receipt: (.receipt_id != null)}'
curl -sS -o /dev/null -w '%{http_code}\n' "$API/v1" -H "Authorization: Bearer $MY_KEY"{"kind":"personal","name":"laptop","scopes":["sessions:read","sessions:write"],"revoked_at":null,"used":true,"issuer":"…"}
{
"receipt": true
}
401A revoked key gets 401 from the next request. Revoking a key that is already revoked changes nothing and returns receipt_id: null.
Operator CLI
An operator can issue a key directly on the server. On the local stack it is scripts/local.sh key (Run a development server). This path does not go through the permission checks above.
scripts/local.sh key <owner> --scopes sessions:read,sessions:write
scripts/local.sh key --workspace <workspace-id> --personal <user-id> --scopes sessions:read,sessions:writeA key made for <owner> uses that name as its owner. --workspace goes only with --personal: the key is that member's personal key and acts as them. --workspace without --personal, or a workspace id in place of <owner>, is refused. No key reaches the whole workspace. Issue agent keys in the web console or with POST /v1/api-keys.
Use a key in the SDK
new Werft() reads the WERFT_BASE_URL and WERFT_API_KEY environment variables. To set them in code, pass baseUrl and apiKey. Check which key calls what with auth.me, and whether the server answers with root.get. No scope is needed.
import { Werft } from "@werft-ai/sdk";
const client = new Werft({
baseUrl: process.env.WERFT_BASE_URL,
apiKey: process.env.WERFT_API_KEY,
});
const me = await client.auth.me();
console.log(me.principal.kind, me.scopes);
const root = await client.root.get();
console.log(root.status);api_key [
'sessions:read',
'sessions:write',
'sessions:approve',
'sessions:control',
'sessions:recover'
]
okThe SDK sends the key only in the Authorization header, and it refuses to create a client for an http:// address that is not a browser or loopback address. The options are in Client options.
A personal or agent key issued in the web console cannot approve requests of a session it started (403 self_approval). Only the development server's operator key (scripts/local.sh key) is an exception. How to split the key that creates sessions from the key that approves is in A separate key for approvals. The agent key needs sessions:read, sessions:write, and sessions:control, and the approver key needs sessions:read and sessions:approve; the approver must also be in the session's share scope.