Ada AI

CLI Authentication

The browser-assisted device flow lets a CLI tool mint a proxy key without ever handling the user's session credentials.

The device flow is a browser-assisted, OAuth-style handshake that lets a CLI tool mint a proxy key (sk-rc-…) without ever handling the user's session credentials. The CLI initiates the request and polls a backend endpoint while the user approves (or denies) it in their browser. This keeps secrets out of the terminal and out of shell history.

If your workflow is purely browser-based, just mint a key from the dashboard instead — see API keys.

At a glance

CLI                                  Backend                  Browser (user)
 |                                      |                           |
 |-- POST /auth/device/code ----------->|                           |
 |<- {device_code, user_code, url} -----|                           |
 |                                      |                           |
 | print url + user_code to terminal    |                           |
 |                                      |<-- GET /auth/device -------|
 |                                      |--- show code + buttons -->|
 |                                      |                           |
 |-- POST /auth/device/token ---------->|  [user clicks Approve]    |
 |<- 429 authorization_pending ---------|                           |
 |-- POST /auth/device/token ---------->|                           |
 |<- 200 {api_key} -------------------->|                           |
 |                                      |                           |
 | save api_key to ~/.config/ada/...    |                           |
  • Poll /auth/device/token on the interval cadence returned by /auth/device/code (default: 5 s).
  • A 429 with authorization_pending is normal — keep polling.
  • The token endpoint is read-once: the first 200 response destroys the pending record.

Endpoints

POST /auth/device/code — initiate

Starts a new device-flow session. No authentication required.

Request body (all fields optional):

{ "name": "alice", "hostname": "laptop.local" }

Response (200):

{
  "device_code": "<opaque string>",
  "user_code": "ABCD-1234",
  "verification_uri": "https://ada.ai/auth/device",
  "verification_uri_complete": "https://ada.ai/auth/device?user_code=ABCD-1234",
  "expires_in": 600,
  "interval": 5
}
  • device_code — bearer token used in polling. Treat as a secret.
  • user_code — short code the user types, embedded in verification_uri_complete.
  • expires_in — seconds until the session expires (600 s / 10 min).
  • interval — minimum polling cadence in seconds.

POST /auth/device/token — poll

Called repeatedly by the CLI until the user approves, denies, or the session expires. No authentication required.

Request body:

{ "device_code": "<device_code from initiate>" }

Approved (200):

{ "api_key": "sk-rc-...", "issued_at_ms": 1747440000000 }

This response is read-once. A second call with the same device_code returns expired_token.

Pending / slow down (429):

{ "error": { "type": "authorization_pending", "interval": 5 } }
{ "error": { "type": "slow_down", "interval": 5 } }

Terminal errors (400):

{ "error": { "type": "access_denied", "message": "Request was denied." } }
{ "error": { "type": "expired_token", "message": "Session expired or already consumed." } }

Error vocabulary

typeMeaning and recommended CLI behavior
authorization_pendingUser hasn't acted yet. Keep polling at interval.
slow_downYou're polling faster than allowed. Wait the interval and retry.
access_deniedUser clicked Deny. Abort with a clear message; do not retry.
expired_tokenThe 10-minute window closed, or the key was already consumed. Exit and prompt the user to re-run.
invalid_requestMalformed body or unknown device_code. Log the response; do not retry.
rate_limitedToo many initiation requests from this IP. Back off and surface the error.

Reference shell loop

Requires curl and jq. Set ADA_API_BASE to point at a non-default backend.

#!/usr/bin/env bash
set -euo pipefail

API="${ADA_API_BASE:-https://api.ada.ai}"
CRED_PATH="${HOME}/.config/ada/credentials"

start=$(curl -sS -X POST "${API}/auth/device/code" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"$(whoami)\",\"hostname\":\"$(hostname)\"}")
device_code=$(echo "$start" | jq -r .device_code)
user_code=$(echo "$start"   | jq -r .user_code)
url=$(echo "$start"         | jq -r .verification_uri_complete)
interval=$(echo "$start"    | jq -r .interval)

echo "Open this URL in your browser:"
echo "  $url"
echo
echo "Or enter this code at $(echo "$start" | jq -r .verification_uri):"
echo "  $user_code"
echo
echo "Waiting for approval..."

while true; do
  sleep "$interval"
  resp=$(curl -sS -X POST "${API}/auth/device/token" \
    -H "Content-Type: application/json" \
    -d "{\"device_code\":\"$device_code\"}")
  type=$(echo "$resp" | jq -r '.error.type // empty')
  case "$type" in
    "")
      api_key=$(echo "$resp" | jq -r .api_key)
      mkdir -p "$(dirname "$CRED_PATH")"
      printf 'api_key=%s\n' "$api_key" > "$CRED_PATH"
      chmod 600 "$CRED_PATH"
      echo "Authorized. Key saved to $CRED_PATH"
      exit 0
      ;;
    authorization_pending|slow_down)
      continue
      ;;
    access_denied)
      echo "Request denied." >&2
      exit 1
      ;;
    expired_token)
      echo "Session expired before approval. Re-run to try again." >&2
      exit 1
      ;;
    *)
      echo "Unexpected error: $resp" >&2
      exit 1
      ;;
  esac
done

Security notes

  • device_code is a bearer credential — treat it like a session token. Do not log it, include it in error reports, or write it to world-readable files.
  • The minted API key has full access to all of the user's enabled upstreams. Its trust level is identical to a key minted manually from the dashboard.
  • Users can revoke any key minted via this flow from the Keys page at ada.ai/keys.
  • The browser approval step is what binds the request to a specific authenticated user. Without it, the CLI cannot mint a key — the backend will not issue one on device_code alone.

On this page