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:

  1. Provision an Identity — create a name, get an email and phone number
  2. Use the Identity in a workflow — receive OTP codes, verify email addresses
  3. Retrieve credentials programmatically — read secrets and passwords from the vault
  4. 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:

ScenarioRecommended approach
Single agent in a shellravi identity use <name> (fine for one process)
Multiple agents running concurrentlyRAVI_CONFIG_DIR=/tmp/agent-N ravi ... per process
Worktree-based orchestrators (Aizen, amux, Claude Squad)Place .ravi/config.json in each worktree root
Containers / CIRAVI_ACCESS_TOKEN=<token> + RAVI_CONFIG_DIR=<dir>

Never call ravi identity use from concurrent processes. It writes a shared config file. Two simultaneous calls corrupt each other’s identity context. Use RAVI_CONFIG_DIR instead — 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")
// 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:

FailureSymptomRecovery
Token expiredCLI exits with token expired / API returns 401CLI auto-refreshes; for headless, inject fresh RAVI_ACCESS_TOKEN — see Auth
OTP poll timeoutLoop ends with no messageIncrease attempts; check identity is active (ravi identity list)
Parallel identity corruptionWrong inbox data in concurrent agentSwitch to RAVI_CONFIG_DIR per process
Identity not foundidentity not found errorCheck ravi identity list --json; verify .ravi/config.json in CWD
E2E-encrypted field in API response"password": "e2e::..." instead of plaintextDecryption is client-side — use CLI commands, not raw API, for encrypted fields
ravi identity use silently persists wrong identitySubsequent commands use unexpected identityAvoid 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