Cubicle

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 groupRoutes that accept API keys
BoxesAll: create, list, get, start, stop, hibernate, delete, base images
CommandsAll: exec and operations
SnapshotsAll: take, list, restore
AccountOnly 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 NAME in {box} resolves only in that organization.
  • Another organization in {org}, and another organization's Boxes and operations, return 404 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 codeMessageCause
401 unauthenticatedmissing or malformed bearer tokenNo Authorization header, a scheme other than Bearer, or a token that is neither a jvs_ nor a cbk_ token.
401 unauthenticatedAPI key is invalid, expired or revokedThe key is unknown, revoked or expired, or its service account was deleted or removed from the organization.
401 unauthenticatedlogin expired; run `cubicle login`The session is unknown, expired, revoked or signed out, or the person's account is disabled.
403 api_key_not_allowedAPI keys cannot use this routeAn API key was sent to a route that needs a sign-in session.
403 insufficient_scopethis API key is read-onlyA 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.

  1. 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 deviceCode secret: whoever holds it receives the session.

  2. Send the person to the Dashboard. Show them verificationUriComplete and userCode:

    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.

  3. Poll for the session. Call Exchange a device code for a session every interval seconds (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
    fi

    Until someone decides, the route answers 400 authorization_pending. access_denied (the person chose Deny), expired_token (600 s passed) and invalid_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"
    }
  4. Use the session. Send it like an API key:

    curl https://api.cubicle.sh/v1/me \
      -H "Authorization: Bearer $CUBICLE_SESSION_TOKEN"
  5. 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, with cubicle sessions revoke or in the Dashboard under Account → Sessions.
  • Starting sign-ins and polling are rate limited per client address. There is no RFC 8628 slow_down code: a refused poll gets 429 rate_limited, and you wait Retry-After seconds. See Rate limits.
  • While sign-in is paused, polls that would return a session fail with 503 logins_paused. Try again later.

On this page