Notix
Guides

Send email from Node.js with one API call.

By the end of this guide you will have sent a real email from a Node.js or TypeScript script through the email API, seen the same send made with plain fetch and with Nodemailer through the SMTP relay, and added the three things a production send needs: an idempotency key, error handling, and a webhook for delivery status.

What you need

Three things, none of them a card.

An API key.

Created in the dashboard under API keys, shown once. A sending access key is enough for this guide and cannot read contacts or delete domains if it leaks.

A verified domain.

Add your domain, publish the SPF, DKIM and DMARC records Notix gives you, and the from address on it becomes valid. The quickstart walks through it.

Node 18 or later.

For built-in fetch. The SDK is one install:

terminal
npm install notix-js
# or: pnpm add notix-js / yarn add notix-js / bun add notix-js

The free plan covers 5,000 emails a month and 200 a day, and the free plan does not ask for a card.

Steps

From an empty file to a queued message.

  1. Initialise the client.

    The constructor takes the API key and, optionally, a different Notix base URL. Read the key from the environment; the SDK never logs it.

    notix.ts
    import { Notix } from "notix-js";
    
    // Keep the key in the environment, never in source.
    const notix = new Notix(process.env.NOTIX_API_KEY);
    
    // Pointing at a different Notix base URL (a staging environment, say)?
    // Pass it as the second argument.
    // const notix = new Notix(process.env.NOTIX_API_KEY, "https://notix.example.com");
    
  2. Send one email.

    notix.emails.send takes from, to, subject and html or text (send both; some clients only show one). Custom headers are forwarded as they are; Notix manages only X-Notix-Email-ID and References. The fetch tab is the same call without the SDK: one POST to /api/v1/emails with a Bearer token.

    send.ts
    import { Notix } from "notix-js";
    
    const notix = new Notix(process.env.NOTIX_API_KEY);
    
    const { data, error } = await notix.emails.send({
      from: "receipts@acme.com",
      to: "customer@example.com",
      subject: "Your receipt for order 4471",
      html: "<p>Thanks for your order.</p>",
      text: "Thanks for your order.",
    });
    
    if (error) {
      console.error(error.code, error.message);
    } else {
      console.log("queued", data.emailId);
    }
    
  3. Keep the id.

    The response carries the email’s id. Store it next to the order, the user or whatever caused the send: it is the key you read the message back with (notix.emails.get(id)) and the data.id every webhook event for that message carries.

  4. Or send through Nodemailer.

    Already on Nodemailer? Point its transport at the SMTP relay. Host smtp.usenotix.dev, port 465 with implicit TLS (or 587 for STARTTLS), username notix, and your API key as the password. The relay posts the parsed message to the same endpoint the SDK calls, so suppression, tracking and webhooks apply either way.

    nodemailer.ts
    import nodemailer from "nodemailer";
    
    const transporter = nodemailer.createTransport({
      host: "smtp.usenotix.dev",
      port: 465,
      // Implicit TLS on 465. Use port 587 with secure: false for STARTTLS.
      secure: true,
      auth: {
        user: "notix",
        // The SMTP password is a Notix API key.
        pass: process.env.NOTIX_API_KEY,
      },
    });
    
    await transporter.sendMail({
      from: "receipts@acme.com",
      to: "customer@example.com",
      subject: "Your receipt for order 4471",
      html: "<p>Thanks for your order.</p>",
      text: "Thanks for your order.",
    });
    
In production

The three lines that separate a demo from a system.

A send that works once is easy. A send that survives a retry, a bad address and a network blip needs an idempotency key, a look at the error envelope, and a webhook.

Idempotency key.

Pass { idempotencyKey } as the second argument (the SDK sends it as the Idempotency-Key header). Retrying the same key and body returns the original message instead of sending a second one; the same key with a different body is refused. Use the id of the business event: the order, the reset, the invoice.

The error envelope.

Every failure is { error: { code, message } }. The codes you will meet first: BAD_REQUEST for an unverified from domain or a malformed address, FORBIDDEN when a sending-access key reaches an endpoint it cannot use, and RATE_LIMITED when the per-second limit or the plan’s send limit is reached. The SDK returns the envelope as error rather than throwing.

