Metadata-Version: 2.4
Name: ravi-sdk
Version: 0.3.0
Summary: Ravi SDK — Python client for the Ravi identity API
Project-URL: Homepage, https://github.com/ravi-hq/ravi-python
Project-URL: Documentation, https://github.com/ravi-hq/ravi-python#readme
Project-URL: Issues, https://github.com/ravi-hq/ravi-python/issues
Project-URL: Repository, https://github.com/ravi-hq/ravi-python
Author: Ravi Technologies, Inc.
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: ai-agents,email,identity,ravi,sms
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.9.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
Requires-Dist: pytest>=8.3.0; extra == 'dev'
Requires-Dist: respx>=0.21.1; extra == 'dev'
Requires-Dist: ruff>=0.8.0; extra == 'dev'
Description-Content-Type: text/markdown

# ravi

The Python SDK for Ravi — the identity provider for AI agents.

```bash
pip install ravi-sdk
```

```python
import os
from ravi import Ravi

# Keys are ravi_id_... (scoped to one identity) or ravi_mgmt_... (account-wide).
# The key is an auth FENCE only — the caller chooses the identity, and every
# per-identity call carries ?identity=<uuid> taken from the identity object.
ravi = Ravi(api_key=os.environ["RAVI_API_KEY"])

# Create an identity. The email address is an identifier (local part) plus an
# optional domain; omit both to auto-generate. provision_phone adds a number.
identity = ravi.identities.create(email_identifier="support-agent", provision_phone=True)
print(identity.email.address, identity.phone.number)   # support-agent@... , +1...

# `email` and `phone` are channel objects, not strings. Comms, vault, and
# contacts are reached ONLY through the identity — never off the client.
identity.email.send(to="user@example.com", subject="Hello", body="Greetings from my AI agent")
msg = identity.email.wait_for(lambda m: "verify" in m.subject)   # poll the inbox
msg.reply(body="got it")                                         # messages are client-bound

identity.phone.send(to="+15551234567", body="START")
otp = identity.phone.wait_for(lambda m: "code" in m.body, timeout=120)

# Place an outbound voice call from the identity's number.
call = identity.phone.call(to="+15551234567")
print(call.uuid, call.status)
transcript = call.transcript()

# Credentials live in the identity's vault.
identity.vault.passwords.create(domain="acme.com", username="agent", password="...")
identity.vault.secrets.create(key="OPENAI_API_KEY", value="sk-...")

# Replay events — the deploy-safe companion to the wss://<host>/ws/events/ stream.
for event in identity.events.list(since=0, event_types=["call.ended", "email.message.received"]):
    print(event.seq, event.type, event.data)

# With an identity-scoped key, `ravi.me` resolves the single fenced identity.
me = ravi.me
```

The SDK talks to Ravi's hosted API. The API host is built into the client.

## Status

## Getting an API key

Mint a key one of three ways, then pass it as `Ravi(api_key=...)` (or set
`RAVI_API_KEY`):

- `ravi auth login` with the [Ravi CLI](https://github.com/ravi-hq/cli) (one-time
  human sign-in, then agents self-serve),
- `POST /api/auth/keys/management/` from an authenticated session, or
- the dashboard at `/dashboard/api-keys/`.

Keys are `ravi_mgmt_...` (account-wide) or `ravi_id_...` (scoped to one identity).

## Resources

The API key is an **auth fence only**. Account-level resources hang off the
client; everything per-identity is reached through an `Identity` and scoped
with `?identity=<uuid>`.

### Account-level (`ravi.*`)

| Accessor | Endpoints |
|----------|-----------|
| `ravi.identities` | identity bundles + `provision_phone` + `voice_agent` |
| `ravi.me` | the single identity an identity-key is fenced to |
| `ravi.events` | durable account-wide event replay (`list(since=...)`) |
| `ravi.domains` | verified domains (read-only) |
| `ravi.webhooks` | subscriptions + `deliveries` log |
| `ravi.api_keys` | `management` + `identity` keys |

### Per-identity (`identity.*`)

| Accessor | Surface |
|----------|---------|
| `identity.email` | `.address`, `inbox`/`threads`/`get`/`send`/`wait_for` (returns `EmailMessage`) |
| `identity.phone` | `.number`, `inbox`/`conversations`/`send`/`call`/`calls`/`wait_for` (or `None`) |
| `identity.contacts` | directory + `find` + `search` |
| `identity.vault.passwords` | website credentials + `generate` |
| `identity.vault.secrets` | key/value secrets |
| `identity.events` | this identity's event replay |

Messages are client-bound: `EmailMessage` has `.reply`/`.reply_all`/`.forward`/
`.mark_read` (and a `.body` property); `SmsMessage` has `.reply`/`.mark_read`;
`Call` has `.hangup`/`.transcript`.

## Verifying inbound webhooks

Ravi signs each webhook with `X-Ravi-Timestamp` + `X-Ravi-Signature: sha256=<hex>`
over `"{timestamp}.{raw_body}"`. Verify against the **raw** body:

```python
from ravi import verify_webhook_signature, WebhookSignatureError

try:
    verify_webhook_signature(
        raw_body,                       # bytes/str, exactly as received
        request.headers["X-Ravi-Timestamp"],
        request.headers["X-Ravi-Signature"],
        signing_secret,                 # from the webhook subscription
    )
except WebhookSignatureError as exc:
    ...  # reject: exc.code is "invalid_header" | "expired" | "signature_mismatch"
```

## License

[Apache 2.0](https://github.com/ravi-hq/ravi/blob/main/LICENSE).
