Skip to content
Node.js

Send Email with Node.js and the TypeScript SDK

Send email from Node.js with the Samva TypeScript SDK: send, retry safely, handle errors, and move over from Nodemailer. Runs on Node 24 with no build step.

Published

To send an email from Node.js, install the samva package, create a client with your API key, and call samva.email.send. It resolves with the accepted message and its id. The SDK is ESM, ships its own types, and runs on Node.js, Bun, and Deno. On Node 24 and later you can run the TypeScript file as it is, with no build step.

Before you send

You need an API key from the dashboard, kept in SAMVA_API_KEY, and a verified sending domain or a verified sender address. Samva sends only from addresses you own, so hello@yourdomain.com below must be one of them.

npm install samva

Send your first email

import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

const message = await samva.email.send({
  from: { email: "hello@yourdomain.com", name: "Acme" },
  to: "ada@example.com",
  subject: "Welcome to Acme",
  html: "<h1>Welcome!</h1><p>Thanks for joining.</p>",
  text: "Welcome! Thanks for joining.",
});

console.log("Email accepted:", message.id);
SAMVA_API_KEY=samva_sk_live_... node send.ts

to, cc, and bcc take one address or an array. replyTo sets where replies go. Send a template instead of inline content with templateSlug and templateData; the send guide covers templates, attachments, and scheduling, and the SDK reference lists every field.

The message comes back as pending: Samva accepted it, and delivery is reported afterward through webhooks or the message's status.

Retry without sending twice

If a request times out, you cannot tell whether Samva accepted it. Give each logical send its own idempotency key, and an identical retry returns the original message instead of sending a second email:

import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

export async function sendReceipt(orderId: string, to: string) {
  return samva.email.send(
    { to, subject: "Your receipt", text: `Thanks for your order ${orderId}.` },
    { idempotencyKey: `receipt:${orderId}` },
  );
}

The SDK sends a key on every call and generates a fresh one when you do not pass your own, so a key you choose is what makes a retry safe. Reusing a key with a different request throws a ConflictError.

Handle errors

A failed call throws. Every error the API declares is a class named for its _tag, and all of them extend SamvaApiError:

import { createClient, RateLimitedError, SamvaApiError, SamvaTransportError } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

try {
  const message = await samva.email.send({
    to: "ada@example.com",
    subject: "Test",
    text: "Hello",
  });
  console.log(message.id);
} catch (error) {
  if (error instanceof RateLimitedError) {
    console.error(`Retry after ${error.retryAfterSeconds} seconds`);
  } else if (error instanceof SamvaApiError) {
    console.error(error._tag, error.status, error.message);
  } else if (error instanceof SamvaTransportError) {
    console.error("Network or response failure", error.cause);
  } else {
    throw error;
  }
}

The error reference says which errors are worth retrying and which need a change to the request.

Moving from Nodemailer

Nodemailer hands a message to an SMTP server and reports whether that server took it. Samva's send path is an HTTPS call that returns a message id you can follow through delivery events. Moving over replaces the transport and the sendMail call; most message fields keep their names.

Before, with Nodemailer:

import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: 587,
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});

await transporter.sendMail({
  from: '"Acme" <hello@yourdomain.com>',
  to: "ada@example.com",
  subject: "Your receipt",
  html: "<p>Thanks for your order.</p>",
});

After, with Samva:

import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

await samva.email.send({
  from: { email: "hello@yourdomain.com", name: "Acme" },
  to: "ada@example.com",
  subject: "Your receipt",
  html: "<p>Thanks for your order.</p>",
});
NodemailerSamva
createTransport({ host, port, auth })createClient({ apiKey }). No SMTP host, port, or password.
from: '"Acme" <hello@yourdomain.com>'from: { email: "hello@yourdomain.com", name: "Acme" }
to, cc, bcc as strings or arraysThe same, as plain addresses
replyTo, subject, html, textThe same names
attachments: [{ filename, path }]Upload the file with samva.media, then pass attachments: [{ filename, mediaId, contentType, size }], or pass base64 content (attachment limits)
inReplyToinReplyToMessageId, the id of a Samva message

Two things change beyond the call. The From domain must be verified in Samva before the first send, which publishes the DKIM and SPF records mailbox providers check. And a message.id replaces the SMTP response, so delivery results arrive as signed webhooks rather than in the return value of the send.

Frequently Asked Questions

Related Resources

Send

Ship your first email today.

Transactional and product email through one typed API, with signed events, conversation threading, and deliverability handled.