Delivery status by webhook.

The API answers as soon as the message is queued. Delivery, bounces, complaints, opens and clicks arrive as email.* events on a webhook you register in the dashboard. Each request is signed with HMAC-SHA256 over timestamp.rawBody and carries X-Notix-Signature and X-Notix-Timestamp; the SDK’s constructEvent verifies both for you.

Environment and keys.

NOTIX_API_KEY for sending, NOTIX_WEBHOOK_SECRET for verifying. Give the backend that only sends a sending-access key, and keep a full-access key for the jobs that manage domains and contacts.

send-with-idempotency.ts
import { Notix } from "notix-js";

const notix = new Notix(process.env.NOTIX_API_KEY);

// One key per business event, so a retry can never send twice.
const { data, error } = await notix.emails.send(
  {
    from: "receipts@acme.com",
    to: "customer@example.com",
    subject: "Your receipt for order 4471",
    html: "<p>Thanks for your order.</p>",
    text: "Thanks for your order.",
  },
  { idempotencyKey: "order-4471-receipt" },
);

if (error) {
  // { code: "RATE_LIMITED" | "BAD_REQUEST" | "FORBIDDEN" | ..., message }
  throw new Error(`${error.code}: ${error.message}`);
}

// Delivery status arrives on your webhook as email.delivered, email.bounced, ...
console.log("queued", data.emailId);
webhook.ts
import { Notix } from "notix-js";

const notix = new Notix(process.env.NOTIX_API_KEY);
const webhooks = notix.webhooks(process.env.NOTIX_WEBHOOK_SECRET);

// Any framework that gives you the raw body and the headers works the same way.
export async function POST(request: Request) {
  const rawBody = await request.text();
  const event = webhooks.constructEvent(rawBody, { headers: request.headers });

  switch (event.type) {
    case "email.delivered":
    case "email.bounced":
    case "email.complained":
      // event.data.id is the emailId you were given at send time.
      break;
  }

  return new Response("ok", { status: 200 });
}

That is the whole production surface for a single send. Batch sends of up to 100 messages, scheduling, templates and retries are in the production guide for transactional email in Node.js.

Using SMTP instead

When the mailer is already wired, keep it.

If your app already sends through Nodemailer, a queue worker or a framework mailer, the relay is the shortest path: change the host and credentials and nothing else. You give up the idempotency key and the batch endpoint, which only the API exposes, but every message still lands in the same log, honours the same suppression list and fires the same webhooks. The Nodemailer step above is the complete configuration; the same four values work in any SMTP client. Sending one-time codes? The OTP use case uses the verification API rather than a template of your own, and the deliverability check can run on any message before it goes out.

FAQ

Questions, answered.

Do I need the SDK, or can I call the API directly?
Either. The SDK is a thin typed wrapper over the same JSON endpoints, so the fetch example on this page sends exactly what notix.emails.send sends. Use the SDK when you want types, the idempotencyKey option and the webhook verifier; use fetch when you would rather not add a dependency or you are on a runtime where you already have a fetch client.
Is Notix a Nodemailer alternative?
It works with Nodemailer rather than replacing it. Point nodemailer.createTransport at smtp.usenotix.dev with the username notix and an API key as the password, and everything Nodemailer already does keeps working. The relay turns the SMTP session into the same tracked, suppressed send the API makes. If you want idempotency keys, batch sends and webhooks, the API and the SDK expose those directly.
Does this work with TypeScript?
Yes. notix-js ships types generated from the API's OpenAPI schema, so the send payload, the response and the error envelope are all typed. The examples on this page are TypeScript; drop the type annotations and they run as plain JavaScript in Node 18 or later, where fetch is built in.
What does the response look like?
A successful send answers with the email's id, which you keep to read the message back or to match webhook events. An error answers with a JSON object of the shape { error: { code, message } }, and the SDK surfaces the same object as error on the result instead of throwing, so one if statement covers both.
Can I send React Email templates?
On a single send, yes: pass a React element as react instead of html and the SDK renders it with @react-email/render before the request goes out. Batch sends take html or text only. The Next.js guide shows a full template.

Send the first one now.

Verify a domain, copy an API key, run the script. The free plan does not ask for a card.