# An EmailJS alternative that keeps keys off the client

> Move a contact form off EmailJS: why browser-side email keys are a risk, what changes in your code, and what you give up, such as the auto-reply.

Published 2026-10-25 · https://ikiform.com/guides/send-form-to-email/emailjs-alternative

EmailJS sends email from the browser, so your service id, template id and public key ship in your page's JavaScript for anyone to copy. A form backend needs no key in the browser: the form posts to a URL, and the backend filters spam, stores the message and emails you. You lose EmailJS's auto-reply to the visitor, which you'd send from your own email tool instead.

## What's different here

EmailJS is popular for a good reason: it sends real email from a static site with no server. To do that, the browser calls EmailJS directly with three values: a service id, a template id and a public key. They're in your bundle, readable in the page source or the network tab.

EmailJS's own FAQ says someone who copies them can only send your templates with your content, not arbitrary email. That holds for the template's fixed text. But the caller fills in the template's variables, so the copied keys can send your template with any name, message and links in it, as often as your quota allows. And if a template's **To** is a variable, as an auto-reply to the visitor's address is, the caller also picks the recipient.

Diagram: Who can send through a browser email key, and through a form endpoint

1. With a browser email SDK, the page holds everything needed to send. A script elsewhere can copy the ids and key and send your template with its own text, up to your quota.
2. If the template's recipient comes from a variable, the copied keys also choose who receives it.
3. With a form endpoint, the page holds only a form id. Posts are filtered for spam and rate limited, and email goes only to the recipients you set in the dashboard.

A form id isn't a secret either. The difference is what a stranger can do with it: submit your form, not send your email.

EmailJS has settings that narrow this, such as limiting which domains may use your key and adding a captcha to a template. Check which ones your plan includes and turn them on if you stay. The comparison below is about the design, not a verdict on the service:

|   | EmailJS | Ikiform |
| --- | --- | --- |
| What the browser holds | Service id, template id, public key | A form id |
| Who decides the recipient | The template, which may read a variable | Only your dashboard settings |
| Spam filtering | Captcha per template, if you add it | Honeypot, spam scoring and rate limits on every form, plus an optional human check |
| Your messages afterwards | In your inbox | In the dashboard, CSV export and the API, as well as your inbox |
| Email to the visitor | Yes, with an auto-reply template | No |
| Your own email design | Yes, templates in their editor | No, a fixed layout with the subject `New submission: <form name>` |

If the auto-reply or a branded email is the point, EmailJS, or your own server with an email API, fits better. If you want the message in your inbox, safely, and kept somewhere, a form backend fits.

## Move the form

A typical EmailJS form in React, based on their contact form tutorial:

`ContactForm.tsx, before`:

```tsx
import emailjs from "@emailjs/browser";
import { type FormEvent, useRef, useState } from "react";

export function ContactForm() {
  const form = useRef<HTMLFormElement>(null);
  const [status, setStatus] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!form.current) return;
    try {
      await emailjs.sendForm("service_abc123", "template_xyz789", form.current, {
        publicKey: "aBcDeFgHiJkLmNoPq",
      });
      setStatus("Message sent.");
    } catch {
      setStatus("Couldn't send your message.");
    }
  }

  return (
    <form ref={form} onSubmit={onSubmit}>
      <input name="user_name" required />
      <input name="user_email" type="email" required />
      <textarea name="message" required />
      <button>Send</button>
      <p role="status">{status}</p>
    </form>
  );
}
```

The same form posting to Ikiform with `submit()` from `@ikiform/sdk`:

`ContactForm.tsx, after`:

```tsx
import { submit } from "@ikiform/sdk";
import { type FormEvent, useState } from "react";

const FORM_ID = "frm_xxxxxxxxxxxxxxxx";

export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
  const [fields, setFields] = useState<Record<string, string[]>>({});
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus("sending");
    setFields({});
    setError("");
    const result = await submit(FORM_ID, new FormData(event.currentTarget));
    if (result.ok) {
      setStatus("sent");
      return;
    }
    setFields(result.error.fields ?? {});
    setError(result.error.code === "network_error" ? "You're offline. Try again." : result.error.message);
    setStatus("idle");
  }

  if (status === "sent") return <p role="status">Message sent. We'll reply by email.</p>;

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="user_name">Name</label>
      <input id="user_name" name="user_name" required />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      {fields.email ? <p role="alert">{fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" required />
      {fields.message ? <p role="alert">{fields.message[0]}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error && !Object.keys(fields).length ? <p role="alert">{error}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send"}</button>
    </form>
  );
}
```

