Vanilla JavaScript (@frontmail/browser)
@frontmail/browser is a tiny (< 3.5 kB gzipped), dependency-free SDK for any website. Its API
mirrors @emailjs/browser, so existing code needs few changes.
Install
Section titled “Install”npm install @frontmail/browserimport { init, send, sendForm, getStatus } from '@frontmail/browser';<script src="https://cdn.jsdelivr.net/npm/@frontmail/browser@0.1.0/dist/frontmail.umd.js" integrity="sha384-g2Rxznkz2yb/YYSdVTkXLxZFZ2Nh2+XdhPStxh0stydXFF3gYjDuyHimWIZLJDCc" crossorigin="anonymous"></script><script> frontmail.init({ publicKey: 'pk_4f2a…' });</script>The UMD build exposes a global frontmail object with init, send, sendForm, getStatus
and the error classes. Always load an exact version with Subresource Integrity – see
Pin the version below.
init(options)
Section titled “init(options)”Call once, before sending:
init({ publicKey: 'pk_4f2a…', blockHeadless: true, // optional, see Bot protection blockList: { list: ['spam@example.net'], watchVariable: 'email' }, limitRate: { id: 'contact', throttle: 10_000 }, // at most one send per 10 s retry: { retries: 3, baseDelayMs: 300 }, // default; `false` disables retries timeoutMs: 15_000, // per attempt apiUrl: 'https://api.frontmail.dev', // default});init('pk_4f2a…') with just the key string also works.
send(serviceId, templateId, params?, options?)
Section titled “send(serviceId, templateId, params?, options?)”const result = await send('svc_01J9…', 'tpl_order', { name: 'Jana', items: [{ title: 'Room', qty: 2, price: 89 }],});// { messageId: 'msg_…', status: 'queued' | 'held', statusToken: '…', status_code: 202, text: 'OK' }serviceIdmay benullto use the organization’s default service.paramsare validated against the template’s schema on the server.optionsacceptsturnstileToken,attachments,idempotencyKey,signal(AbortSignal), per-callpublicKeyand per-call guards (blockHeadless,blockList,limitRate). A plain string is treated as the public key, like in EmailJS.
The promise resolves for both queued and held – a held message is accepted and will be
sent once credits are available (see Hold queue). It rejects with a
typed error otherwise (see Error handling).
sendForm(serviceId, templateId, form, options?)
Section titled “sendForm(serviceId, templateId, form, options?)”<form id="contact"> <input name="name" required> <input name="email" type="email" required> <textarea name="message"></textarea> <input type="file" name="attachment"> <div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY"></div> <button>Send</button></form><script type="module"> import { init, sendForm } from '@frontmail/browser'; init({ publicKey: 'pk_4f2a…' });
document.getElementById('contact').addEventListener('submit', async (event) => { event.preventDefault(); const { status } = await sendForm('svc_01J9…', 'tpl_contact', event.target); // or: sendForm('svc_01J9…', 'tpl_contact', '#contact') });</script>The form is sent as multipart/form-data. Text fields become params (coerced to the declared
types), file inputs become attachments, and a
Turnstile token in cf-turnstile-response is picked up automatically. Use the site key of your own
Turnstile widget – see Cloudflare Turnstile.
Fields that must not leave your site are skipped: password inputs (type="password"), the
anti-CSRF fields of common frameworks (csrfmiddlewaretoken, _token, authenticity_token,
__RequestVerificationToken, _csrf, csrf_token) and reserved API names (accessToken,
privateKey, turnstile_key, …). Choose the fields yourself with formFields:
await sendForm('svc_01J9…', 'tpl_contact', '#contact', { formFields: { include: ['name', 'email', 'message'] }, // only these (+ Turnstile token) // or: formFields: { exclude: ['internal_note'] }});getStatus(messageId, options)
Section titled “getStatus(messageId, options)”const status = await getStatus(result.messageId, { token: result.statusToken });// { messageId, status: 'sent', createdAt, sentAt, templateId, serviceId, events: [...] }With a public key you can only read messages you sent, using the statusToken from the send
result. The SDK sends it in the X-Frontmail-Status-Token header (not in the URL, so it doesn’t end
up in logs). See Message statuses.
Pin the version and use Subresource Integrity
Section titled “Pin the version and use Subresource Integrity”A <script> tag runs with full access to your page. Load an exact version and let the browser
verify its hash, so a changed file (a compromised CDN or release) is refused instead of executed:
<script src="https://cdn.jsdelivr.net/npm/@frontmail/browser@0.1.0/dist/frontmail.umd.js" integrity="sha384-g2Rxznkz2yb/YYSdVTkXLxZFZ2Nh2+XdhPStxh0stydXFF3gYjDuyHimWIZLJDCc" crossorigin="anonymous"></script>- jsDelivr (npm): the hash of every release is in its GitHub release notes (and jsDelivr’s “Copy HTML + SRI” button).
- Frontmail CDN:
https://cdn.frontmail.dev/v<version>/frontmail.umd.js; itsintegrityvalue is inhttps://cdn.frontmail.dev/v<version>/sri.json. - Upgrade by changing the version and the hash together (see the changelog).
Content Security Policy
Section titled “Content Security Policy”If your site uses CSP, allow:
connect-src https://api.frontmail.dev;script-src https://cdn.jsdelivr.net https://challenges.cloudflare.com; # CDN build (or cdn.frontmail.dev), Turnstileframe-src https://challenges.cloudflare.com; # Turnstile