@vibemailai/sdk
v1.2.0
Published
Official VibeMail SDK for TypeScript, Node and Bun
Maintainers
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/sdkSend 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 scheduledUp 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 reactYour 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
