Migrate from EmailJS
Swap one npm package and keep your code — then get the parts EmailJS left out: templates with CC/BCC and Reply-To, variables in every field but To, and the same template sent from a browser or a backend.
Facts verified 2026-08-14 — corrections: hello@emailfast.dev
One import, same code
Migrating from EmailJS is an npm swap:
- import emailjs from "@emailjs/browser";
+ import emailjs from "@email-fast/browser";
emailjs.init({ publicKey: "ef_pub_..." });
await emailjs.send("default", "template_contact", {
name: "Ada",
message: "Hello from the contact form",
});sendForm works too, with the same signature:
document.querySelector("#contact").addEventListener("submit", (e) => {
e.preventDefault();
emailjs.sendForm("default", "template_contact", e.target);
});Under the hood, the endpoints /api/v1.0/email/send and /api/v1.0/email/send-form accept the EmailJS body — user_id, service_id, template_id, template_params — so even code you can't touch keeps working against a new base URL.
Templates carry the addressing — including CC and BCC
In EmailJS, a template holds the subject, the body, and the To / CC / BCC / Reply-To fields, with {{variables}} throughout. That model carries over with one deliberate exception — the To address here is a fixed, literal address chosen when the template is created, never a variable, so a public key in your page source can never steer where mail goes — and the CC and BCC fields you asked for are first-class:
- CC — visible copy recipients. Everyone on To and CC sees each other.
- BCC — hidden copy recipients. They receive the message but never appear in a header, so no recipient can see them.
- Reply-To — where replies go, independent of the From address.
- Variables in every field but To —
{{sales_email}}in the CC field,{{first_name}}in the subject,{{order_id}}in the body. Each value is filled from the send's data and each address is validated: a CC/BCC value that tries to carry a comma, a second address, or a header break is dropped, not split — a variable can add at most the one address its slot was written for.
One message goes out with a shared body and a single DKIM signature — CC and BCC ride along, they are not re-sent as separate emails.
Step by step: a template that CCs your team
In the dashboard
- Open Templates → New template.
- Fill the fields. A support-ticket notification might be: - To:
helpdesk@yourdomain.com— a fixed address, set here and only here; the browser can never choose the recipient - CC:support-leads@yourdomain.com, {{assigned_rep}}- BCC:archive@yourdomain.com- Reply-To:{{email}}(the visitor — a plain reply goes straight to them) - Subject:Ticket {{ticket_id}}: {{subject}}- Body: your HTML, with{{name}},{{ticket_id}}, and the rest. - Declare the variables the template accepts (name, email, ticket_id, …). Unknown keys are stripped and wrong types rejected, so a caller can't inject markup.
- Save. The template gets a slug (its
template_id) — that's all your code needs.
Want the visitor to get their own confirmation email? That's the per-template auto-reply — opted in and daily-capped — never a variable in the To field.
Or over the API
The same template, created with a secret key:
curl https://api.emailfast.dev/v1/templates \
-H "Authorization: Bearer ef_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "support_ack",
"to_email": "helpdesk@yourdomain.com",
"from_email": "support@yourdomain.com",
"subject": "Ticket {{ticket_id}}: {{subject}}",
"html": "<p>{{name}} wrote in — ticket {{ticket_id}} is open. Replying goes straight to them.</p>",
"cc": "support-leads@yourdomain.com, {{assigned_rep}}",
"bcc": "archive@yourdomain.com",
"reply_to_email": "{{email}}"
}'Send it — from the browser, or from your server
Browser (unchanged EmailJS call). The CC/BCC live in the template, so your front-end code doesn't change:
await emailjs.send("default", "support_ack", {
name: "Ada", email: "ada@example.com",
subject: "Login help", ticket_id: "T-4821", assigned_rep: "rep@yourdomain.com",
});Server (REST API). The part EmailJS never offered: send the same stored template from a backend, where a secret key chooses the recipient — the fixed To binds only the public browser lane. This is the path for app notifications, receipts, and workplace automation:
curl https://api.emailfast.dev/v1/emails \
-H "Authorization: Bearer ef_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "escalations@yourdomain.com",
"template": "support_ack",
"data": { "name": "Ada", "email": "ada@example.com", "subject": "Login help", "ticket_id": "T-4821", "assigned_rep": "rep@yourdomain.com" }
}'The template supplies the subject, body, CC, BCC, and Reply-To; data fills the variables; and any field you set on the request itself overrides the template's default for that one send.
CC and BCC per send, without a template
If you're sending ad-hoc rather than from a template, pass the recipients inline:
curl https://api.emailfast.dev/v1/emails \
-H "Authorization: Bearer ef_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "customer@example.com",
"cc": ["account-manager@yourdomain.com"],
"bcc": ["compliance@yourdomain.com"],
"reply_to": "account-manager@yourdomain.com",
"subject": "Your invoice",
"html": "<p>Attached is your invoice.</p>"
}'Every address is validated, unsubscribes and suppressions are honoured for CC and BCC recipients too, and the total recipient count is capped so a single call can't fan out into a blast.
The security upgrade
The reason to switch is not the code you keep; it is the abuse you stop. A key shipped in frontend source is public by definition, so the platform has to assume it is in hostile hands: a browser SDK with EmailJS-compatible endpoints — the recipient always comes from the server-stored template, never from the request, so a public key in your frontend can't be abused to spam arbitrary addresses.
Concretely, per key and template:
- Recipient from the template, always (browser SDK).
template_paramscannot redirect mail. A stolen public key sends your contact-form template to its own configured recipient — that's the ceiling. (A secret key, used only from your server, is trusted to choose the recipient — that's how the REST examples above setto.) - Origin allowlist. Each public key lists the origins allowed to use it; requests from anywhere else are refused.
- Typed variable schema. Each template declares what
template_paramsit accepts; unknown keys are stripped and wrong types rejected, so injected content can't smuggle markup — or an extra recipient — into your emails. - Rate caps. Per-key limits bound what a scripted abuser can burn.
- Visitor auto-reply, opted in and capped. If you want the submitter to get a confirmation, you enable it per template with a daily cap.
Try it against the sandbox
Attach the template to a sandbox project first: sandbox keys (ef_sandbox_…) that run the real pipeline dry: real validation, real rendering, real events, a hosted capture inbox — and no email leaves.
Next
Questions, answered plainly
Do I have to rewrite my EmailJS integration?
No. @email-fast/browser keeps the init, send, and sendForm signatures, and the endpoints accept the EmailJS request body — user_id, service_id, template_id, template_params — unchanged.
Can a template CC or BCC people, like EmailJS?
Yes. A template carries CC, BCC, and Reply-To fields, and every field except To — subject, body, CC, BCC, Reply-To — can contain {{variables}} filled from the send. The To address is fixed when the template is created: on the browser lane the recipient never comes from the request. CC recipients are visible to each other; BCC are hidden and never appear in a header.
Can my backend send the same templates, not just the browser?
Yes. The REST API takes { "to": "…", "template": "your_slug", "data": { … } }, so the same stored template that your contact form uses can also be sent server-side from your app — one template, two callers.
Can someone abuse the public key in my page source?
They can call it, but on the browser SDK the recipient always comes from the server-stored template — never from the request — so the worst they can do is send your own template to its configured recipient, inside your rate caps and only from allowlisted origins.