Skip to content

Node.js (@frontmail/node)

@frontmail/node is for servers, serverless functions and scripts. It authenticates with a private key, so origin and Turnstile checks don’t apply, and it unlocks batch sending, history and template schemas.

Terminal window
npm install @frontmail/node

Requires Node.js 20+ (uses the built-in fetch). Also works in Bun, Deno and edge runtimes that provide fetch.

import { Frontmail } from '@frontmail/node';
const frontmail = new Frontmail({
privateKey: process.env.FRONTMAIL_PRIVATE_KEY!, // sk_…
// retry: { retries: 3 }, timeoutMs: 15_000, apiUrl: 'https://api.frontmail.dev'
});

Without privateKey, the client reads FRONTMAIL_PRIVATE_KEY (and FRONTMAIL_API_URL) from the environment. Never expose the private key to a browser bundle – keep it in environment variables or a secret manager. See Public and private keys.

The package refuses to run in a web browser: importing it there logs a warning and new Frontmail() throws private_key_in_browser (opt out with dangerouslyAllowPrivateKeyInBrowser: true only for internal tools on trusted machines). In the browser use @frontmail/browser with the public key.

const result = await frontmail.send({
serviceId: 'svc_01J9…', // optional – omit to use the default service
templateId: 'tpl_order',
params: {
order_id: 'A-1042',
items: [{ title: 'Room', qty: 2, price: 89 }],
total: 178,
currency: 'EUR',
},
});
console.log(result.messageId, result.status); // 'queued' | 'held'

Other input fields: idempotencyKey (recommended when the send is triggered by something that can repeat, e.g. a webhook – use its event ID), attachments, signal.

Send up to 100 messages in one request. Each message is validated and billed on its own – one invalid message doesn’t fail the others. sendBatch doesn’t throw for per-message errors; check ok on each result.

const results = await frontmail.sendBatch([
{ serviceId: 'svc_01J9…', templateId: 'tpl_reminder', params: { name: 'Jana' }, idempotencyKey: 'reminder-42-jana' },
{ serviceId: 'svc_01J9…', templateId: 'tpl_reminder', params: { name: 'Petr' }, idempotencyKey: 'reminder-42-petr' },
]);
for (const r of results) {
if (!r.ok) console.error(r.index, r.error.code);
else console.log(r.index, r.messageId, r.status);
}

A batch counts as one request for rate limits. For more than 100 messages, split them into chunks.

const page = await frontmail.history({
status: 'failed',
templateId: 'tpl_order',
from: '2026-09-01T00:00:00Z',
limit: 50,
});
for (const m of page.items) console.log(m.messageId, m.status, m.to, m.subject);
// Next page
if (page.nextCursor) await frontmail.history({ cursor: page.nextCursor });
// Or iterate over everything
for await (const m of frontmail.history({ status: 'bounced' })) console.log(m.to);

Filters: status, templateId, serviceId, from, to, q (recipient search), limit (≤ 100), cursor. Awaiting history() returns one page; for await walks through all pages lazily. History covers your plan’s retention period.

const msg = await frontmail.getMessage('msg_01J9Z3K7Q2');
// { messageId, status, createdAt, sentAt, templateId, serviceId, events: [{ type, at }, …] }
const templates = await frontmail.templates.list(); // param schemas, used by `frontmail types`
const contact = await frontmail.templates.get('tpl_contact');

The package includes the frontmail CLI – see Typed params.

Example project: examples/node.