Skip to content

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.

Terminal window
npm install @frontmail/browser
import { init, send, sendForm, getStatus } from '@frontmail/browser';

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' }
  • serviceId may be null to use the organization’s default service.
  • params are validated against the template’s schema on the server.
  • options accepts turnstileToken, attachments, idempotencyKey, signal (AbortSignal), per-call publicKey and 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'] }
});
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; its integrity value is in https://cdn.frontmail.dev/v<version>/sri.json.
  • Upgrade by changing the version and the hash together (see the changelog).

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), Turnstile
frame-src https://challenges.cloudflare.com; # Turnstile