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:

SetupWhat to read
Single agent, interactive terminalFollow Login flow below — runs in 30 seconds
Multiple agents in parallelRead Per-project Identity — one Identity per worker process, zero race conditions
Headless / CI / DockerThe 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
  1. The CLI requests a device code from the Ravi API
  2. A verification URL is displayed — open it in any browser
  3. Complete the OAuth flow in the browser
  4. 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:

  1. Choose a PIN — enter and confirm a 6-digit numeric PIN
  2. Key derivation — the PIN + a server-stored salt produce encryption keys via Argon2id
  3. Public key upload — the derived public key is sent to the server
  4. 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:

FileContents
auth.jsonAccess token, refresh token, encryption keypair
config.jsonActive Identity (UUID and name)
recovery-key.txtRecovery 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

ErrorCauseRecovery
device_code_expiredDevice code was not authorized within 15 minutesRe-run ravi auth login and complete the OAuth flow promptly
pin_lockedPIN entered incorrectly 3 timesRun ravi auth recover and enter your recovery key from ~/.ravi/recovery-key.txt
token_expiredRefresh token invalidated (e.g. password change, logout from another device)Re-run ravi auth login
encryption_missingauth.json exists but PIN was never setRun ravi auth setup-encryption to initialize the vault
identity_not_foundConfig references a deleted Identity UUIDRun 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