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

@vibemailai/sdk

v1.2.0

Published

Official VibeMail SDK for TypeScript, Node and Bun

Readme

VibeMail SDK for TypeScript

Official SDK for the VibeMail transactional email API. Works in Node 18+, Bun and Deno. No runtime dependencies.

npm install @vibemailai/sdk

Send an email

import { VibeMail } from "@vibemailai/sdk";

const vibemail = new VibeMail(process.env.VIBEMAIL_API_KEY!);

const { id } = await vibemail.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Welcome",
  html: "<p>Glad you're here.</p>",
  text: "Glad you're here.",
});

The call returns once the API has accepted the message, not once it has been delivered. Use emails.get(id) to follow it.

Always send a text alternative alongside html. Mail with no plain-text part is measurably more likely to be spam-foldered.

Recipients

One message goes to one recipient. To reach several people, use batch() — it sends a separate message to each, which is also what stops your recipients from seeing one another's addresses:

await vibemail.emails.batch([
  { to: "[email protected]", subject: "Welcome", text: "Hi Ada" },
  { to: "[email protected]", subject: "Welcome", text: "Hi Alan" },
]);

Each entry succeeds or fails on its own; one bad address does not sink the rest.

const { data } = await vibemail.emails.batch(messages);
for (const result of data) {
  if ("error" in result) console.warn("rejected:", result.error);
}

Retries and idempotency

Pass an idempotencyKey on anything you might retry. A repeat carrying the same key returns the original result instead of sending a second copy — which matters because a client that times out has no way of knowing whether the message went out.

await vibemail.send({
  to: "[email protected]",
  subject: "Receipt",
  text: "Thanks!",
  idempotencyKey: `receipt-${orderId}`,
});

The SDK retries 429 and 5xx responses on its own (twice by default, honouring Retry-After) and never retries a request the server rejected on its merits. Set maxRetries: 0 to handle that yourself.

Scheduling

const { id } = await vibemail.send({
  to: "[email protected]",
  subject: "Reminder",
  text: "Standup in 15 minutes.",
  scheduledAt: "in 2 hours",          // or a Date, or an ISO 8601 string
});

await vibemail.emails.cancel(id);      // while it is still scheduled

Up to 30 days ahead. Once the dispatcher has claimed a message it is on its way out and can no longer be withdrawn.

Templates

const templates = await vibemail.templates.list();

await vibemail.send({
  to: "[email protected]",
  template: "welcome",
  variables: { name: "Ada" },
});

Anything you pass explicitly — subject, html, text — overrides the stored template, so you can vary one part without redefining the rest.

React email

import { WelcomeEmail } from "./emails/welcome";

await vibemail.send({
  to: "[email protected]",
  subject: "Welcome",
  react: <WelcomeEmail name="Ada" />,
});

Requires @react-email/render as an optional peer dependency:

npm install @react-email/render react

Your component is rendered in your process, not on our servers. It may close over anything in your application, and shipping it elsewhere to execute would mean handing us your code and your data; what leaves is plain HTML you can inspect. A plain-text alternative is generated automatically unless you pass text.

Domains

Mail sent from your own domain needs that domain added and verified. Adding one returns the DNS records to publish; it stays unverified until they resolve.

const domain = await vibemail.domains.create("yourdomain.com");

for (const [purpose, record] of Object.entries(domain.dns_records ?? {})) {
  console.log(purpose, record.type, record.host, record.value);
}
// verification TXT _vibemail-verify.yourdomain.com vm-verify-...
// mx           MX  yourdomain.com                 mail.vibemail.ai
// spf          TXT yourdomain.com                 v=spf1 include:mail.vibemail.ai -all
// dmarc        TXT _dmarc.yourdomain.com          v=DMARC1; p=quarantine; ...

dns_records is keyed by purpose, not a list: verification, mx, spf and dmarc. Each carries type, host, value, and priority where the type takes one. Publish all four; the domain verifies once they resolve.

Come back for the same records at any time, along with whether verification has gone through:

const { is_verified, dns_records } = await vibemail.domains.get(domain.id);

Listing is paged. total counts every domain on the account, not just the page you asked for:

const { data, total } = await vibemail.domains.list({ limit: 25, offset: 0 });
await vibemail.domains.delete(domain.id);

Removing a domain does not affect mail already sent from it.

Contacts

await vibemail.contacts.create({
  email: "[email protected]",
  name: "Ada Lovelace",
  notes: "met at the analytical engine demo",
});

