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/tokenon theintervalcadence returned by/auth/device/code(default: 5 s). - A
429withauthorization_pendingis normal — keep polling. - The token endpoint is read-once: the first
200response 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 inverification_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
type | Meaning and recommended CLI behavior |
|---|---|
authorization_pending | User hasn't acted yet. Keep polling at interval. |
slow_down | You're polling faster than allowed. Wait the interval and retry. |
access_denied | User clicked Deny. Abort with a clear message; do not retry. |
expired_token | The 10-minute window closed, or the key was already consumed. Exit and prompt the user to re-run. |
invalid_request | Malformed body or unknown device_code. Log the response; do not retry. |
rate_limited | Too 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
doneSecurity notes
device_codeis 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_codealone.
Organizations
Organizations let a team share one pool of upstreams under a single account. Each proxy key is scoped to exactly one owner — you, or one organization.
Usage & Analytics
Every request records tokens, latency, time-to-first-token, provider, and status. View it in the dashboard or query GET /me/usage.