INTEGRATIONS · Published 2026-10-06 · Updated 2026-10-06 · SecureSMTP

Next.js

Send from a route handler and from a Server Action.

Send from a route handler or a Server Action. Both run on the server, so the API key never reaches the browser. These examples use the App Router (Next.js 14 and 15).

Setup

Add the key to .env.local (and to your host’s environment variables in production). Do not prefix it with NEXT_PUBLIC_ — that would put it in the browser bundle.

.env.local
SECURESSMTP_API_KEY=qcs_live_...

Then add a small send function. Import it only from server code: route handlers, Server Actions and server components.

lib/securessmtp.ts
// lib/securessmtp.ts — import this only from server code
const API_URL = 'https://securessmtp.com/api/v1/mail/send';

export type EmailMessage = {
  to: string | string[];
  subject: string;
  html?: string;
  text?: string;
  cc?: string | string[];
  bcc?: string | string[];
  reply_to?: string | string[];
  from?: { name: string };
  source_plugin?: string;
};

export type SendResult = {
  ok: boolean;
  sent?: boolean;
  held?: boolean;
  mode?: 'relay' | 'domain';
  message_id?: string;
  reason?: string;
  error?: string;
};

export class SecureSMTPError extends Error {
  readonly reason: string;
  readonly status: number;

  constructor(reason: string, status: number) {
    super(`SecureSMTP: ${reason} (HTTP ${status})`);
    this.name = 'SecureSMTPError';
    this.reason = reason;
    this.status = status;
  }
}

export async function sendEmail(message: EmailMessage): Promise<SendResult> {
  const apiKey = process.env.SECURESSMTP_API_KEY;
  if (!apiKey) throw new Error('SECURESSMTP_API_KEY is not set');

  const res = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'x-securessmtp-api-key': apiKey,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(message),
    signal: AbortSignal.timeout(30_000),
  });

  const result = (await res.json().catch(() => ({ ok: false }))) as SendResult;
  if (!result.ok) {
    throw new SecureSMTPError(result.reason ?? result.error ?? 'request_failed', res.status);
  }
  return result;
}

Route handler

A contact endpoint that the browser can post JSON to:

app/api/contact/route.ts
// app/api/contact/route.ts
import { NextResponse } from 'next/server';
import { sendEmail } from '@/lib/securessmtp';

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);
  const name = String(body?.name ?? '').trim();
  const email = String(body?.email ?? '').trim();
  const message = String(body?.message ?? '').trim();

  if (!name || name.length > 100 || !EMAIL_RE.test(email) || !message || message.length > 5000) {
    return NextResponse.json({ ok: false, error: 'Please fill in every field.' }, { status: 422 });
  }

  try {
    await sendEmail({
      to: '[email protected]',
      subject: `Contact form: ${name}`,
      text: `Name: ${name}\nEmail: ${email}\n\n${message}`,
      from: { name: 'Website contact form' },
      reply_to: email,
      source_plugin: 'nextjs-contact',
    });
    return NextResponse.json({ ok: true });
  } catch (err) {
    console.error('Contact email failed:', err);
    return NextResponse.json({ ok: false, error: 'Could not send your message.' }, { status: 502 });
  }
}

Server Action

The same thing with a Server Action and a form, without writing an API route:

// app/contact/actions.ts
'use server';

import { sendEmail } from '@/lib/securessmtp';

export type ContactState = { status: 'idle' | 'sent' | 'error'; message?: string };

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export async function sendContact(_prev: ContactState, formData: FormData): Promise<ContactState> {
  const name = String(formData.get('name') ?? '').trim();
  const email = String(formData.get('email') ?? '').trim();
  const message = String(formData.get('message') ?? '').trim();

  if (!name || !EMAIL_RE.test(email) || !message) {
    return { status: 'error', message: 'Please fill in every field.' };
  }

  try {
    await sendEmail({
      to: '[email protected]',
      subject: `Contact form: ${name}`,
      text: `Name: ${name}\nEmail: ${email}\n\n${message}`,
      from: { name: 'Website contact form' },
      reply_to: email,
    });
    return { status: 'sent', message: 'Thanks. Your message was sent.' };
  } catch (err) {
    console.error('Contact email failed:', err);
    return { status: 'error', message: 'Could not send your message. Please try again later.' };
  }
}

useActionState is in React 19, which Next.js 15 uses. On Next.js 14 use useFormState from react-dom instead.

Handle errors

sendEmail throws a SecureSMTPError with the reason from the API. Log it on the server and show the visitor a general message.

ResponseMeaningWhat to do
200 ok: trueAccepted for sending.Nothing. Keep message_id if you want to find the email later.
200 rate_limitedMore than 120 sends from this site in 60 seconds.Wait a minute, then try again.
200 over_quotaThe account’s monthly email count is used up (relay mode only).Wait for the new month, upgrade, or send from a verified domain.
200 send_failedThe email was not sent. The error field says why.Read error. Do not retry in a loop.
400 invalid_payload, no_body, …The request is wrong. details lists the fields for invalid_payload.Fix the request. Retrying the same request fails the same way.
400 content_flagged, ai_flaggedThe content was judged to be spam.Change the content. See Deliverability.
401 missing_api_key, invalid_api_keyNo key was sent, or the key is wrong or was rotated.Check the environment variable on the server.
403 site_disabledThe site is disabled.See Blocks.
  • Check ok in the body, not only the HTTP status. Rate limit, monthly limit and send failures come back with HTTP 200.
  • There is no idempotency key. If a request times out, the email may already have been sent, and sending it again can deliver it twice.
  • Every reason is listed in the error catalogue.
Subjects like “Contact form: …” are treated as contact-form notifications. If one is judged to be spam it is held: the API answers "ok": true, "held": true and the email is not sent. It shows as flagged in the email log.

Or use SMTP

If you already send with Nodemailer, change its settings instead — see Node.js. Nodemailer needs the Node.js runtime, not the Edge runtime.

Next