const { data } = await vibemail.contacts.list({ search: "ada", limit: 50 });
await vibemail.contacts.delete(data[0].id);

search matches against address and name.

Suppressions

Addresses that will not be sent to: hard bounces, spam complaints, and anything blocked by hand. A send to a suppressed address is dropped rather than attempted, which is what keeps a bad list from taking your sending reputation with it.

const { data, total } = await vibemail.suppressions.list({ limit: 100 });

for (const entry of data) {
  console.log(entry.email, entry.reason);   // "manual", or "hard bounce: ..."
}

reason is free text, not a fixed set: manual for anything added by hand or through the API, and hard bounce: ... carrying the remote server's own wording when the queue gave up on an address.

Paging

domains.list, contacts.list and suppressions.list all return the same envelope:

{ object: "list", total: 128, limit: 50, offset: 0, data: [...] }

limit defaults to 50 and is capped at 100. Walk the whole set by stepping offset until you have total:

const all = [];
for (let offset = 0; ; offset += 100) {
  const page = await vibemail.contacts.list({ limit: 100, offset });
  all.push(...page.data);
  if (all.length >= page.total) break;
}

Errors

import { VibeMailError, VibeMailTimeoutError } from "@vibemailai/sdk";

try {
  await vibemail.send({ to: "[email protected]", subject: "Hi", text: "..." });
} catch (err) {
  if (err instanceof VibeMailError) {
    console.error(err.status, err.detail);   // 422, "'to' and 'subject' are required"
    if (err.isRetryable) { /* 429 or 5xx */ }
  } else if (err instanceof VibeMailTimeoutError) {
    // exceeded `timeout`, default 30s
  }
}

| Status | Means | | --- | --- | | 400 | The request was malformed, such as a domain that is not a domain. | | 401 | The API key is missing, wrong, or revoked. | | 402 | A plan limit was reached. err.detail says which. Not retried. | | 404 | No such record, or not yours. | | 409 | Already exists, such as a domain someone has registered. | | 422 | Required fields missing, or a schedule more than 30 days out. | | 429 | Rate limited. Retried automatically, honouring Retry-After. | | 5xx | Our fault. Retried automatically. |

Options

Either form works:

new VibeMail("vm_live_...");
new VibeMail("vm_live_...", { timeout: 10_000 });
new VibeMail({ apiKey: "vm_live_...", timeout: 10_000 });

| Option | Default | | | --- | --- | --- | | apiKey | — | Required. | | baseUrl | https://vibemail.ai | Point at a self-hosted deployment. | | timeout | 30000 | Per-request, in milliseconds. | | maxRetries | 2 | Applies to 429 and 5xx only. | | fetch | global fetch | Supply your own for tests or proxying. |

API

| | | | --- | --- | | send(input) | Shorthand for emails.send. | | emails.send(input) | Send one message. | | emails.batch(inputs, opts?) | Send many, one recipient each. | | emails.get(id) | Status of a send. | | emails.cancel(id) | Withdraw a scheduled send. | | templates.list() | Stored templates. | | domains.list(opts?) | A page of sending domains. | | domains.get(id) | One domain, with the records that verify it. | | domains.create(domain) | Add a domain. | | domains.delete(id) | Remove a domain. | | contacts.list(opts?) | A page of contacts. search matches address or name. | | contacts.create(contact) | Store a contact. | | contacts.delete(id) | Forget a contact. | | suppressions.list(opts?) | Addresses that will not be sent to. |

Send fields

| Field | | | --- | --- | | to | Recipient. One per message; use batch() for many. | | from | Sender. Must be an address your account owns. Defaults to the account address. | | subject | | | text | Plain-text body. Always send one alongside html. | | html | HTML body. | | react | A React element, rendered in your process. See above. | | template | Name or id of a stored template. | | variables | Values substituted into the template. | | tags | Labels carried through to analytics. | | trackOpens | Injects a tracking pixel into HTML sends. Defaults to on. | | trackClicks | Rewrites links through the redirector. Defaults to on. | | scheduledAt | ISO 8601, a Date, or a relative offset like "in 2 hours". Up to 30 days. | | idempotencyKey | A retry with the same key returns the original result. |

Not in the SDK yet

Webhooks and analytics are served by the API but are not wrapped here. They will be added when the shapes settle; until then reach them with fetch and the same bearer token.

Framework examples

License

MIT