@ravi-hq/sdk
v0.3.0
Published
Ravi SDK — TypeScript client for the Ravi identity API
Readme
ravi
The TypeScript SDK for Ravi — the identity provider for AI agents.
npm install @ravi-hq/sdkimport { Ravi } from "@ravi-hq/sdk";
// Use an identity key (ravi_id_...) scoped to one identity, or a
// management key (ravi_mgmt_...) for account-level access.
const ravi = new Ravi({ apiKey: process.env.RAVI_API_KEY });
// The caller chooses the identity, then works on it. The API key is only an
// auth fence — every per-identity call sends ?identity=<uuid> automatically.
const identity = await ravi.identities.create({
name: "shopping-agent",
email_identifier: "shopping", // local part; omit to auto-generate. optional `domain` too.
provision_phone: true, // also give it a phone number
});
// `email` and `phone` are channel objects, not strings. The identifiers are
// `.address` / `.number`.
console.log(identity.email.address, identity.phone?.number);
// Send an email from the identity's inbox.
const msg = await identity.email.send(
"[email protected]",
"Hello",
"Greetings from my AI agent",
);
// Wait for a reply, then reply to it — messages carry behaviour.
const reply = await identity.email.waitFor((m) => m.subject.includes("re:"));
await reply.reply("Thanks!");
// Send an SMS from the identity's number, then wait for the OTP.
await identity.phone?.send("+15551234567", "START");
const otp = await identity.phone?.waitFor((m) => /\d{6}/.test(m.body), { timeoutMs: 120_000 });
// Store credentials in the identity's vault.
await identity.vault.passwords.create({ domain: "acme.com", username: "agent", password: "…" });With an identity key you don't have the uuid handy — ravi.me() returns the
single identity the key is fenced to:
const me = await ravi.me(); // throws on a management key (ambiguous)
await me.email.send("[email protected]", "Hi", "…");The client talks to Ravi's hosted API. The API host is built into the SDK.
Object model
The API key is an auth fence only — it never selects the identity. The
caller picks an identity and calls channels/resources on it, each of which
sends ?identity=<uuid> on every request.
Account-level accessors on ravi:
| Accessor | Covers |
|----------|--------|
| ravi.identities | create/list/get/update, provisionPhone, voiceAgent.get/set |
| ravi.me() | the single identity an identity key is fenced to (throws on a mgmt key) |
| ravi.events | account-wide replay of the durable event log (since cursor) |
| ravi.domains | verified sending domains |
| ravi.webhooks | subscriptions + deliveries log |
| ravi.apiKeys | management + identity key management |
Per-identity accessors on an identity object:
| Accessor | Covers |
|----------|--------|
| identity.email | .address, inbox, threads, get, send, waitFor |
| identity.phone | null | .number, inbox, conversations, send, call, calls, waitFor |
| identity.contacts | contact directory + find/search |
| identity.vault.passwords | encrypted website credentials + generate |
| identity.vault.secrets | encrypted key/value secrets |
| identity.events | this identity's event replay |
Messages carry behaviour: EmailMessage has reply/replyAll/forward/markRead;
SmsMessage has reply (synthesized as a fresh send to from_number)/markRead;
Call has hangup/transcript.
Field names on the returned objects mirror the API's snake_case wire shapes verbatim.
Voice calls
const call = await identity.phone?.call("+15551234567");
// ...later
const segments = await call?.transcript();Configure the voice agent (the webhook Ravi calls with each transcript) per identity:
const cfg = await ravi.identities.voiceAgent.set(identity.uuid, {
response_url: "https://my-agent.example.com/voice",
});
console.log(cfg.signing_secret); // shown once — use it to verify inbound requestsReceiving events
Ravi persists every event (inbound email/SMS, call lifecycle, ...). Consume them two ways — a WebSocket for real time, and a replay endpoint for catch-up after a reconnect.
// Replay everything after your last cursor, then dedup by envelope id.
const events = await ravi.events.list({ since: lastSeq, event_types: ["call.ended"] });
for (const e of events) {
console.log(e.seq, e.type, e.data);
}For live push, open a WebSocket to wss://api.ravi.app/ws/events/ with an
Authorization: Bearer <key> header; the server sends { id, seq, type, data }
envelopes plus periodic { type: "heartbeat" }. On (re)connect, replay via
ravi.events.list({ since }) first, then tail the socket — no events are lost across
a deploy-time disconnect.
Webhooks
Register a subscription and verify inbound deliveries. Ravi signs every delivery
(and every voice-agent request) with X-Ravi-Timestamp and
X-Ravi-Signature: sha256=<hex>, where the digest is
HMAC-SHA256(signing_secret, "<timestamp>.<rawBody>").
const sub = await ravi.webhooks.create({
url: "https://my-app.example.com/webhooks/ravi",
event_types: ["email.message.received", "call.ended"], // empty = all
});
console.log(sub.signing_secret); // shown onceimport { verifyWebhookSignature, WebhookSignatureError } from "@ravi-hq/sdk";
app.post("/webhooks/ravi", async (req, res) => {
const rawBody = await readRawBody(req); // the exact body string
try {
verifyWebhookSignature(
rawBody,
req.headers["x-ravi-timestamp"],
req.headers["x-ravi-signature"],
process.env.RAVI_WEBHOOK_SECRET!,
);
} catch (e) {
if (e instanceof WebhookSignatureError) return res.status(401).end();
throw e;
}
const event = JSON.parse(rawBody); // { id, type, data }
res.status(202).end();
// ...handle event in background
});verifyWebhookSignature throws WebhookSignatureError with code set to
"invalid_header", "expired", or "signature_mismatch". The timestamp tolerance
defaults to 5 minutes — override via the options argument.
