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.
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.
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.
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.
For built-in fetch. The SDK is one install:
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.
The constructor takes the API key and, optionally, a different Notix base URL. Read the key from the environment; the SDK never logs it.
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");
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.
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);
}
const response = await fetch("https://app.usenotix.dev/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTIX_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
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.",
}),
});
const body = await response.json();
if (!response.ok) {
// Every error is { error: { code, message } }.
console.error(body.error.code, body.error.message);
} else {
console.log("queued", body.emailId);
}
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.
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.
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.",
});
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.
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.
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.
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.
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.
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);
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.
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.
One JSON API for transactional and marketing email: send, batch, schedule, webhooks, typed SDKs.
GuidesIdempotency keys, batch sends, a webhook handler, templates, the error envelope and retries on the typed SDK.
Use casesSend one-time codes by email or SMS with two API calls, with expiry, length and risk scoring handled for you.
LearnWhat a pre-send deliverability check looks at, the verdict and score it returns, and the four findings that block a send.
IntegrationsSend from Cloud Functions with the SDK, and point Firebase Auth's custom SMTP at the relay for its own emails.
NotixA free plan with no card, and Pro at $15 a month for 50,000 emails. Only sent volume is metered.
Verify a domain, copy an API key, run the script. The free plan does not ask for a card.