Build a Ravi Integration
This guide is for developers building a new Ravi integration — a Leon skill, a custom agent harness, a library wrapper, or any tool that needs to use Ravi programmatically. It uses bash, Python, and TypeScript examples side by side so you can pick the one closest to your stack.
If you want a named guide for a specific tool (Claude Code, vibe-kanban, Dorothy, etc.), see Integrations.
The four steps
Every Ravi integration follows the same shape:
- Provision an Identity — create a name, get an email and phone number
- Use the Identity in a workflow — receive OTP codes, verify email addresses
- Retrieve credentials programmatically — read secrets and passwords from the vault
- Handle failures — token expiry, polling timeouts, parallel isolation
Step 1: Provision an Identity
An Identity is the atomic unit in Ravi. One Identity = one email address + one phone number + one credential vault. You provision one per agent, per task, or per environment — whatever unit of isolation your system needs.
Via CLI
ravi identity create --name "my-agent" --json
Output:
{
"uuid": "abc-123",
"name": "my-agent",
"inbox": "my-agent@in.ravi.app",
"phone": "+15551234567",
"created_dt": "2026-02-25T10:30:00Z"
}
Store the uuid — you need it for the X-Ravi-Identity header if you use the REST API directly.
Via Python (subprocess)
import subprocess, json
def create_identity(name: str) -> dict:
result = subprocess.run(
["ravi", "identity", "create", "--name", name, "--json"],
capture_output=True, text=True, check=True
)
return json.loads(result.stdout)
identity = create_identity("my-agent")
print(identity["inbox"]) # my-agent@in.ravi.app
print(identity["phone"]) # +15551234567
Via TypeScript (subprocess)
import { execSync } from "child_process";
function createIdentity(name: string): Record<string, string> {
const output = execSync(
`ravi identity create --name "${name}" --json`,
{ encoding: "utf8" }
);
return JSON.parse(output);
}
const identity = createIdentity("my-agent");
console.log(identity.inbox); // my-agent@in.ravi.app
Via REST API (any language)
POST https://ravi.app/api/identities/
Authorization: Bearer <access-token>
Content-Type: application/json
{"name": "my-agent"}
See API Endpoints for the full response shape.
Step 2: Use the Identity in a workflow
The most common workflow is: use your agent’s email to sign up for a service, then receive and extract a verification code.
Parallel-safe Identity scoping
Before running any commands, decide how to scope the Identity:
| Scenario | Recommended approach |
|---|---|
| Single agent in a shell | ravi identity use <name> (fine for one process) |
| Multiple agents running concurrently | RAVI_CONFIG_DIR=/tmp/agent-N ravi ... per process |
| Worktree-based orchestrators (Aizen, amux, Claude Squad) | Place .ravi/config.json in each worktree root |
| Containers / CI | RAVI_ACCESS_TOKEN=<token> + RAVI_CONFIG_DIR=<dir> |
Never call
ravi identity usefrom concurrent processes. It writes a shared config file. Two simultaneous calls corrupt each other’s identity context. UseRAVI_CONFIG_DIRinstead — see Parallel Environments.
Poll for an SMS OTP
When an external service sends a verification code by SMS, poll the inbox rather than using a bare sleep:
# Bash — poll with timeout
for i in $(seq 1 15); do
CODE=$(ravi inbox sms --unread --json 2>/dev/null | jq -r '.[0].preview // empty')
if [ -n "$CODE" ]; then
echo "OTP: $CODE"
break
fi
sleep 2
done
if [ -z "$CODE" ]; then echo "Error: no SMS received within 30s" >&2; exit 1; fi
# Python — poll with timeout
import subprocess, json, time
def poll_sms_otp(config_dir: str, max_attempts: int = 15, delay: float = 2.0) -> str:
env = {"RAVI_CONFIG_DIR": config_dir}
for _ in range(max_attempts):
result = subprocess.run(
["ravi", "inbox", "sms", "--unread", "--json"],
capture_output=True, text=True, env={**__import__("os").environ, **env}
)
messages = json.loads(result.stdout or "[]")
if messages:
return messages[0]["preview"]
time.sleep(delay)
raise TimeoutError("No SMS received within timeout")
Poll for an email verification link
// TypeScript — poll email inbox
import { execSync } from "child_process";
function pollEmailInbox(
configDir: string,
maxAttempts = 15,
delayMs = 2000
): Promise<string> {
return new Promise((resolve, reject) => {
let attempts = 0;
const check = () => {
attempts++;
const output = execSync(
"ravi inbox email --unread --json",
{ encoding: "utf8", env: { ...process.env, RAVI_CONFIG_DIR: configDir } }
);
const threads = JSON.parse(output || "[]");
if (threads.length > 0) {
resolve(threads[0].preview);
} else if (attempts >= maxAttempts) {
reject(new Error("No email received within timeout"));
} else {
setTimeout(check, delayMs);
}
};
check();
});
}
Step 3: Retrieve credentials programmatically
After your agent signs up for a service, store and retrieve credentials from the vault.
Store a credential
ravi passwords create \
--domain example.com \
--username myuser \
--password mysecretpassword \
--json
Retrieve a stored credential
# List to find the UUID
ravi passwords list --json | jq '.[] | select(.domain == "example.com") | .uuid'
# Get the full entry (including decrypted password)
ravi passwords get <uuid> --json
Store and retrieve a secret (API key, token)
# Store
ravi vault set MY_API_KEY sk-abc123
# Retrieve
ravi vault get MY_API_KEY --json
Via Python
import subprocess, json, os
def get_credential(domain: str, config_dir: str) -> dict:
env = {**os.environ, "RAVI_CONFIG_DIR": config_dir}
entries = json.loads(subprocess.check_output(
["ravi", "passwords", "list", "--json"], env=env
))
for entry in entries:
if entry["domain"] == domain:
return json.loads(subprocess.check_output(
["ravi", "passwords", "get", entry["uuid"], "--json"], env=env
))
raise KeyError(f"No credential found for {domain}")
Step 4: Handle common failures
Every Ravi integration needs to handle these failure modes:
| Failure | Symptom | Recovery |
|---|---|---|
| Token expired | CLI exits with token expired / API returns 401 | CLI auto-refreshes; for headless, inject fresh RAVI_ACCESS_TOKEN — see Auth |
| OTP poll timeout | Loop ends with no message | Increase attempts; check identity is active (ravi identity list) |
| Parallel identity corruption | Wrong inbox data in concurrent agent | Switch to RAVI_CONFIG_DIR per process |
| Identity not found | identity not found error | Check ravi identity list --json; verify .ravi/config.json in CWD |
| E2E-encrypted field in API response | "password": "e2e::..." instead of plaintext | Decryption is client-side — use CLI commands, not raw API, for encrypted fields |
ravi identity use silently persists wrong identity | Subsequent commands use unexpected identity | Avoid ravi identity use in scripts; prefer RAVI_CONFIG_DIR |
Subprocess error handling pattern
import subprocess, json
def ravi(args: list[str], config_dir: str | None = None) -> dict:
env = {**__import__("os").environ}
if config_dir:
env["RAVI_CONFIG_DIR"] = config_dir
try:
result = subprocess.run(
["ravi"] + args,
capture_output=True, text=True, check=True, env=env
)
return json.loads(result.stdout) if result.stdout.strip() else {}
except subprocess.CalledProcessError as e:
raise RuntimeError(f"ravi {' '.join(args)} failed: {e.stderr.strip()}") from e
Putting it together
A minimal end-to-end integration in Python:
import subprocess, json, os, time, pathlib, tempfile
def ravi(args, config_dir=None):
env = {**os.environ}
if config_dir:
env["RAVI_CONFIG_DIR"] = config_dir
return json.loads(subprocess.check_output(["ravi"] + args, env=env, text=True))
def poll(args, config_dir, key, max_wait=30, interval=2):
for _ in range(max_wait // interval):
items = ravi(args + ["--json"], config_dir)
if items:
return items[0][key]
time.sleep(interval)
raise TimeoutError(f"Timed out waiting for {key}")
# 1. Provision
with tempfile.TemporaryDirectory() as config_dir:
identity = ravi(["identity", "create", "--name", "task-agent", "--json"])
pathlib.Path(config_dir, "config.json").write_text(
json.dumps({"identity_uuid": identity["uuid"]})
)
# 2. Use in workflow (sign up somewhere, trigger an OTP)
email = identity["inbox"]
print(f"Agent email: {email}")
# ... trigger the signup flow ...
# 3. Poll for verification
otp = poll(["inbox", "sms", "--unread"], config_dir, "preview")
print(f"OTP received: {otp}")
# 4. Store resulting credential
ravi(["passwords", "create", "--domain", "example.com",
"--username", "myuser", "--password", "mypassword"], config_dir)
print("Done. Identity and credentials stored.")
# config_dir cleaned up — identity persists in Ravi until explicitly deleted
Next steps
- Production Patterns — production-ready Python and TypeScript patterns
- Parallel Environments — worktree and RAVI_CONFIG_DIR isolation
- TypeScript REST API — full TypeScript client without CLI subprocess
- API Endpoints — complete REST API reference
- Troubleshooting — common errors and recovery