npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/sdk
import { 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 requests

Receiving 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 once
import { 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.

License

Apache 2.0.