Documentation
Neuctra Mailer docs
Send email from any JavaScript app through your own SMTP account, and collect subscribers with a welcome email, without writing an email backend.
Introduction
Neuctra Mailer sits between your app and an SMTP account you already own, such as Gmail, Outlook, Zoho or Amazon SES. Your app calls the @neuctra/mailer SDK, Neuctra Mailer checks the request and sends it through your SMTP server.
- Works in the browser, React, Next.js and Node.js 18 or later. The SDK has no dependencies.
- Each app gets its own service with its own SMTP login, API key and logs.
- Lists, templates and each list's welcome email are managed in the dashboard.
Quick start
1. Create a service
Sign in to the dashboard, click New service, and enter your SMTP host, port, username, password and From address. The service's Overview tab then shows four values: Server URL, Service ID, Service name and API key.
2. Install the SDK
npm install @neuctra/mailer3. Create the mailer once
1import { NeuctraMailer } from "@neuctra/mailer";
2
3export const mailer = new NeuctraMailer({
4 serverUrl: process.env.NEXT_PUBLIC_MAILER_URL,
5 serviceId: process.env.NEXT_PUBLIC_MAILER_SERVICE_ID,
6 serviceName: process.env.NEXT_PUBLIC_MAILER_SERVICE_NAME,
7 apiKey: process.env.NEXT_PUBLIC_MAILER_API_KEY,
8});4. Send your first email
1import { mailer } from "@/lib/mailer";
2
3await mailer.send({
4 to: "jane@example.com",
5 subject: "Welcome to Acme",
6 html: "<h1>Hi Jane</h1><p>Thanks for signing up.</p>",
7});The call resolves with the message ID once your SMTP server accepts the email. If something is wrong, the SDK throws an error that says what to fix. See Errors.
Configuration
Put the four values in environment variables. The names below are for Next.js, where the NEXT_PUBLIC_ prefix makes them available in the browser. Any names work.
1NEXT_PUBLIC_MAILER_URL=https://your-mailer-api.example.com
2NEXT_PUBLIC_MAILER_SERVICE_ID=your-service-id
3NEXT_PUBLIC_MAILER_SERVICE_NAME=your-service-name
4NEXT_PUBLIC_MAILER_API_KEY=your-api-keyLock the key to your website
new NeuctraMailer(config)
Creates a client for one service. Create it once and import it wherever you send email.
| Option | Type | Description |
|---|---|---|
serverUrl | string | URL of the Neuctra Mailer API. A trailing slash is removed. |
serviceId | string | From the service's Overview tab. |
serviceName | string | Must match the service's name. Case does not matter. |
apiKey | string | The public key (nm_...) in browser code, or the server key (nm_sk_...) on a server. Both are on the Overview tab. |
All four are required. If any is missing, the constructor throws a NeuctraMailerError with status 0 that names it.
mailer.send(mail)
Sends one email through the service's SMTP server.
| Field | Type | Description |
|---|---|---|
to | string | string[] | Required. One address, a comma-separated string, or an array. |
subject | string | Required. |
html | string | HTML body. Provide html, text, or both. |
text | string | Plain-text body. Provide html, text, or both. |
cc | string | string[] | Optional. |
bcc | string | string[] | Optional. |
replyTo | string | Optional. One address. |
captchaToken | string | Cloudflare Turnstile token. Required with the public key when the service's captcha is on. |
1const result = await mailer.send({
2 to: ["jane@example.com", "sam@example.com"], // or "a@x.com, b@y.com"
3 subject: "Order #1042 confirmed",
4 html: "<h1>Thanks for your order</h1>",
5 text: "Thanks for your order", // plain-text version, optional
6 replyTo: "support@acme.com",
7 bcc: "orders@acme.com",
8});
9
10// { success: true, messageId: "<...>", accepted: ["jane@example.com", ...], rejected: [] }The email can have at most 50 recipients across to, cc and bcc, and every address must be valid. The From address is the one saved on the service.
mailer.subscribe(options)
Adds someone to one of your email lists. If the list's welcome email is on, it is sent before the call returns, and welcomeSent in the result says whether it went out.
| Field | Type | Description |
|---|---|---|
listId | string | Required. The newsletter's id, from the Newsletters tab. Email lists can't be subscribed to. |
email | string | Required. |
name | string | Optional. Up to 100 characters. |
captchaToken | string | Cloudflare Turnstile token, when the service's captcha is on. |
1const { alreadySubscribed } = await mailer.subscribe({
2 listId: "cm_newsletter",
3 email: "jane@example.com",
4 name: "Jane",
5});Returns { success: true, alreadySubscribed }. When alreadySubscribed is true the address was already on the list, and no second welcome email is sent.
mailer.unsubscribe(options)
Removes someone from a list. The answer is the same whether or not the address was on the list, so the endpoint cannot be used to check who is subscribed. Every marketing email also carries its own unsubscribe link.
1await mailer.unsubscribe({ listId: "cm_newsletter", email: "jane@example.com" });
2// { success: true }mailer.verify()
Checks your credentials and logs in to the service's SMTP server without sending anything. Call it once while setting up, or in a health check.
1const { service } = await mailer.verify();
2console.log(`Connected to ${service.name}`);Errors
Every method returns a promise. On failure it throws a NeuctraMailerError with a readable message from the server and the HTTP status.
1import { NeuctraMailerError } from "@neuctra/mailer";
2
3try {
4 await mailer.send({ to, subject, html });
5} catch (err) {
6 if (err instanceof NeuctraMailerError) {
7 console.error(err.status, err.message);
8 }
9}| Status | Meaning |
|---|---|
0 | The API could not be reached, or the config is missing a value. Check serverUrl. |
400 | Something is wrong with the request: no recipient or subject, no body, an invalid address, or more than 50 recipients. |
401 | Wrong apiKey or serviceId, or serviceName does not match the service. |
403 | The service is turned off, the website isn't allowed, the recipient isn't allowed for the public key, the captcha failed, or a server key was used from a browser. |
404 | The list does not exist or belongs to another service. |
429 | Too many emails: the service's per-minute or per-day rate limit (with a Retry-After header), or your plan's monthly emails or contacts. The message says which. |
502 | The SMTP server refused. The message says why, for example a wrong password or port. |
Contact form
A React form that emails you each message. replyTo is set to the visitor's address, so you can answer straight from your inbox.
1"use client";
2
3import { useState } from "react";
4import { mailer } from "@/lib/mailer";
5
6export function ContactForm() {
7 const [status, setStatus] = useState("idle");
8
9 async function handleSubmit(event) {
10 event.preventDefault();
11 const form = new FormData(event.currentTarget);
12 setStatus("sending");
13 try {
14 await mailer.send({
15 to: "hello@acme.com",
16 replyTo: form.get("email"),
17 subject: `New message from ${form.get("name")}`,
18 text: form.get("message"),
19 });
20 setStatus("sent");
21 } catch {
22 setStatus("error");
23 }
24 }
25
26 return (
27 <form onSubmit={handleSubmit}>
28 <input name="name" required />
29 <input name="email" type="email" required />
30 <textarea name="message" required />
31 <button disabled={status === "sending"}>Send</button>
32 {status === "sent" && <p>Thanks, we will reply soon.</p>}
33 {status === "error" && <p>Something went wrong. Please try again.</p>}
34 </form>
35 );
36}Sending from a server
The SDK also runs on the server: Next.js route handlers, server actions, or any Node.js 18+ code. The API key then stays out of the browser.
1// app/api/order-confirmation/route.js
2import { NeuctraMailer } from "@neuctra/mailer";
3
4// No NEXT_PUBLIC_ prefix, so the key never reaches the browser.
5const mailer = new NeuctraMailer({
6 serverUrl: process.env.MAILER_URL,
7 serviceId: process.env.MAILER_SERVICE_ID,
8 serviceName: process.env.MAILER_SERVICE_NAME,
9 apiKey: process.env.MAILER_API_KEY,
10});
11
12export async function POST(request) {
13 const { email, orderId } = await request.json();
14 const { messageId } = await mailer.send({
15 to: email,
16 subject: `Order #${orderId} confirmed`,
17 html: `<p>Order #${orderId} is on its way.</p>`,
18 });
19 return Response.json({ messageId });
20}Use the server key on servers
MAILER_API_KEY. It isn't limited by allowed websites, allowed recipients or the captcha, so it can send order emails to any customer. It is refused from a browser, and the SDK won't start with it in browser code.Templates and sending
Templates, welcome emails and one-off sends are managed in the dashboard. Your code only adds and removes subscribers.
- Templates. Build emails from blocks (headings, text, buttons, images, dividers) or paste your own HTML. Start from a ready-made design and preview on desktop and mobile.
- Welcome email. Choose a template for a list and turn it on. Everyone who subscribes from your app gets it immediately. Contacts you add in the dashboard don't.
- Send tab. Pick a list (or import emails from a paste or a CSV file), then send to one person with the row's Send button or tick several and send to all of them. Each contact shows Not sent, Sent or Failed for that template and remembers it, so you can finish later.
- Every welcome and Send tab email includes an unsubscribe link.
- Do-not-send list. Each service keeps one. People who unsubscribe, or whose address bounces, are added by themselves and never get Send tab or welcome emails again, from any list. You can add or remove addresses in the Newsletters tab. SDK emails aren't blocked, since those are usually receipts and password resets.
- Warm-up. For a new service's first 3 weeks, the Send tab sends at most 50 emails a day, rising to 100, 250 and 500, so mail providers learn to trust a new sender. Turn it off under Settings, Security.
Read more in the email template builder guide.
REST API
The SDK is a thin wrapper over four HTTP endpoints, so any language can call them. Send the service's credentials as headers and a JSON body.
| Endpoint | Body |
|---|---|
POST /api/mail/send | { to, subject, html?, text?, cc?, bcc?, replyTo? } |
POST /api/mail/subscribe | { listId, email, name? } |
POST /api/mail/unsubscribe | { listId, email } |
GET /api/mail/verify | No body. |
| Header | Value |
|---|---|
x-api-key | The service's API key. |
x-service-id | The service ID. |
x-service-name | The service name. |
Content-Type | application/json |
1curl -X POST "$MAILER_URL/api/mail/send" \
2 -H "Content-Type: application/json" \
3 -H "x-api-key: $MAILER_API_KEY" \
4 -H "x-service-id: $MAILER_SERVICE_ID" \
5 -H "x-service-name: $MAILER_SERVICE_NAME" \
6 -d '{"to":"jane@example.com","subject":"Hello","text":"Sent with curl"}'Errors come back as { "error": "message" } with the status codes listed under Errors.
SMTP providers
Any SMTP account works. These are the settings for common providers.
| Host | Port | Notes |
|---|---|---|
smtp.gmail.com | 465 | Gmail and Google Workspace. Use an App Password, not your normal password. |
smtp.office365.com | 587 | Outlook and Microsoft 365. |
smtp.zoho.com | 465 | Zoho Mail. |
email-smtp.us-east-1.amazonaws.com | 587 | Amazon SES. Use your region's host and SMTP credentials. |
smtp-relay.brevo.com | 587 | Brevo. |
smtp.mailgun.org | 587 | Mailgun. |
Port 465 uses SSL from the start, so turn on the SSL switch. Ports 587 and 25 start plain and upgrade to TLS, so leave it off. Click Check connection on the service's SMTP tab to test the settings.
Security
Anyone can read a key that runs in a browser, and a script can fake the website a request claims to come from. So each service has two keys and a few limits, set under Settings, Security.
| Key | Prefix | Use |
|---|---|---|
Public key | nm_ | Browser code. Limited by allowed websites, allowed recipients, the captcha and rate limits. |
Server key | nm_sk_ | Your server only. Rate limited, can email anyone, and refused from a browser. |
- Allowed recipients. For a contact form, list only your own address. A copied public key then can't email anyone else.
- Rate limits. Emails per minute and per day for each service (defaults 30 and 500), counting every recipient. Keep the daily number at or under your SMTP provider's limit.
- Captcha. Turn on Cloudflare Turnstile, add its secret key, put the widget in your form, and pass its token as
captchaTokentosendorsubscribe. - Allowed websites. List the sites that may use the public key, such as
https://acme.com. This stops other websites, but not scripts, which is why the limits above matter. - Rotate the key. Click Rotate key in the service's settings to get a new API key. The old key stops working immediately.
- Turn a service off. Switch off Accepting emails in the service's settings, and every request is refused until you switch it back on.
- SMTP passwords and API keys are stored encrypted with AES-256-GCM.
Limits
| Limit | Value |
|---|---|
Recipients per email | 50 across to, cc and bcc |
Request body | 1 MB |
Contacts added at once | 100 per request in the dashboard |
Template | 40 blocks |
Your plan also sets how many emails you can send each month, how many subscribed contacts and services you can have, how many newsletters, email lists and templates each service can have, and how many contacts each service can send to per day from the dashboard's Send tab. Each recipient counts as one email. Emails sent with the SDK only count toward the monthly emails. See pricing for each plan's limits, and Plan & usage in the dashboard for what you have used.
Your SMTP provider has its own daily sending limits. Gmail, for example, limits how many emails an account can send per day.
MCP server for AI tools
The Neuctra Mailer MCP server gives AI assistants such as Claude Code, Claude Desktop, Cursor and VS Code the SDK reference, so they write correct Neuctra Mailer code. With a service's credentials it can also verify the service, send email and manage list subscribers.
claude mcp add neuctra-mailer -- npx -y @neuctra/mailer-mcpFor other clients, add it to the MCP config. Leave out env if you only want the docs tools.
1{
2 "mcpServers": {
3 "neuctra-mailer": {
4 "command": "npx",
5 "args": ["-y", "@neuctra/mailer-mcp"],
6 "env": {
7 "NEUCTRA_MAILER_SERVER_URL": "https://your-mailer-api.example.com",
8 "NEUCTRA_MAILER_SERVICE_ID": "your-service-id",
9 "NEUCTRA_MAILER_SERVICE_NAME": "your-service-name",
10 "NEUCTRA_MAILER_API_KEY": "your-api-key"
11 }
12 }
13 }
14}