Node.js
Updated
Send emails from Node.js with Lumail
The lumail package is a typed client over the Lumail REST API. It works in any Node.js 20+ process: a one-off script, an Express server, a queue worker or a cron job.
TL;DR
npm install lumail, create the client with new Lumail({ apiKey }), then await lumail.emails.send({ from, to, subject, html }). The result is { data, error }: check error, read data.id. For several emails at once use lumail.emails.batch([...]), up to 100 per request. Add an idempotencyKey to anything you might retry.Prerequisites
- Node.js 20 or later (the package targets Node 20 and uses the built-in fetch).
- A Lumail API token that starts with
lum_. - A sending domain verified in the same Lumail organization as the token.
1. Install the packages
2. Set your environment variables
Keep the token in a .env file locally and in your platform's secret settings in production. Node 20.6 and later can load the file without a dependency through node --env-file=.env.
Add .env to .gitignore before the first commit. A leaked token can send email from your verified domain.
3. Send your first email from a script
The package ships ESM and CommonJS builds. In an ES module, top-level await keeps a script short. In CommonJS, use const { Lumail } = require("lumail") inside an async function.
Exactly one of html, markdown or tiptap is required. Leave text out and Lumail generates the plain-text part from the HTML.
4. Send from an Express route
Create the client once at module level and reuse it. The constructor throws on an empty key, so a missing variable fails at boot rather than on the first request.
5. Send a batch
lumail.emails.batch takes an array of the same objects send accepts, up to 100, and returns the queued ids in the same order. Each item still has one recipient.
Batch is for transactional fan-out, such as notifying every member of a workspace. Newsletters belong in Lumail campaigns, which handle unsubscribes and segments for you.
Handle errors
Every SDK method resolves to { data, error } and never throws on an HTTP error. error.name is the stable code to branch on and error.statusCode is the HTTP status. When the request never got an answer, error.name is network_error or timeout and there is no status.
Retry only what is worth retrying: transport failures and 5xx responses. A validation_error will fail the same way every time, and a 429 from the recipient guard means that mailbox already got enough email.
| Status | error.name | What to do |
|---|---|---|
| 401 | missing_api_key | Missing or invalid token. Check the env var on the server that sends. |
| 403 | missing_permission | The token lacks the emails permission. Mint one that has it. |
| 402 | plan limit | The organization used its plan's email volume. Upgrade or wait for the next period. |
| 4xx | validation_error | Missing field, more than one body format, or a from domain that is not verified. |
| 429 | RECIPIENT_RATE_LIMITED | That mailbox hit 5 sends in 10 minutes or 20 in 24 hours. Honor Retry-After. |
| none | network_error / timeout | The request never got an answer. Safe to retry with the same idempotency key. |
Make retries safe with idempotency
The SDK retries GET, PUT and DELETE on its own, but never POST, because a blind retry could send twice. The idempotencyKey option makes your own retries safe: the same key returns the email already queued.
On batch, the key is suffixed with :index per item on the server, so a retried batch reuses each email individually. Keys can be up to 256 characters.
Verify your sending domain
Lumail only sends from a domain you have verified. Add the domain in your organization's Domains settings, then publish the SPF, DKIM and DMARC records it shows at your DNS provider. Until the domain verifies, every send fails with an error saying the domain is not authorized or verified.
Use a subdomain such as mail.yourdomain.com if your root domain already sends from another provider. Start DMARC at p=none, then tighten it once reports look clean.
- Email domains - Add a domain and the SPF, DKIM and DMARC records.
- Add a DMARC record - Publish a policy, then tighten it safely.
- Mail tester - Send a real email and check authentication and spam signals.
Production checklist
- Workers and queues pass an idempotency key built from the job or event id.
- The process fails at boot when
LUMAIL_API_KEYis missing instead of failing per request. - The
fromaddress is on a domain verified in the same organization as the token, with SPF, DKIM and a DMARC record. LUMAIL_API_KEYis set on every environment that sends, never committed, never in a public or client-side variable. Use one token per environment.- Every send that can be retried (webhooks, queues, jobs) passes an idempotency key derived from the triggering event.
- One-time codes and magic links set
tracking: { links: false, open: false }so links are not rewritten. - End-to-end tests send to fixture addresses such as
[email protected], which return an id without sending real email. - A failed email is logged with
error.nameand never fails the user's request on its own.
Frequently asked questions
Which Node.js version does the lumail package need?
Node.js 20 or later. The package is built for Node 20 and relies on the global fetch and AbortSignal.timeout, so it needs no HTTP dependency.
Can I use the Lumail SDK with CommonJS and require?
Yes. The package ships both an ES module and a CommonJS build, so import { Lumail } from "lumail" and const { Lumail } = require("lumail") both work, with TypeScript types for each.
Does the SDK retry failed sends automatically?
Not for sends. It retries idempotent GET, PUT and DELETE requests with backoff, but POST requests like emails.send are returned to you as-is. Retry yourself with the same idempotencyKey so a retry never sends twice.
Can I send one email to several recipients?
Each email has a single to address. Use cc and bcc for copies, or emails.batch to send up to 100 separate emails in one request so every recipient gets their own message.
Can I use Nodemailer with Lumail instead of the SDK?
Yes, through Lumail's SMTP relay at smtp.lumail.io on port 587 with your API token as the password. The HTTP API gives clearer errors and idempotency, so prefer the SDK when you control the code.
How do I test sending without spamming real inboxes?
Send to fixture addresses such as [email protected], [email protected] or any .test domain. Lumail queues them and returns an id but never delivers them, and they skip the per-recipient rate guards.
Keep building
- GuideSend emails from Next.js with LumailApp Router route handlers and server actions with the lumail SDK.
- GuideSend emails from Python with LumailCall the REST API with requests or httpx, from scripts, Flask or FastAPI.
- GuideSend emails from TanStack Start with LumailServer functions and server routes that keep the key on the server.
- DocsSDK: lumail.emails.sendSignature, options and the result shape.
- DocsBatch Emails API referenceUp to 100 transactional emails in one request.
- DocsSMTP relayFor tools that can only speak SMTP.
- DocsTest-mode recipientsFixture addresses that never send real email.
- DocsLumail docsAPI reference, SDK and tutorials.
- Free toolMail testerCheck SPF, DKIM, DMARC and spam signals on a real send.
Send your first email from code today.
3,000 emails a month free. Transactional and marketing email on one verified domain, with unlimited subscribers on every plan.