- `@emailjs/browser` and the three ids go. Uninstall the package, and delete the EmailJS template or service once the new form works, so cached copies of your page stop sending.
- `user_email` becomes `email`. EmailJS doesn't care about names, but Ikiform uses the field called `email` as the reply-to. The other names can stay, because notifications list fields as they arrive.
- `submit()` never throws. It returns the error envelope, so field errors from the form's definition show under their input.
- The honeypot is new. EmailJS had none; this hidden input catches the simplest bots with no setup.
- Add your domain under **Allowed domains**, so posts from other sites are marked as spam.

> Using the human check? Set the form's **Spam check** to **Ikiform**, then call `challenge(FORM_ID)` before `submit` and pass the token as `turnstileToken`. The [React guide](https://ikiform.com/docs/guides/react.md) shows it.

## If you used the auto-reply

Ikiform doesn't email the person who fills in the form. Notification emails go to you and your team only, so there is no autoresponder, confirmation email or double opt-in. Send those from your email tool, triggered by a [webhook](https://ikiform.com/docs/webhooks.md). A sending service you control from a server, such as Resend or Postmark, can send it on each `submission.created` webhook, with the visitor's address from the trusted payload rather than from the browser. The [multiple recipients guide](https://ikiform.com/guides/send-form-to-email/multiple-recipients.md#route) has a handler you can adapt.

Ask whether you need it. A contact form's thank-you message on the page tells the visitor it worked, and a real reply from you an hour later beats a template.

## Check it works

1. Under the form's **Allowed domains**, add your site's domain, plus `localhost` while you develop. Leave it empty to accept posts from anywhere.
2. Search your built site for `emailjs` and your old service id. Nothing should match.
3. Fill in the form on your page and submit it. You land on your thank-you page, or Ikiform's hosted one.
4. Check the inbox of each notification recipient. Each gets an email with the fields, and **Reply** goes to the submitter when the form has an `email` field.
5. Post with the honeypot filled in, `_gotcha=bot`. It still gets a success answer, but the submission is stored as spam and sends no email or webhook.

## If something goes wrong

| Problem | What to do |
| --- | --- |
| Reply still goes to Ikiform | The field is still `user_email`. Rename it `email`, or one of `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. |
| `422 validation_failed` | The post doesn't match the form's field definition. `error.fields` names each field and the reason. Match the input `name` to the field id. |
| `403 challenge_required` | The form's spam check is set to **Ikiform** and a `fetch` post had no token. Get one with `challenge(formId)` from `@ikiform/sdk` before each submit, or turn the check off. |
| `403 origin_not_allowed`, or every post lands in spam | The page's domain isn't in the form's **Allowed domains**. Add it, plus `localhost` for local tests. Subdomains of a listed domain are allowed. |

## Questions

### Is Ikiform cheaper than EmailJS?

It depends on volume and plan, and both change over time, so compare current pricing pages. Ikiform has no free plan; it bills per submission, with Pro or one-time packs.

### Is it safe to expose the EmailJS public key?

EmailJS says a copied key can only send your templates, not custom email. The caller still fills in the template's variables, though, and if the recipient is a variable they choose that too. Restrict the key to your domains and add a captcha if you keep using it.

### Does Ikiform need an API key in the browser?

No. A browser form posts to your form's URL with no key. Secret keys exist only for posting from your own server, and they belong in server-side environment variables.

### Can Ikiform email the person who submitted the form?

No. Ikiform emails you and your team, not the submitter, so there is no autoresponder or confirmation email. Send a webhook to your email tool and send the confirmation from there.

### Can I keep my EmailJS field names?

Yes, except the email field: name it email so notification replies go to the visitor. Ikiform stores any field names you post.

## More guides

- [Send a form to your email](https://ikiform.com/guides/send-form-to-email.md): Get every form submission in your inbox without a mail server: why mailto: and PHP mail() fail, and a setup that filters spam and lets you reply.
- [Next.js](https://ikiform.com/guides/send-form-to-email/nextjs.md): Two ways to email a Next.js form with a Server Action: send it yourself with Resend or Nodemailer, or forward it to a form backend, compared honestly.
- [Reply from your inbox](https://ikiform.com/guides/send-form-to-email/reply-to.md): Make Reply on a form notification write to the person who filled in the form: which field names work, when to use _replyto, and why replies go astray.
- [Without PHP](https://ikiform.com/guides/send-form-to-email/without-php.md): Replace a contact.php script that calls mail() with a form that posts to a hosted endpoint, keeping your field names, your thank-you page and replies.
- [To Gmail](https://ikiform.com/guides/send-form-to-email/gmail.md): Get form submissions in Gmail without mailto: links, app passwords or Apps Script, then label them, keep them out of spam and reply from Gmail.
- [Multiple recipients](https://ikiform.com/guides/send-form-to-email/multiple-recipients.md): Email one form to your whole team, or route each message to the right person by topic, without letting the form itself pick who gets the mail.
