Authentication
Authenticate Cubicle HTTP API requests with an Authorization Bearer header and a service account API key (cbk_) or a sign-in session (jvs_), covering routes that accept keys, read and write scopes, the organization fence, authentication errors, token lifetimes and the device authorization flow for tools that sign people in.
Every request carries a bearer token in the Authorization header, except the two device sign-in
routes, which need none:
curl https://api.cubicle.sh/v1/me \
-H "Authorization: Bearer $CB_SDK_API_KEY"Write the scheme as Bearer, followed by one space and the token. Any other form fails with
401 unauthenticated and the message missing or malformed bearer token.
Two kinds of token
An API key (cbk_…) acts as a service account, such as acme/ci, and works only on the
routes that accept API keys, in its own organization. A sign-in
session (jvs_…) acts as a person and can call every route their roles and grants allow. It comes
from the Dashboard, cubicle login or the device flow, and lasts
30 days from sign-in, however often it is used. Sign in and API keys compares
the two and shows how to create, scope, expire and revoke keys.
Use an API key for programs: CI jobs, agents and services. Use a sign-in session only in a tool that acts for a person and needs a route that refuses keys, such as adding an SSH key or granting access to a Box.
Routes that accept API keys
| Endpoint group | Routes that accept API keys |
|---|---|
| Boxes | All: create, list, get, start, stop, hibernate, delete, base images |
| Commands | All: exec and operations |
| Snapshots | All: take, list, restore |
| Account | Only Get the caller (GET /v1/me) |
A read key may send only GET requests; any other method fails with 403 insufficient_scope and
this API key is read-only. A write key may send them all. A scope never widens what the service
account itself may do: a write key still needs a grant on a Box to run commands there, and a
grant or the admin role to stop or delete it. See
Service accounts.
Every other route refuses keys: Box access, Organizations,
Service accounts, sessions and SSH keys under Account,
sign-out, and the routes the Dashboard and the CLI use for
the private network and the desktop console. They answer 403 api_key_not_allowed to any cbk_
token, valid or not. So a service account cannot manage service accounts or keys, register an SSH
key, SSH into a Box, join the private network or open the console. It runs commands with exec
instead.
The organization fence
An API key works only inside its service account's organization:
- Get the caller and List the caller's Boxes show only that organization.
- A bare
NAMEin{box}resolves only in that organization. - Another organization in
{org}, and another organization's Boxes and operations, return404 not_found, as if they did not exist. This holds for IDs too.
A sign-in session has no fence: it reaches every organization where the person is an active member.
Organizations where they are not a member return 404 not_found as well.
Authentication errors
| Status and code | Message | Cause |
|---|---|---|
401 unauthenticated | missing or malformed bearer token | No Authorization header, a scheme other than Bearer, or a token that is neither a jvs_ nor a cbk_ token. |
401 unauthenticated | API key is invalid, expired or revoked | The key is unknown, revoked or expired, or its service account was deleted or removed from the organization. |
401 unauthenticated | login expired; run `cubicle login` | The session is unknown, expired, revoked or signed out, or the person's account is disabled. |
403 api_key_not_allowed | API keys cannot use this route | An API key was sent to a route that needs a sign-in session. |
403 insufficient_scope | this API key is read-only | A read key was sent with a method other than GET. |
Do not retry these unchanged: fix the header, or get a new token. Errors lists every other code.
Sign people in from your tool
A tool that acts for a person, such as an agent harness or an internal CLI, can sign them in with
the device authorization flow (RFC 8628) that
cubicle login uses. Your tool only polls; nothing calls back into it, so it works over SSH and
behind NAT. The result is a sign-in session for the person who approves it.
-
Start the sign-in. Start a device sign-in needs no credentials and no body:
started=$(curl -sS -X POST https://api.cubicle.sh/v1/auth/device) echo "$started"{ "deviceCode": "jvd_qRbPpKtAPzyMvWuZ4Pp_3fQxlO0lB7qDcE9E2MpjzJM", "userCode": "BCDF-GHJK", "expiresIn": 600, "interval": 2, "verificationUri": "https://app.cubicle.sh/device", "verificationUriComplete": "https://app.cubicle.sh/device?user_code=BCDF-GHJK" }Keep
deviceCodesecret: whoever holds it receives the session. -
Send the person to the Dashboard. Show them
verificationUriCompleteanduserCode:echo "$started" | jq -r '"Open \(.verificationUriComplete) and check the code \(.userCode)."'They sign in if needed, check that the page shows the same code, and choose Authorize or Deny. The page has no field for typing a code, so send the complete link. The page is titled Authorize Cubicle CLI for every tool that uses this flow.
-
Poll for the session. Call Exchange a device code for a session every
intervalseconds (2) until it returns a session or a final error:device_code=$(echo "$started" | jq -r .deviceCode) while :; do sleep 2 response=$(curl -sS -X POST https://api.cubicle.sh/v1/auth/device/token \ -H "Content-Type: application/json" \ -d "{\"deviceCode\": \"$device_code\"}") error=$(echo "$response" | jq -r '.error.code // empty') case $error in authorization_pending) ;; rate_limited) sleep "$(echo "$response" | jq -r .error.retryAfterSeconds)" ;; *) break ;; esac done if [ -z "$error" ]; then CUBICLE_SESSION_TOKEN=$(echo "$response" | jq -r .token) else echo "sign-in failed: $error" >&2 fiUntil someone decides, the route answers
400 authorization_pending.access_denied(the person chose Deny),expired_token(600 s passed) andinvalid_grant(the code is unknown or already used) are final. After Authorize, the next poll returns the session:{ "token": "jvs_lhxa_0pvIYcKCzNEyglwfzH8TbnsmXmRVcCTJ1xaQr8", "identity": "ada@example.com", "expiresAt": "2026-12-05T10:16:40.000Z" } -
Use the session. Send it like an API key:
curl https://api.cubicle.sh/v1/me \ -H "Authorization: Bearer $CUBICLE_SESSION_TOKEN" -
Sign out when you are done. Sign out revokes the session that sends it and returns
204:curl -X POST https://api.cubicle.sh/v1/auth/logout \ -H "Authorization: Bearer $CUBICLE_SESSION_TOKEN"
- The session belongs to whoever chooses Authorize, not to whoever started the sign-in. It can call every route that person's roles and grants allow, for 30 days. Store it like a password.
- It shows as
client: "cli"in List your sessions. The person can end it there, withcubicle sessions revokeor in the Dashboard under Account → Sessions. - Starting sign-ins and polling are rate limited per client address. There is no RFC 8628
slow_downcode: a refused poll gets429 rate_limited, and you waitRetry-Afterseconds. See Rate limits. - While sign-in is paused, polls that would return a session fail with
503 logins_paused. Try again later.
HTTP API
The Cubicle HTTP API at https://api.cubicle.sh/v1, with the base URL, a first authenticated curl request, JSON, UUID and ISO 8601 conventions, Box and organization references in paths, the OpenAPI 3.1 document for generating clients in other languages, and an index of every endpoint with its TypeScript SDK method.
Requests and waiting
How asynchronous Cubicle HTTP API requests work, with 202 Accepted and desired state, following a request by polling a Box's state, generation and ssh.ready, failure and superseded signals, deletion, exec, snapshot and restore operations, Idempotency-Key retries and polling budgets.

