INTEGRATIONS · Published 2026-10-06 · Updated 2026-10-06 · SecureSMTP
Node.js
A small send function with fetch — no package to install.
There is no SecureSMTP package to install. Node.js 18 and later has fetch built in, so a short function is all you need. You can also send over SMTP with Nodemailer.
Setup
Every request needs your site’s API key. Get it when you add the site under Sites (/app/forms/sites) in the dashboard — see Sites and API keys. Keep it in an environment variable on the server:
export SECURESSMTP_API_KEY="qcs_live_..."A send function
Save this as securessmtp.js. It sends one email and throws if the API does not answer "ok": true.
// securessmtp.js (Node.js 18 or later)
const API_URL = 'https://securessmtp.com/api/v1/mail/send';
export class SecureSMTPError extends Error {
constructor(reason, status, result) {
super(`SecureSMTP: ${reason} (HTTP ${status})`);
this.name = 'SecureSMTPError';
this.reason = reason;
this.status = status;
this.result = result;
}
}
export async function sendEmail(message) {
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),
});
// The API always answers JSON with an "ok" field. A body that is not JSON
// (for example a 413 from the web server) is treated as a failure too.
const result = await res.json().catch(() => ({ ok: false }));
if (!result.ok) {
throw new SecureSMTPError(result.reason ?? result.error ?? 'request_failed', res.status, result);
}
return result; // { ok: true, sent: true, mode: 'relay' | 'domain', message_id: '<...>' }
}Use it anywhere on the server:
import { sendEmail } from './securessmtp.js';
const result = await sendEmail({
to: '[email protected]',
subject: 'Your order has shipped',
html: '<p>Your order is on its way.</p>',
text: 'Your order is on its way.',
from: { name: 'Acme Store' },
reply_to: '[email protected]',
});
console.log(result.message_id);from is an object; only name is used. Pass html, text or both — if you pass one, the other is made from it. Every field is listed in the API reference.
Attachments
Read the file and send its content as base64. Up to 20 files and 15 MB in total per email.
import { readFile } from 'node:fs/promises';
import { sendEmail } from './securessmtp.js';
const pdf = await readFile('./invoice-1042.pdf');
await sendEmail({
to: '[email protected]',
subject: 'Invoice 1042',
text: 'Your invoice is attached.',
from: { name: 'Acme Store' },
attachments: [
{
filename: 'invoice-1042.pdf',
content: pdf.toString('base64'),
content_type: 'application/pdf',
},
],
});Handle errors
import { sendEmail, SecureSMTPError } from './securessmtp.js';
try {
await sendEmail({ to: '[email protected]', subject: 'Hello', text: 'Hi there.' });
} catch (err) {
if (err instanceof SecureSMTPError) {
switch (err.reason) {
case 'rate_limited':
// More than 120 sends from this site in 60 seconds. Wait a minute, then retry.
break;
case 'over_quota':
// The account's monthly email count is used up.
break;
case 'send_failed':
console.error('Not sent:', err.result.error);
break;
default:
// 400/401/403: fix the request or the key. Retrying will not help.
console.error(err.message, err.result.details ?? '');
}
} else {
// Network error or timeout. The email may or may not have been sent.
console.error(err);
}
}| Response | Meaning | What to do |
|---|---|---|
200 ok: true | Accepted for sending. | Nothing. Keep message_id if you want to find the email later. |
200 rate_limited | More than 120 sends from this site in 60 seconds. | Wait a minute, then try again. |
200 over_quota | The 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_failed | The 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_flagged | The content was judged to be spam. | Change the content. See Deliverability. |
401 missing_api_key, invalid_api_key | No key was sent, or the key is wrong or was rotated. | Check the environment variable on the server. |
403 site_disabled | The site is disabled. | See Blocks. |
- Check
okin 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.
Or send over SMTP with Nodemailer
If your app already uses Nodemailer, point it at SecureSMTP. Install it with npm install nodemailer.
| Setting | Value |
|---|---|
| Host | smtp.securessmtp.com |
| Port | 587 with STARTTLS, or 465 with SSL/TLS |
| Username | securessmtp |
| Password | Your site’s API key (qcs_live_...) |
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: 'smtp.securessmtp.com',
port: 587,
secure: false, // STARTTLS on 587. For port 465 use: port: 465, secure: true
requireTLS: true,
auth: {
user: 'securessmtp',
pass: process.env.SECURESSMTP_API_KEY,
},
});
const info = await transporter.sendMail({
from: '"Acme Store" <[email protected]>',
to: '[email protected]',
subject: 'Your order has shipped',
text: 'Your order is on its way.',
html: '<p>Your order is on its way.</p>',
attachments: [{ filename: 'invoice-1042.pdf', path: './invoice-1042.pdf' }],
});
console.log(info.response);[email protected] with your From name, and the From address you set becomes the Reply-To. Verify your domain and the email goes out from your own address. See Domains and DNS.When the server refuses a message, Nodemailer throws with the SMTP reply code:
try {
await transporter.sendMail(message);
} catch (err) {
// err.responseCode is the SMTP reply code, err.response the full reply text.
if (err.responseCode >= 400 && err.responseCode < 500) {
// Temporary (e.g. 450 rate limit). Retry in a minute.
} else if (err.responseCode === 535) {
// Wrong API key in the password.
}
console.error(err.responseCode, err.response ?? err.message);
}| SMTP reply | Meaning |
|---|---|
250 | Accepted. |
535 | Login failed: the password is not a valid API key. |
450 4.7.0 | Rate limit: more than 120 sends from this site in 60 seconds. Retry later. |
550 5.7.0 | Monthly email limit reached. |
550 5.7.1 | Rejected as spam, the site is disabled, or every recipient has unsubscribed. |
550 5.1.1 | Every recipient is on the suppression list. |
552 5.3.4 | Attachments are over 15 MB in total. |
4xx 4.3.0 | Temporary failure. Retry later. |
Next
- Next.js and Express examples: Next.js, Express.
- Every field and response: POST /mail/send.
- SMTP details: SMTP.