Next.js
Updated
Send emails from Next.js with Lumail
Install the lumail package, keep the API key in a server-only module, and send from a route handler or a server action. About ten minutes from zero to a delivered email.
TL;DR
npm install lumail server-only, set LUMAIL_API_KEY in .env.local, create a lib/lumail.ts module that starts with import "server-only", then call lumail.emails.send({ from, to, subject, html }) inside a route handler or a server action. The SDK returns { data, error } instead of throwing, so check error before you use data.id.Prerequisites
- A Next.js 14 or later project using the App Router (
app/directory). - A Lumail account and an API token that starts with
lum_(Settings > API Tokens). - A sending domain verified in the same Lumail organization as the token.
1. Install the packages
2. Set your environment variables
Put the token in .env.local. Next.js loads it on the server for next dev, and .env.local is git-ignored by default in new projects.
Do not prefix it with NEXT_PUBLIC_. That prefix inlines the value into the browser bundle, which would hand your sending key to every visitor. In production, add the same variable in your host's environment settings.
3. Create one server-only client
Create the client once and import it everywhere. import "server-only" makes the build fail if a client component ever imports this file, so the token cannot leak by accident.
The constructor needs a string, so the module checks the variable and fails loudly at startup instead of sending unauthenticated requests later.
4. Send from a route handler
Route handlers are the right place when something outside React calls you: a webhook, a cron job, or a mobile client. Response.json is the standard Web API, so no Next-specific helper is needed.
to takes one recipient per email. For many recipients, loop or use lumail.emails.batch (up to 100 per request).
5. Or send from a server action
Server actions fit forms inside your own app: invites, contact forms, resend-verification buttons. The action runs on the server, so it can import lib/lumail.ts directly.
With useActionState, the action receives the previous state first and the FormData second, and whatever it returns is shown back in the form.
6. Test without sending real email
Send to a fixture address such as [email protected] or anything ending in .test. Lumail runs the whole path, including domain checks, and returns an eml_ id without delivering a message or touching your reputation.
Then send one email to your own inbox and run it through the mail tester to confirm SPF, DKIM and DMARC pass.
Handle errors
lumail.emails.send never throws on an API error. It resolves to { data: null, error }, where error.name is a stable code and error.statusCode is the HTTP status. Network failures and timeouts come back the same way with network_error or timeout and no status.
Return a useful status to your caller, log error.name, and never let an email failure crash the request that triggered it, such as a signup.
| 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 does not retry POST requests, so a retry is your call. Pass an idempotencyKey as the second argument: repeating the same key returns the email that was already queued instead of sending a second one.
Derive the key from the thing that caused the email, such as the user id or the webhook event id, never from a random value generated per attempt.
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
lib/lumail.tsstarts withimport "server-only"and no file under a"use client"boundary imports it.LUMAIL_API_KEYis set in your host's production environment, not only in.env.local.- 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
Should I send email from a route handler or a server action?
Use a server action for forms and buttons inside your own Next.js app, because it needs no fetch call and returns state to the form. Use a route handler when something outside React calls you, such as a webhook, a cron job or a mobile app.
Can I call the Lumail SDK from a client component?
No. The API token can send email from your domain, so it must stay on the server. Call a server action or a route handler from the client, and keep the lumail import in a module marked server-only.
Does the Lumail SDK work on the Edge runtime?
The SDK only uses fetch, so it does not depend on Node APIs for sending. The default Node.js runtime for route handlers is the safest choice, and it is what this guide uses.
How do I send a React Email template from Next.js?
Render the component to an HTML string with render from @react-email/render, then pass that string as html. The React Email guide covers the template, plain text and preview server.
Why does my send fail with a domain error?
The from address must be on a domain verified in the same Lumail organization as your API token. Add the domain, publish its DNS records, wait for it to verify, then retry.
How much does sending from Next.js with Lumail cost?
The Free plan includes 3,000 emails a month. Premium is $20 a month for 40,000 emails, then $0.60 per 1,000. Transactional and marketing email share the same volume, and subscribers are unlimited on every plan.
Keep building
- GuideSend emails from Node.js with LumailA script, an Express route, batch sends and a safe retry helper.
- 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.
- DocsSend Email API referenceEvery field, option and error for POST /api/v2/emails.
- DocsTest-mode recipientsFixture addresses that never send real email.
- ProductIn-app integrationLet your coding agent wire Lumail into your codebase.
- 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.