Node.js SDK
@torvath/mail: the email API, typed. Works on Node 18+, Cloudflare Workers, Bun and Deno, with no dependencies.
Install
npm install @torvath/mailimport { Torvath } from "@torvath/mail";
const torvath = new Torvath({ apiKey: process.env.TORVATH_API_KEY });| Option | Default | |
|---|---|---|
apiKey | required | Your key from Admin → API. |
maxRetries | 2 | Retries after rate limits, server errors and lost connections, waiting longer each time. |
timeoutMs | 30000 | Per request. A long event fetch waits longer. |
fetch | global fetch | Another fetch, such as for tests or a proxy. |
Send
const { id } = await torvath.emails.send({
from: { email: "billing@yourcompany.in", name: "Your Company" },
to: "customer@example.com",
subject: "Invoice 1042",
html: "<p>Your invoice is attached.</p>",
attachments: [{ filename: "invoice-1042.pdf", content: pdf.toString("base64") }],
tags: ["invoice"],
metadata: { invoice: "1042" },
});
const email = await torvath.emails.get(id); // status, and each recipient'sEvery send carries an Idempotency-Key, so a retry after a lost connection never sends twice. Pass your own to tie it to your records: send(email, { idempotencyKey: "invoice-1042" }). All fields are on Send an email.
Templates
await torvath.emails.send({
from: "billing@yourcompany.in",
to: "asha@example.com",
template: "invoice-ready",
variables: { name: "Asha", amount: "₹500" },
});Events
const listener = torvath.events.listen({ types: ["email.bounced", "email.complained"] }, async (event) => {
await markAddressBad(event.recipient, event.reason);
});| listen option | Default | |
|---|---|---|
types | all | Only these types; others stay for other consumers. |
concurrency | 5 | Events handled at once. |
includeMachine | false | Also pass opens and clicks that were likely machines. Otherwise they are acknowledged without calling you. |
onError | none | Called when a fetch fails. Listening carries on, waiting longer each time. |
listener.stop() stops fetching, lets running handlers finish and acknowledges them. listener.done settles when listening ends, and rejects on an error that can't clear by itself, such as a revoked key. More in Events queue.
Webhooks
import { verifyWebhook } from "@torvath/mail";
const event = await verifyWebhook(rawBody, request.headers, process.env.TORVATH_WEBHOOK_SECRET);Throws WebhookVerificationError when the request wasn't signed with your secret or its timestamp is more than 5 minutes off. A full example is on Webhooks.
Errors
Every error is a TorvathError with status, a code that stays the same (see error codes) and a message.
| Class | Status |
|---|---|
ValidationError | 400 |
AuthError | 401 |
PermissionError | 403 |
NotFoundError | 404 |
ConflictError | 409 |
RateLimitError | 429 |
ServerError | 5xx |
ConnectionError | No answer |
import { RateLimitError } from "@torvath/mail";
try {
await torvath.emails.send(email);
} catch (err) {
if (err instanceof RateLimitError && err.code === "monthly_limit") notifyBilling();
else throw err;
}