Send email from Node.js with an email API
Send email from Node.js with the fetch it already has: one POST with four headers, an id back, and every delivery event on a webhook.
// Node 18+ ships fetch: there is nothing to install.const apiKey = process.env.RASKET_API_KEY;if (apiKey === undefined) { throw new Error("RASKET_API_KEY is not set");}
const response = await fetch( "https://api.rasket.com/emails", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, // Required on every request. "User-Agent": "acme-billing/1.0", "Content-Type": "application/json", // A retry with this key cannot send twice. "Idempotency-Key": "order-1042", }, body: JSON.stringify({ from: "Acme <orders@send.acme.example>", to: ["ronald.williams@example.com"], subject: "Your order has shipped", html: "<p>Order 1042 shipped today.</p>", }), },);
if (!response.ok) { throw new Error(`Rasket answered ${response.status}`);}
const email = (await response.json()) as { id: string };console.log(email.id);What sending from Node.js actually involves
To send email from Node.js you make one HTTPS request. Your server posts the message to an email API, gets an id back, and hears what happened to it afterwards on a webhook. Rasket is that API, and Node 18 and later ship fetch, so the sample below needs nothing installed. A typed Node client, the rasket package, is coming soon.
Last updated 2026-10-13. Every field, header and limit below is the one the emails reference carries.
What you get with the key
Five things that arrive with the first request rather than with a later plan.
Idempotent sends
Put an Idempotency-Key on a send and repeat it as often as you like. For 24 hours the same key and payload return the first response instead of a second email, and a different payload under that key is refused rather than sent.
Signed webhooks you can replay
Delivery, bounce, complaint, open and click posted to your endpoint with a timestamp and a signature, retried on failure and replayable from the dashboard.
Inbound mail on your own domain
Receive at your domain on every plan. Each message arrives on a signed webhook with its headers, text and HTML, and its attachments behind signed links.
Templates with typed variables
Publish a template, then send it by ID or alias with the values it declares. Every email records the exact version it was rendered from.
Campaigns and automations
Contacts with typed properties, topics people subscribe to, and per-contact workflows that wait, branch and send — through the same pipeline as the rest of your mail.
How to send email from Node.js
Six steps, in this order. Everything before the fifth is done once, and only the fifth is about the language you write in.
- 1
Create an API key
Open API keys in the dashboard and create one. It is shown once and stored hashed, so copy it then. Put it in your environment as RASKET_API_KEY and read it from there; a key committed to a repository is a key you have to rotate.
- 2
Add a sending domain
POST /domains with a domain you control. A subdomain such as send.acme.example is the usual choice, because it keeps this mail's reputation separate from the address your people write from. The response carries a records array.
- 3
Publish the DNS records
Every row of records goes into your DNS: the DKIM TXT record at rasket._domainkey, and the CNAME that gives the domain its own return path. Nothing sends from the domain until they resolve.
- 4
Verify the domain
POST /domains/{domain_id}/verify. The domain comes back with a status on each record, and once it reads verified you can send from any address on it. An unverified sender is refused rather than quietly dropped.
- 5
Send your first email from Node.js
Post the message with the sample above. Carry an Idempotency-Key — yours or your queue's retry cannot then turn into a second email — and a User-Agent, which every request has to have. A 200 with an id means we have taken responsibility for the message.
- 6
Subscribe a webhook and check its signature
POST /webhooks with an https endpoint and the events you care about. Delivery, bounce, complaint, open and click arrive there with svix-id, svix-timestamp and svix-signature; verify against the raw body before anything parses it.
Verify webhooks in Node.js
Read the raw bytes before anything parses them. In a bare Node server that means concatenating the chunks on the request; in a framework with a global JSON body parser it means mounting a raw parser on that route only. Then pass the bytes and the headers to new Webhook(secret).verify() from the svix package — the scheme is Svix-compatible. A body that has been parsed and serialised again is a different string from the one that was signed, so the check will fail even though the request was genuine.
The five steps a verifier performs, in seven languages, are on the webhooks reference.
The three ways to send email from Node.js
To send email from Node.js you have to hand the message to something that will deliver it. Node has no mail transport of its own — there is no fs.writeMail — so every answer is a choice about what sits between your process and the recipient’s mailbox provider.
There are three, and they are easy to confuse because two of them are the same thing wearing different clothes. A Node.js email API is an HTTPS endpoint: you POST a JSON message and get back an id. SMTP is the protocol mail servers have spoken since RFC 5321, and you can open a socket and speak it yourself. Nodemailer is the package almost everyone reaches for when they mean the second one: it is an SMTP client with a friendly message builder on top.
The important distinction is not the protocol. It is what the thing on the other end does for you. A relay takes bytes and forwards them. A sending platform verifies your domain, signs the message, keeps a suppression list so you do not mail an address that already bounced, records what happened, and tells you about it.
Rasket takes both
Rasket is an HTTP API, and it also runs an SMTP relay. If your application is built around a Nodemailer transport today, you can point that transport at smtp.rasket.com with your key as the password and change nothing else — then move to the one HTTP call below when you want the message id back. The SMTP docs have the transport settings.
API, SMTP and Nodemailer, side by side
The three approaches differ in how much of the work is yours. This is the shape of the trade, with the last row the one most people actually decide on.
| HTTP API | Raw SMTP | Nodemailer | |
|---|---|---|---|
| Protocol | HTTPS, one request | SMTP over TCP | SMTP over TCP |
| Round trips per send | One | Several, by design | Several, by design |
| What you get back | A message id, immediately | A queue response line | A queue response line |
| Attachments | In the same JSON body | MIME you assemble | Built for you |
| Domain authentication | Generated for you, checked for you | Yours to publish and watch | Yours to publish and watch |
| Suppressions | Kept and enforced on send | None | None |
| Delivery, bounce and complaint events | Signed webhooks | Bounce mail you parse | Bounce mail you parse |
| Works behind a firewall that blocks port 587 | Yes | No | No |
| Best for | Product mail you need to account for: receipts, resets, campaigns | A mail server you already run | An app that already has an SMTP relay and does not need events |
None of this makes Nodemailer a bad package. It is a very good SMTP client. It is simply not a sending platform, and a great many outages have started with a team assuming it was one.
Sending with plain fetch
The shortest path today is the fetch built into Node. The API is ordinary HTTP: a bearer token, a JSON body, and an idempotency header, with nothing to install.
const response = await fetch("https://api.rasket.com/emails", { method: "POST", headers: { authorization: `Bearer ${process.env.RASKET_API_KEY}`, "content-type": "application/json", "user-agent": "acme-billing/1.0", "idempotency-key": "receipt-1042", }, body: JSON.stringify({ from: "Acme <billing@acme.example>", to: ["ronald.williams@example.com"], subject: "Your receipt", html: "<p>Thanks for your order.</p>", }),});
if (!response.ok) { throw new Error(`send failed: ${String(response.status)}`);}
const email = await response.json();console.log(email.id);Four headers carry the whole contract. Authorization is Bearer and your API key, read from the environment and never written into the file — a key in a repository is a key you will rotate on somebody else’s schedule. Content-Type is application/json. User-Agent is required: a request with no User-Agent is refused, and naming your application is what lets support tell your billing worker from your web process. And Idempotency-Key is derived from the thing that caused the send — the order — rather than generated fresh, which is the difference between a retry that is safe and a retry that sends a second receipt.
What comes back
A 200 carries the message’s id, and the response headers carry the rate limit state and the request id to quote in a support conversation. Store email.id against your own record. It is the id every later event and every question about this message is about.
The Rasket Node SDK, coming soon
Coming soon: the rasket package is not published yet. When it is, it will wrap this same request with typed bodies generated from the API’s OpenAPI document, a retry policy and the webhook verifier. Until then, the fetch above is the whole of a send, and a curl of the same request is the quickest first experiment.
Where SMTP and Nodemailer still fit
SMTP is not obsolete, and saying so would be a sales pitch rather than an engineering claim. It is the protocol every message eventually travels over, and there are three situations where speaking it directly from Node is the right answer.
- You already run a mail server. If there is a relay inside your network that handles authentication, queueing and retries, pointing Nodemailer at it is less work than adding an external dependency.
- The mail never leaves the building. Internal notifications to an internal relay have no deliverability problem to solve, because there is no external reputation involved.
- You are porting something that already works. A working SMTP integration you are not being asked to improve is not a bug.
For reference, this is what the Nodemailer version of the same send looks like.
import nodemailer from "nodemailer";
const transport = nodemailer.createTransport({ host: "smtp.example.com", port: 587, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },});
await transport.sendMail({ from: "Acme <billing@acme.example>", to: "ronald.williams@example.com", subject: "Your receipt", html: "<p>Thanks for your order.</p>",});Compare it with the API sample above and notice what is missing rather than what is different. There is no id to store. There is nothing that will tell you, four hours later, that the message bounced. If the address was already known to be dead, the message goes out anyway. Those gaps are not Nodemailer’s fault — they are on the other side of the socket — but they are yours to fill.
Retries, idempotency and rate limits
A network call that times out has not necessarily failed. The request may have arrived, the message may have been accepted, and only the response may have been lost. Retrying blindly is how one order becomes three receipts.
An Idempotency-Key is what makes the retry safe. Send the same key with the retry and a repeat is recognised as a repeat: the original response comes back, marked as a replay, and no second message is sent. The window is 24 hours. Derive the key from the event that caused the send — an order id, an invoice number, a password-reset request id — so that two attempts to send the same thing produce the same key, and two different things never do. Read the idempotency guide for the exact semantics.
The rate limit is ten requests a second per team, across every endpoint. You do not have to guess where you are in the window, because every response says:
ratelimit-limit: 10ratelimit-remaining: 7ratelimit-reset: 1Pace a bulk job off ratelimit-remaining rather than off a sleep you tuned once on a quiet afternoon. If you do go over, the answer is a 429 carrying how long to wait in retry-after: wait that long, then send the same request with the same Idempotency-Key. Never retry a write that carries no key, which is the correct default: at-most-once matters more than a saved round trip.
Verifying delivery with webhooks
A 200 from the send endpoint means the message was accepted, not that it arrived. What happened next — delivered, delayed, bounced, complained, opened, clicked — reaches you as a signed webhook. This is the part that has no equivalent in the Nodemailer path, and it is usually the reason a team moves.
Three headers travel with every delivery:
svix-id— the event’s own id, stable across replays. Deduplicate on it.svix-timestamp— Unix seconds. Anything more than five minutes out is refused.svix-signature— one or more signatures; any one matching is enough, which is what makes a secret rotation survivable.
Verify before you trust, and verify against the bytes that arrived rather than a body your framework has already parsed and re-serialised — a re-serialised body is a different string, and a different string has a different signature.
// npm install svix — the scheme is Svix-compatible, so the official package verifies it.import { Webhook } from "svix";
const wh = new Webhook(process.env.RASKET_WEBHOOK_SECRET);
export async function POST(request: Request): Promise<Response> { const rawBody = await request.text(); // text(), never json()
try { const event = wh.verify(rawBody, Object.fromEntries(request.headers)); await record(event); return new Response(null, { status: 200 }); } catch { return new Response("invalid signature", { status: 400 }); }}The scheme is Svix-compatible, so the official svix package verifies our deliveries as they are; @rasket/webhook-verify, our own zero-dependency verifier, follows when it is published. Events can arrive more than once and can arrive out of order, so treat your handler as a fold over a stream rather than a state machine with an expected sequence. The events guide lists every event and its payload.
Where to go from here
If you are starting from nothing, the quickstart is a key, a domain and a first send in about five minutes. If you are migrating, the shortest useful step is to send one real message through the API beside your existing transport and compare what each one can tell you about it an hour later.
Sources
- RFC 5321: Simple Mail Transfer Protocol — IETF, read 2026-09-16
- RFC 5322: Internet Message Format — IETF, read 2026-09-16
- Nodemailer documentation — Nodemailer, read 2026-09-16
- Using the Fetch API — MDN Web Docs, read 2026-09-16
Questions about Node.js
Do I need an SMTP server to send email from Node.js?
No. Rasket sends over HTTPS: an API key, one POST and a JSON body. If your app already sends through Nodemailer, you can also keep it and point the transport at Rasket's SMTP relay — smtp.rasket.com, the username rasket and your key — then move to the API when you want the message id back.
Where do I keep the API key in a Node app?
In the environment, read as process.env.RASKET_API_KEY when the process starts and checked once, so a missing key fails at boot rather than on the first send. Nothing in the sample above holds a key literal and nothing should: a key is shown once and stored hashed, so a leaked one is rotated rather than recovered.
How do I stop a retry from sending the same email twice?
Send an Idempotency-Key header, as the sample above does, derived from the work the mail belongs to. For 24 hours the same key with the same payload returns the first response instead of sending again, and a different payload under a key you have already used is refused rather than sent.
How do I verify a Rasket webhook in Node?
npm install svix, read the raw body first, then call new Webhook(secret).verify(rawBody, headers) — the scheme is Svix-compatible. It requires svix-id, svix-timestamp and svix-signature, rejects a timestamp more than five minutes out, and compares in constant time. Parsing the body before you verify destroys what was signed.
Can I send a template or a batch from Node?
Yes. A send can name a published template with template: { id, variables } instead of carrying a body, and the batch route takes up to a hundred messages in one request, validated all or none. A batch counts as one call against the ten-a-second limit, which is usually the fix for a throttled loop.
Can I use Nodemailer with Rasket?
Yes. Point a Nodemailer transport at smtp.rasket.com on port 465 with secure: true, the username rasket and your API key as the password, and each message becomes an ordinary API send. The HTTP call this guide shows is still the better long-term shape, because it returns the message id and makes a retry safe, and the message fields have the same names, so the move is small.
Do I need a verified domain to send?
Yes. A send from a domain that has not passed verification is refused before anything leaves the building. Verification means publishing the DKIM, SPF and DMARC records the dashboard generates for that domain and letting the checks pass, which usually takes minutes once DNS has propagated.
How many requests a second can I make?
Ten per second per team, across every endpoint. Every response carries the limit, what is left and when the window resets, so a client can pace itself rather than guess. Going over gets a 429 with a retry-after value.
Is an HTTP API faster than SMTP?
Usually, and for a reason that has nothing to do with the network. SMTP is a conversational protocol: a send is several round trips before the body is even offered. One HTTPS request is one round trip, and it returns an id you can store immediately rather than a queue receipt you have to correlate later.
Can I send attachments from Node.js?
Yes. Attachments go in the same request, either as base64 content or as a URL we fetch. There is a ceiling on the total request size, so very large files are better sent as a signed link in the body than as an attachment.
Keep reading
Make your first send
Create a key, verify a domain, and post your first message from Node.js. The free plan does not expire.