Authentication
Overview
Ravi uses OAuth device-code flow for authentication. This works in any environment — terminal, headless VM, or CI — without needing a callback URL.
:::note
No static API keys. Unlike most developer APIs, Ravi issues short-lived OAuth Bearer tokens (1-hour TTL), not static API keys. For automated environments — CI, Docker, cloud sandboxes — inject the access token directly via RAVI_ACCESS_TOKEN and skip the login flow entirely. Jump to Calling Ravi from code if that’s your setup.
:::
Choose your setup
Pick the path that matches how you’re running agents:
| Setup | What to read |
|---|---|
| Single agent, interactive terminal | Follow Login flow below — runs in 30 seconds |
| Multiple agents in parallel | Read Per-project Identity — one Identity per worker process, zero race conditions |
| Headless / CI / Docker | The device-code URL can be opened on any browser. See Headless auth note below |
If you’re running more than one agent at once, skip to Per-project Identity before you do anything else — ravi identity use writes shared global state and will cause subtle bugs in parallel processes.
Login flow
ravi auth login
- The CLI requests a device code from the Ravi API
- A verification URL is displayed — open it in any browser
- Complete the OAuth flow in the browser
- The CLI polls for the token and stores it locally
On headless machines, copy-paste the verification URL to a browser on any device.
Encryption setup
On first login, you’ll be prompted to create a 6-digit PIN:
- Choose a PIN — enter and confirm a 6-digit numeric PIN
- Key derivation — the PIN + a server-stored salt produce encryption keys via Argon2id
- Public key upload — the derived public key is sent to the server
- Recovery key — saved to
~/.ravi/recovery-key.txt(back this up)
On subsequent logins, you enter your existing PIN (3 attempts) to unlock the stored keypair.
Token management
- Access tokens expire after 1 hour
- Automatic refresh — the CLI refreshes tokens transparently before expiry
- No re-login needed — as long as the refresh token is valid, you stay authenticated
# Check current auth status
ravi auth status --json
{
"authenticated": true,
"email": "you@example.com",
"has_encryption": true
}
Stored credentials
Authentication state is stored in ~/.ravi/ with 0600 permissions:
| File | Contents |
|---|---|
auth.json | Access token, refresh token, encryption keypair |
config.json | Active Identity (UUID and name) |
recovery-key.txt | Recovery key for encryption (first login only) |
Per-project Identity
Place a .ravi/config.json in any project directory to override the global Identity for that project:
{
"identity_uuid": "uuid-here",
"identity_name": "project-agent"
}
The CLI checks for a local config first, then falls back to ~/.ravi/config.json.
Parallel agents: give each worker process its own project directory with its own .ravi/config.json. Never call ravi identity use from concurrent processes — it mutates the shared ~/.ravi/config.json and will cause identity mixups.
# Parallel agent setup — one directory per worker
mkdir -p workers/agent-{1,2,3}
for i in 1 2 3; do
UUID=$(ravi identity create --name "worker-$i" --json | jq -r '.uuid')
echo "{\"identity_uuid\":\"$UUID\",\"identity_name\":\"worker-$i\"}" \
> workers/agent-$i/.ravi/config.json
done
# Each worker process runs from its own directory — no shared state
Dev/test hygiene: prefix test identities so you can clean them up easily:
ravi identity create --name "dev-test-$(date +%s)"
# List and delete test identities when done:
ravi identity list --json | jq '.[] | select(.name | startswith("dev-test-"))'
Error handling
The happy path works automatically. When it doesn’t, here’s what each failure looks like and how to recover.
Auth status JSON
ravi auth status --json returns different shapes depending on state:
// Authenticated
{ "authenticated": true, "email": "you@example.com", "has_encryption": true }
// Token expired (auto-refresh failed)
{ "authenticated": false, "error": "token_expired", "message": "Access token expired and refresh failed." }
// Not logged in
{ "authenticated": false, "error": "not_authenticated", "message": "No credentials found. Run ravi auth login." }
Use authenticated as the boolean gate in automation scripts — do not parse the message string.
Error reference
| Error | Cause | Recovery |
|---|---|---|
device_code_expired | Device code was not authorized within 15 minutes | Re-run ravi auth login and complete the OAuth flow promptly |
pin_locked | PIN entered incorrectly 3 times | Run ravi auth recover and enter your recovery key from ~/.ravi/recovery-key.txt |
token_expired | Refresh token invalidated (e.g. password change, logout from another device) | Re-run ravi auth login |
encryption_missing | auth.json exists but PIN was never set | Run ravi auth setup-encryption to initialize the vault |
identity_not_found | Config references a deleted Identity UUID | Run ravi identity list and update .ravi/config.json with a valid UUID |
Token refresh in long-running processes
Access tokens expire after 1 hour. The CLI handles refresh transparently for short-lived commands. For agents or daemons that run for hours or days, add a proactive check before any identity-sensitive operation:
# Bash — guard block before any ravi command
ravi_auth_guard() {
local status
status=$(ravi auth status --json 2>/dev/null)
if ! echo "$status" | jq -e '.authenticated' > /dev/null 2>&1; then
echo "Ravi auth failed — re-login required" >&2
exit 1
fi
}
import subprocess, json, sys
def check_ravi_auth():
result = subprocess.run(
["ravi", "auth", "status", "--json"],
capture_output=True, text=True
)
try:
status = json.loads(result.stdout)
except json.JSONDecodeError:
sys.exit("ravi auth status returned unexpected output")
if not status.get("authenticated"):
sys.exit(f"Ravi auth error: {status.get('error')} — re-login required")
For long-running containers, pass tokens at launch time via environment variables rather than relying on file-based credentials that may expire mid-run. See Harness Integration for the full pattern.
Calling Ravi from code
When Ravi is embedded inside another runtime — a Rust host process, a cloud sandbox, a container — you call the HTTP API directly without the CLI. The auth model is straightforward Bearer token auth.
Token format
All Ravi API requests use:
Authorization: Bearer <access_token>
The access token is the access_token field in ~/.ravi/auth.json. It is a short-lived JWT (1-hour TTL). Read it at process startup or inject it via environment variable.
import json, pathlib, os
def get_ravi_token() -> str:
# Prefer env var injection (CI/Docker/containers)
token = os.environ.get("RAVI_ACCESS_TOKEN")
if token:
return token
# Fall back to reading auth file (local/dev)
auth_path = pathlib.Path.home() / ".ravi" / "auth.json"
return json.loads(auth_path.read_text())["access_token"]
import fs from "fs";
import os from "os";
import path from "path";
function getRaviToken(): string {
// Prefer env var (CI/Docker/containers)
if (process.env.RAVI_ACCESS_TOKEN) return process.env.RAVI_ACCESS_TOKEN;
// Fall back to auth file (local/dev)
const authPath = path.join(os.homedir(), ".ravi", "auth.json");
return JSON.parse(fs.readFileSync(authPath, "utf-8")).access_token;
}
Base URL and headers
Base URL: https://api.ravi.app
Content-Type: application/json
Authorization: Bearer <access_token>
X-Ravi-Identity: <identity_uuid> ← required for identity-scoped endpoints
The X-Ravi-Identity header scopes each request to a specific Identity — the equivalent of ravi identity use but per-request and parallel-safe. Pass the Identity UUID (from ravi identity list --json).
Token refresh (programmatic)
Access tokens expire after 1 hour. To refresh without the CLI:
POST https://api.ravi.app/api/auth/token/refresh/
Content-Type: application/json
{"refresh": "<refresh_token>"}
The refresh_token is the refresh_token field in ~/.ravi/auth.json. The response contains a new access_token. Store it and continue.
import httpx
def refresh_ravi_token(refresh_token: str) -> str:
resp = httpx.post(
"https://api.ravi.app/api/auth/token/refresh/",
json={"refresh": refresh_token},
)
resp.raise_for_status()
return resp.json()["access_token"]
API key path
Ravi uses OAuth tokens, not static API keys. For CI and containerized environments, inject the access token at launch time via RAVI_ACCESS_TOKEN — this avoids file-based auth entirely. See CLI Environment Variables for the full list.
For full working examples — REST calls, SSE streaming, parallel workers — see TypeScript REST API and Production Patterns.
Logout
ravi auth logout
This clears stored tokens. Your encryption keys and Identity configuration remain intact.
OpenClaw authentication
The OpenClaw plugin uses the same auth flow:
openclaw ravi login
This runs the same device-code flow, sets up encryption, selects an Identity, and auto-configures the OpenClaw plugin. Credentials are stored in the same ~/.ravi/auth.json file, so the CLI and OpenClaw plugin share authentication state.
Next steps
- Identities — create and manage multiple Identities
- E2E Encryption — how the zero-knowledge vault works