Sign up for a service in 5 minutes
:::note[Page guide] Who this is for — developers provisioning a Ravi Identity for the first time and wiring it into a real agent workflow.
If you came from…
- Introduction or Installation → you’re in the right place, continue below.
- A search engine, directly → install the CLI first: Installation, then return here.
- You already have an Identity and want parallel-agent patterns → jump to Using Ravi with your orchestrator below.
Read this next — Multi-Agent Setup for parallel isolation, Production Patterns for the deployment checklist, or Authentication if you need headless / CI auth. :::
By the end of this guide your agent will have a real email address, a real phone number, and a stored password — all provisioned in one terminal session.
Prerequisites
- Ravi CLI installed
- Authenticated via
ravi auth login— opens a browser on the same machine. Running headless, in CI, or on an edge device (Raspberry Pi, RISC-V board, Docker)? See Authentication → Choose your setup before continuing — you’ll inject a pre-extracted token instead of running the browser flow.
Create an Identity
ravi identity create --name "my-agent" --json
This provisions:
- A unique email address (e.g.
my-agent@in.ravi.app) - A phone number for SMS
- An empty credential vault
Set the active Identity
ravi identity use my-agent
All subsequent commands in this shell session operate against this Identity.
Running multiple agents in parallel?
ravi identity usewrites to a shared config file — it is not safe to call from concurrent processes. For parallel agents, use per-project config files instead. Place a.ravi/config.jsonin each agent’s working directory, or see the Multi-Agent Setup guide.
Get your Identity details
# Get your agent's email address
ravi get email --json
# Get your agent's phone number
ravi get phone --json
Sign up for a service
Here’s a typical agent workflow — signing up for a service, receiving a verification code, and storing credentials:
# 1. Get your email and phone
EMAIL=$(ravi get email --json | jq -r '.email')
PHONE=$(ravi get phone --json | jq -r '.phone_number')
# 2. Generate and store credentials
CREDS=$(ravi passwords create example.com --username "$EMAIL" --json)
PASSWORD=$(echo "$CREDS" | jq -r '.password')
# 3. Use $EMAIL, $PHONE, and $PASSWORD to fill the signup form
# 4. Poll for SMS OTP (delivery takes 2–10 seconds; retry up to 20s)
OTP=""
for i in $(seq 1 10); do
OTP=$(ravi inbox sms --unread --json | jq -r '.[0].preview // empty')
[ -n "$OTP" ] && break
sleep 2
done
echo "OTP: $OTP"
# 5. Or poll for email verification link
THREAD_ID=""
for i in $(seq 1 10); do
THREAD_ID=$(ravi inbox email --unread --json | jq -r '.[0].thread_id // empty')
[ -n "$THREAD_ID" ] && break
sleep 2
done
ravi inbox email "$THREAD_ID" --json | jq -r '.messages[0].text_content'
Check your inbox
# All messages (SMS + email combined)
ravi inbox list --json
# Unread SMS only
ravi inbox sms --unread --json
# Unread email only
ravi inbox email --unread --json
Send an email
ravi email compose \
--to "recipient@example.com" \
--subject "Hello from my agent" \
--body "<p>This email was sent by an AI agent using Ravi.</p>" \
--json
Store a secret
# Store an API key
ravi secrets set OPENAI_API_KEY "sk-abc123..." --json
# Retrieve it later
ravi secrets get OPENAI_API_KEY --json | jq -r '.value'
Key conventions
- Always use
--json— all commands support it. JSON output is stable and designed for programmatic parsing. - Pipe through
jq— extract specific fields from JSON responses. - Poll with retry loops — SMS and email delivery takes 2–10 seconds. Use a short loop (
sleep 2× up to 10 iterations) rather than a fixedsleep 5— delivery is often faster and a fixed delay fails silently when it’s slow.
Using Ravi with your orchestrator
If you’re running agents through a parallel orchestrator (Claude Squad, Mux, amux, vibe-kanban, cmux, dmux, or similar), the correct wiring is a per-workspace .ravi/config.json file — not ravi identity use. This scopes all Ravi CLI commands to the specified identity without touching the global shared config.
Claude Squad
# Create one identity per Claude Squad session slot
ravi identity create --name "squad-worker-1" --json
ravi identity create --name "squad-worker-2" --json
# Write a config file in each worktree root before launching
echo '{"identity":"squad-worker-1"}' > ~/worktrees/feature-a/.ravi/config.json
echo '{"identity":"squad-worker-2"}' > ~/worktrees/feature-b/.ravi/config.json
# Launch as normal — Ravi picks up the local config automatically
cs --worktree ~/worktrees/feature-a -p "claude"
Mux / amux
# Each Mux workspace gets its own config directory
mkdir -p ~/workspaces/ws-1/.ravi ~/workspaces/ws-2/.ravi
ravi identity create --name "mux-ws-1" --json
ravi identity create --name "mux-ws-2" --json
echo '{"identity":"mux-ws-1"}' > ~/workspaces/ws-1/.ravi/config.json
echo '{"identity":"mux-ws-2"}' > ~/workspaces/ws-2/.ravi/config.json
# Agents running from ~/workspaces/ws-1 now resolve to mux-ws-1 automatically
vibe-kanban / any worktree-based tool
# Generic pattern: one identity per worktree, config file at worktree root
for branch in feature-a feature-b feature-c; do
git worktree add ~/worktrees/$branch $branch
mkdir -p ~/worktrees/$branch/.ravi
ravi identity create --name "agent-$branch" --json
echo "{\"identity\":\"agent-$branch\"}" > ~/worktrees/$branch/.ravi/config.json
done
# Each agent that runs under ~/worktrees/<branch> automatically uses the right identity
Superset / Jean / any setup-script orchestrator
Some orchestrators — Superset, Jean, and others — run a shell script when each worktree is created and another when it’s torn down. That lifecycle hook is the right place to provision and clean up a Ravi identity.
.superset/setup.sh (or .jean/setup.sh)
#!/bin/bash
set -e
# SUPERSET_WORKSPACE_NAME / JEAN_WORKSPACE_NAME is injected by the orchestrator.
# Fall back to the current directory name if running manually.
IDENTITY_NAME="${SUPERSET_WORKSPACE_NAME:-${JEAN_WORKSPACE_NAME:-$(basename "$PWD")}}"
# Idempotent create — silent no-op if the identity already exists
ravi identity create --name "$IDENTITY_NAME" --json 2>/dev/null || true
# Write a scoped config so every ravi command in this worktree uses the right identity
mkdir -p .ravi
echo "{\"identity\":\"$IDENTITY_NAME\"}" > .ravi/config.json
# (Optional) expose the agent's contact details to the running agent as env vars
AGENT_EMAIL=$(ravi get email --json | jq -r '.email')
AGENT_PHONE=$(ravi get phone --json | jq -r '.phone_number')
echo "AGENT_EMAIL=$AGENT_EMAIL" >> .env
echo "AGENT_PHONE=$AGENT_PHONE" >> .env
echo "Ravi identity '$IDENTITY_NAME' ready: $AGENT_EMAIL / $AGENT_PHONE"
.superset/teardown.sh (or .jean/teardown.sh)
#!/bin/bash
IDENTITY_NAME="${SUPERSET_WORKSPACE_NAME:-${JEAN_WORKSPACE_NAME:-$(basename "$PWD")}}"
# Clean up the ephemeral identity after the session closes
ravi identity delete --name "$IDENTITY_NAME" --json 2>/dev/null || true
echo "Identity '$IDENTITY_NAME' removed."
Why
|| trueonidentity create? Worktrees can be rebuilt without a full teardown — e.g., after a crash or during CI retry. Without the guard, the setup script fails on the second run. Silent idempotency is the correct default in any scripted lifecycle hook.
See the Superset integration guide and Jean integration guide for full configuration examples.
Using RAVI_CONFIG_DIR for process-level isolation
When your orchestrator launches agents as separate processes (not just in separate directories), RAVI_CONFIG_DIR gives you the same isolation without needing a .ravi/config.json on disk:
# Orchestrator launch snippet — each worker gets its own config dir
RAVI_CONFIG_DIR=/tmp/ravi-agent-1 ravi get email --json
RAVI_CONFIG_DIR=/tmp/ravi-agent-2 ravi get email --json
See the Multi-Agent Setup guide for fleet initialization patterns at larger scale.
Python & TypeScript patterns
The bash examples above use the Ravi CLI. If you’re writing a Python script or a TypeScript service, here are the equivalent patterns using subprocess and the REST API.
Get email / phone in Python
import subprocess, json, os
def ravi(args: list[str], config_dir: str | None = None) -> dict:
"""Run a ravi CLI command, return parsed JSON."""
env = {**os.environ}
if config_dir:
env["RAVI_CONFIG_DIR"] = config_dir
result = subprocess.run(
["ravi", *args, "--json"],
capture_output=True, text=True, env=env, check=True
)
return json.loads(result.stdout)
email = ravi(["get", "email"])["email"]
phone = ravi(["get", "phone"])["phone_number"]
print(f"Agent email: {email}, phone: {phone}")
Poll for SMS OTP in Python
import time
def poll_sms_otp(timeout_s: int = 30, interval_s: int = 2) -> str | None:
"""Return the first unread SMS preview, or None on timeout."""
for _ in range(timeout_s // interval_s):
messages = ravi(["inbox", "sms", "--unread"])
if messages:
return messages[0]["preview"]
time.sleep(interval_s)
return None
otp = poll_sms_otp()
if otp:
print(f"OTP received: {otp}")
else:
raise TimeoutError("No SMS OTP arrived within 30 seconds")
Poll for email verification in TypeScript
import { execSync } from "child_process";
function ravi(args: string[], configDir?: string): unknown {
const env = configDir ? { ...process.env, RAVI_CONFIG_DIR: configDir } : process.env;
const out = execSync(`ravi ${args.join(" ")} --json`, { env });
return JSON.parse(out.toString());
}
async function pollEmailInbox(
timeoutMs = 30_000,
intervalMs = 2_000
): Promise<{ thread_id: string; subject: string } | null> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const threads = ravi(["inbox", "email", "--unread"]) as Array<{
thread_id: string;
subject: string;
}>;
if (threads.length > 0) return threads[0];
await new Promise((r) => setTimeout(r, intervalMs));
}
return null;
}
const thread = await pollEmailInbox();
if (thread) {
const msgs = ravi(["inbox", "email", thread.thread_id]) as { messages: Array<{ text_content: string }> };
console.log(msgs.messages[0].text_content);
}
Parallel agents? Pass a per-agent
RAVI_CONFIG_DIRinto theravi()helper above (or viaRAVI_CONFIG_DIRenv var). Each agent process gets an isolated config directory with no shared state. See Production Patterns for full examples.
Close the loop — browser automation + OTP
When an agent fills a signup or login form through a browser tool (Playwright, cmux’s browser pane, Ariana, Aperant, or any headless browser), the flow ends with an SMS or email OTP that needs to land somewhere real. Ravi closes that loop.
The full chain — form submission → OTP intercept → credential store — in bash:
# 1. Get this agent's provisioned email and phone
EMAIL=$(ravi get email --json | jq -r '.email')
PHONE=$(ravi get phone --json | jq -r '.phone_number')
# 2. Fill the signup form with the agent's Ravi contact details
# (replace with your browser automation tool's API)
browser fill "#email-field" "$EMAIL"
browser fill "#phone-field" "$PHONE"
browser click "#submit"
# 3. Poll for SMS OTP (30 attempts × 2 s = 60 s timeout)
OTP=""
for i in $(seq 1 30); do
OTP=$(ravi inbox sms --unread --json | jq -r '.[0].body // empty' | grep -oE '[0-9]{4,8}' | head -1)
[ -n "$OTP" ] && break
sleep 2
done
[ -z "$OTP" ] && { echo "OTP timeout after 60 s" >&2; exit 1; }
# 4. Complete the flow
browser fill "#otp-field" "$OTP"
browser click "#verify"
# 5. Store the resulting credential so it survives beyond this session
ravi passwords create example.com --username "$EMAIL" --json
The same pattern works for email OTP — replace step 3’s ravi inbox sms with ravi inbox email --unread --json | jq -r '.[0].preview // empty'.
Python equivalent (using Playwright + subprocess):
import subprocess, time, re
def ravi(cmd: str) -> str:
return subprocess.check_output(f"ravi {cmd}", shell=True).decode().strip()
email = ravi("get email --json | jq -r .email")
phone = ravi("get phone --json | jq -r .phone_number")
# Fill form with your Playwright page object
page.fill("#email-field", email)
page.fill("#phone-field", phone)
page.click("#submit")
# Poll for OTP
otp = None
for _ in range(30):
raw = ravi("inbox sms --unread --json")
msgs = __import__("json").loads(raw)
if msgs:
match = re.search(r"\b[0-9]{4,8}\b", msgs[0].get("body", ""))
if match:
otp = match.group()
break
time.sleep(2)
if not otp:
raise TimeoutError("OTP not received within 60 s")
page.fill("#otp-field", otp)
page.click("#verify")
# Persist the credential
ravi(f"passwords create example.com --username {email} --json")
Tools with native browser + agent primitives (cmux, Aperant, Aizen, agent-deck) can wire their own browser APIs in place of the browser fill / Playwright calls above — the Ravi OTP polling side is identical regardless of the browser tool.
Before you ship
Five things to harden before moving this beyond your local terminal.
1. Handle identity already-exists errors
ravi identity create returns a non-zero exit code if an identity with that name already exists. In orchestrators that run on every task, guard the create:
# Bash — idempotent identity creation
ravi identity create --name "my-agent" --json 2>/dev/null \
|| ravi identity list --json | jq '.[] | select(.name == "my-agent")'
import subprocess, json
def get_or_create_identity(name: str) -> dict:
try:
result = subprocess.run(
["ravi", "identity", "create", "--name", name, "--json"],
capture_output=True, text=True, check=True
)
return json.loads(result.stdout)
except subprocess.CalledProcessError:
# Identity already exists — fetch it from the list
result = subprocess.run(
["ravi", "identity", "list", "--json"],
capture_output=True, text=True, check=True
)
identities = json.loads(result.stdout)
return next(i for i in identities if i["name"] == name)
2. Never swallow poll failures silently
The bash poll loop above exits with an empty $OTP if delivery never arrives — and your script keeps running. Make timeouts loud:
# ✅ Fail loudly on poll timeout
OTP=""
for i in $(seq 1 10); do
OTP=$(ravi inbox sms --unread --json | jq -r '.[0].preview // empty')
[ -n "$OTP" ] && break
sleep 2
done
if [ -z "$OTP" ]; then
echo "ERROR: SMS OTP did not arrive within 20 seconds" >&2
exit 1
fi
For TypeScript, wrap execSync so CLI errors don’t disappear into uncaught exceptions:
import { execSync, ExecSyncOptionsWithStringEncoding } from "child_process";
function ravi(args: string[], configDir?: string): unknown {
const env = configDir ? { ...process.env, RAVI_CONFIG_DIR: configDir } : process.env;
try {
const out = execSync(`ravi ${args.join(" ")} --json`, {
env,
stdio: ["pipe", "pipe", "pipe"],
} as ExecSyncOptionsWithStringEncoding);
return JSON.parse(out.toString());
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(`ravi ${args[0]} failed: ${msg}`);
}
}
3. Ravi secrets vault vs. OS keychain
If you already use the OS keychain (macOS Keychain, Windows Credential Manager), you don’t need to migrate. Use Ravi’s secrets vault when:
- credentials need to be readable by a subprocess or remote agent (no keychain daemon available)
- credentials must be shared across machines or containers without manual copy
- you want a single audit trail of all credentials an agent identity holds
For purely local, single-user tools on a managed machine, the OS keychain is fine. Ravi’s vault is the right call for headless, containerized, or multi-machine deployments.
4. Know the rate limits
| Operation | Limit |
|---|---|
ravi email compose | 60/hour, 500/day |
ravi identity create | no documented hard limit; expect throttling above ~10/min |
ravi inbox sms --unread / ravi inbox email --unread | poll freely; no documented inbox-read limit |
If you’re provisioning identities in bulk (fleet initialization), add a short sleep 1 between creates to stay within polite thresholds.
5. Link to the full production checklist
For a complete set of production hardening items — token injection, parallel isolation, token refresh for long-running processes — see the Production Patterns checklist.
Verify everything works
Before moving to production code, do a quick end-to-end check. These three commands confirm your Identity is live, email is deliverable, and the vault is writable:
1. Send yourself a test email (agent → agent):
MY_EMAIL=$(ravi get email --json | jq -r '.email')
ravi email compose \
--to "$MY_EMAIL" \
--subject "Ravi identity check" \
--body "<p>If you can read this, your identity is working.</p>" \
--json
Expected output:
{
"id": 4821,
"subject": "Ravi identity check",
"to": "my-agent@in.ravi.app",
"status": "sent"
}
2. Poll your inbox to confirm delivery:
sleep 3
ravi inbox email --unread --json | jq '.[0] | {subject, from_email, preview}'
Expected output:
{
"subject": "Ravi identity check",
"from_email": "my-agent@in.ravi.app",
"preview": "If you can read this, your identity is working."
}
If the inbox is empty, wait a few more seconds and retry — delivery takes 2–10 seconds. If it stays empty, check ravi auth status --json to confirm you’re authenticated.
3. Store and retrieve a test secret:
ravi secrets set TEST_KEY "hello-ravi" --json
ravi secrets get TEST_KEY --json | jq -r '.value'
# → hello-ravi
When all three work, your Identity is fully operational. Clean up the test secret:
ravi secrets delete TEST_KEY
Next steps
- Authentication — understand login flows and token management
- Identities — manage multiple Identities for different agents
- Multi-Agent Setup — safely run parallel agents with isolated Identities
- Production Patterns — Python and TypeScript patterns for production systems
- CLI Reference — full command reference