# Kirim hasil formulir HTML ke email Anda

> Terima setiap kiriman formulir di inbox tanpa mail server: mengapa mailto: dan mail() PHP gagal, dan setup yang menyaring spam serta memungkinkan Anda membalas.

Terbit 2026-10-25 · https://ikiform.com/id/guides/send-form-to-email

Formulir HTML tidak bisa mengirim email sendiri, karena browser tidak punya cara untuk berbicara dengan mail server. Arahkan `action` formulir ke form backend, yang menerima kiriman, menyaring spam, menyimpannya, lalu mengirim email ke Anda dan tim. Beri nama field email `email`, dan menekan **Reply** di inbox akan langsung membalas ke orang yang mengisi formulir.

Kebanyakan situs hanya butuh satu hal dari formulir kontak: pesannya masuk ke inbox yang dibaca seseorang. Ternyata itu lebih sulit dari kelihatannya. HTML punya elemen `<form>` tetapi tidak punya cara mengirim email, dan jalan pintas lama entah melempar tugasnya kembali ke pengunjung atau berakhir di folder spam. Panduan ini menjelaskan alasannya, membandingkan pilihan yang ada secara jujur, dan menyiapkan formulir yang mengirimi Anda email di situs dan stack apa pun.

## Mengapa formulir tidak bisa mengirim email sendiri

Saat pengunjung menekan **Send**, browser membuat satu request HTTP ke URL di `action` formulir. Hanya itu yang bisa dilakukan formulir. Mengirim email butuh percakapan SMTP dengan mail server, dari mesin yang dipercaya oleh server penerima. Browser tidak bisa membuka koneksi itu, dan seandainya bisa pun, email dari laptop pengunjung acak akan ditolak. Jadi selalu ada sesuatu di server di antara formulir dan inbox Anda: kode Anda sendiri, skrip di hosting Anda, atau sebuah layanan.

Dengan form backend, sesuatu itu adalah sebuah URL. Formulir mengirim ke `https://ikiform.com/f/<id>`, dan Ikiform mengerjakan sisanya:

Diagram: Bagaimana kiriman formulir menjadi email di inbox Anda

1. Pengunjung mengirim formulir Anda, lalu browser atau server Anda mengirimnya ke endpoint Ikiform Anda.
2. Ikiform memeriksa field terhadap definisi formulir, memberi skor spam pada kiriman, lalu menyimpannya.
3. Kiriman yang mencapai batas skor spam disimpan sebagai spam. Kiriman itu mendapat jawaban sukses yang sama, tetapi tidak memicu email maupun webhook.
4. Setiap kiriman lainnya mengirim satu email ke setiap penerima, dengan alamat pengunjung sebagai reply-to.
5. Email tiba di inbox Anda beberapa detik kemudian. Menekan Reply akan membalas ke pengunjung.

Pengunjung tidak pernah melihat alamat email dan tidak pernah membuka aplikasi email. Satu-satunya yang ada di halaman Anda adalah formulir.

## Enam cara memasukkan formulir ke inbox Anda

Masing-masing cocok untuk seseorang. Tabel ini membahas apa biayanya bagi Anda begitu pengunjung sungguhan, dan bot sungguhan, mulai memakai formulir.

| Pendekatan | Unggul dalam | Kekurangan |
| --- | --- | --- |
| `action="mailto:you@example.com"` | Tanpa server, tanpa akun, satu atribut. | Tidak ada yang terkirim. Browser membuka aplikasi email pengunjung dengan draf, atau tidak melakukan apa-apa jika tidak ada aplikasi email yang disiapkan. Alamat Anda tertera di halaman untuk para scraper. Lihat [panduan Gmail](https://ikiform.com/id/guides/send-form-to-email/gmail.md). |
| Skrip PHP dengan `mail()` | Berjalan di hampir semua shared hosting PHP, tanpa pihak ketiga. | Bergantung pada setup email hosting, dan email yang mengaku berasal dari domain Anda sering gagal SPF atau DMARC lalu masuk spam. Tidak ada penyaringan spam, tidak ada penyimpanan, dan sama sekali tidak ada PHP di hosting statis. Lihat [tanpa PHP](https://ikiform.com/id/guides/send-form-to-email/without-php.md). |
| SDK email di browser, seperti EmailJS | Tanpa server. Template email yang Anda desain di dashboard mereka. | Key dan ID template ada di JavaScript halaman Anda, jadi siapa pun bisa mengirim template Anda dengan teks mereka sendiri. Lihat [alternatif EmailJS](https://ikiform.com/id/guides/send-form-to-email/emailjs-alternative.md). |
| Route Anda sendiri ditambah API email, seperti Resend, Postmark, Amazon SES, atau Nodemailer lewat SMTP | Kendali penuh: subjek Anda, HTML Anda, konfirmasi ke pengunjung. | Anda membangun sendiri validasi, penyaringan spam, rate limit, penyimpanan, dan percobaan ulang, memverifikasi domain pengirim, dan butuh server bahkan untuk situs statis. Lihat [panduan Next.js](https://ikiform.com/id/guides/send-form-to-email/nextjs.md). |
| Google Apps Script sebagai web app | Berjalan di akun Google yang sudah Anda punya. | Kuota skrip membatasi berapa banyak penerima yang bisa dikirimi email per hari, URL-nya tanpa perlindungan spam, dan Anda yang memelihara skripnya. |
| Form backend, seperti Ikiform, [Formspree](https://ikiform.com/alternatives/formspree), atau [Web3Forms](https://ikiform.com/alternatives/web3forms) | Satu URL di `action`. Penyaringan spam, penyimpanan, dan email tanpa server. | Tata letak dan subjek email ditentukan layanannya. Dengan Ikiform, tidak ada yang pernah dikirim lewat email ke pengunjung. |

Jika Anda butuh email dengan branding penuh, atau konfirmasi ke pengunjung, tulis route sendiri dengan API email. Jika Anda ingin pesan masuk ke inbox dan bisa dibalas, dengan spam tetap di luar, form backend adalah cara dengan kode paling sedikit. Sisa panduan ini memakai Ikiform.

## Formulirnya

Tiga field sudah mencakup kebanyakan formulir kontak: siapa mereka, ke mana membalas, dan apa yang mereka inginkan. Pilih stack Anda di bawah; pilihan itu berlaku untuk setiap contoh kode di panduan ini.

**HTML**

Formulir ini berfungsi tanpa JavaScript sama sekali: browser mengirimnya, Ikiform mengirimi Anda email, dan pengunjung mendarat di halaman pada `_redirect`. Setiap field punya slot error kosong di bawahnya untuk skrip di bawah.

`contact.html`:

```html
<form id="contact" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required maxlength="120" />
  <p data-error="name"></p>

  <label for="email">Your email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <p data-error="email"></p>

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
  <p data-error="message"></p>

  <input type="hidden" name="page" value="/contact" />
  <input type="hidden" name="_redirect" value="https://example.com/contact/sent" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send message</button>
  <p id="contact-status" role="status"></p>
</form>
```

Skrip ini membuat pengunjung tetap di halaman. Skrip mengirim dengan `fetch`, menulis setiap error dari server di bawah field-nya, lalu mengganti formulir dengan kalimat yang mengulang alamat yang akan Anda balas, sehingga salah ketik mudah terlihat.

`contact.js`:

```js
const form = document.getElementById("contact");
const status = document.getElementById("contact-status");
const button = form.querySelector("button");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  for (const slot of form.querySelectorAll("[data-error]")) slot.textContent = "";
  button.disabled = true;
  status.textContent = "Sending";

  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      const email = form.elements.namedItem("email").value;
      const done = document.createElement("p");
      done.setAttribute("role", "status");
      done.textContent = `Sent. We'll reply to ${email}.`;
      form.replaceWith(done);
      return;
    }
    for (const [field, messages] of Object.entries(result.error.fields ?? {})) {
      const slot = form.querySelector(`[data-error="${field}"]`);
      if (slot) slot.textContent = messages[0];
    }
    status.textContent = result.error.fields ? "Check the fields above." : result.error.message;
  } catch {
    status.textContent = "Couldn't send. Check your connection and try again.";
  }
  button.disabled = false;
});
```

**Next.js**

Server action mengirim pesan dengan secret key Anda. Action ini memetakan `error.fields` dari Ikiform menjadi satu pesan per field dan mengembalikan nilai yang sudah diketik, sehingga tidak ada tulisan pengunjung yang hilang saat terjadi error.

`app/contact/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface ContactState {
  sent: boolean;
  email?: string;
  errors: Record<string, string>;
  values?: { name: string; email: string; message: string };
}

export async function sendMessage(_previous: ContactState, formData: FormData): Promise<ContactState> {
  const values = {
    name: String(formData.get("name") ?? "").trim(),
    email: String(formData.get("email") ?? "").trim(),
    message: String(formData.get("message") ?? "").trim(),
  };
  const origin = (await headers()).get("origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({ ...values, page: "/contact", _gotcha: String(formData.get("_gotcha") ?? "") }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { sent: true, email: values.email, errors: {} };

  const errors: Record<string, string> = {};
  for (const [field, messages] of Object.entries(result?.error?.fields ?? {})) {
    errors[field] = (messages as string[])[0] ?? "Check this field.";
  }
  if (!Object.keys(errors).length) errors.form = result?.error?.message ?? "Couldn't send your message. Try again.";
  return { sent: false, errors, values };
}
```

`app/contact/contact-form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type ContactState, sendMessage } from "./actions";

const initial: ContactState = { sent: false, errors: {} };

export function ContactForm() {
  const [state, action, pending] = useActionState(sendMessage, initial);

  if (state.sent) return <p role="status">Sent. We'll reply to {state.email}.</p>;

  return (
    <form action={action}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required defaultValue={state.values?.name} />
      {state.errors.name ? <p role="alert">{state.errors.name}</p> : null}

      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required defaultValue={state.values?.email} />
      {state.errors.email ? <p role="alert">{state.errors.email}</p> : null}

      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} defaultValue={state.values?.message} />
      {state.errors.message ? <p role="alert">{state.errors.message}</p> : null}

      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.errors.form ? <p role="alert">{state.errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
    </form>
  );
}
```

Versi sisi server mengirim dengan secret key, sehingga Ikiform melewati **Allowed domains** dan pemeriksaan manusianya. Semua kiriman lalu datang dari IP server Anda, dan batas Standard 5 per menit per pengunjung menghitung semua pengunjung Anda sebagai satu. Pilih [rate limit](https://ikiform.com/id/docs/rate-limits.md) Relaxed atau Custom untuk formulir yang Anda kirim lewat server, dan simpan `IKIFORM_SECRET_KEY` di environment variable khusus server. Setiap contoh server meneruskan header `Origin` dari browser jika ada, karena `fetch` milik server sendiri tidak mengirimnya dan Ikiform menghitung kiriman uji gratis dari `localhost` berdasarkan header itu.

Ingin lebih jauh dengan Next.js? Baca [Formulir kontak Next.js yang mengirim email](https://ikiform.com/id/guides/send-form-to-email/nextjs.md).

**Astro**

Zod memeriksa setiap field di action sebelum apa pun dikirim, dan `isInputError` memberi Anda pesannya per field. Halaman harus dirender sesuai permintaan, jadi pertahankan `prerender = false` dan tambahkan adapter.

`src/actions/index.ts`:

```ts
import { ActionError, defineAction } from "astro:actions";
import { getSecret } from "astro:env/server";
import { z } from "astro/zod";

export const server = {
  sendMessage: defineAction({
    accept: "form",
    input: z.object({
      name: z.string().trim().min(1, "Tell us your name.").max(120),
      email: z.email("Enter an address we can reply to."),
      message: z.string().trim().min(10, "Write at least a sentence."),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          Authorization: `Bearer ${getSecret("IKIFORM_SECRET_KEY")}`,
          ...(origin ? { Origin: origin } : {}),
        },
        body: JSON.stringify({ ...input, page: "/contact" }),
      });
      const result = await response.json();
      if (!result.ok) throw new ActionError({ code: "BAD_REQUEST", message: result.error.message });
      return { email: input.email };
    },
  }),
};
```

`src/pages/contact.astro`:

```astro
---
import { actions, isInputError } from "astro:actions";

export const prerender = false;

const result = Astro.getActionResult(actions.sendMessage);
const fields = result?.error && isInputError(result.error) ? result.error.fields : {};
---

{result?.data ? (
  <p role="status">Sent. We'll reply to {result.data.email}.</p>
) : (
  <form method="POST" action={actions.sendMessage}>
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    {fields.name && <p role="alert">{fields.name[0]}</p>}

    <label for="email">Your email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    {fields.email && <p role="alert">{fields.email[0]}</p>}

    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
    {fields.message && <p role="alert">{fields.message[0]}</p>}

    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && !isInputError(result.error) && <p role="alert">{result.error.message}</p>}
    <button>Send message</button>
  </form>
)}
```

Versi sisi server mengirim dengan secret key, sehingga Ikiform melewati **Allowed domains** dan pemeriksaan manusianya. Semua kiriman lalu datang dari IP server Anda, dan batas Standard 5 per menit per pengunjung menghitung semua pengunjung Anda sebagai satu. Pilih [rate limit](https://ikiform.com/id/docs/rate-limits.md) Relaxed atau Custom untuk formulir yang Anda kirim lewat server, dan simpan `IKIFORM_SECRET_KEY` di environment variable khusus server. Setiap contoh server meneruskan header `Origin` dari browser jika ada, karena `fetch` milik server sendiri tidak mengirimnya dan Ikiform menghitung kiriman uji gratis dari `localhost` berdasarkan header itu.

**TanStack Start**

Server function membaca `FormData` di `.validator()`, mengirimnya dari server, dan mengembalikan alamat balasan atau satu error per field.

`src/routes/contact.tsx`:

```tsx
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";

type Outcome = { sent: true; email: string } | { sent: false; errors: Record<string, string> };

const sendMessage = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    return {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }): Promise<Outcome> => {
    const origin = getRequestHeader("origin");
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify({ ...data, page: "/contact" }),
    });
    const result = await response.json();
    if (result.ok) return { sent: true, email: data.email };
    const errors: Record<string, string> = { form: String(result.error.message) };
    for (const [field, messages] of Object.entries(result.error.fields ?? {})) {
      errors[field] = String((messages as string[])[0]);
    }
    return { sent: false, errors };
  });

export const Route = createFileRoute("/contact")({ component: Contact });

function Contact() {
  const send = useServerFn(sendMessage);
  const [outcome, setOutcome] = useState<Outcome | null>(null);
  const [pending, setPending] = useState(false);
  const errors = outcome && !outcome.sent ? outcome.errors : {};

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setPending(true);
    try {
      setOutcome(await send({ data }));
    } catch {
      setOutcome({ sent: false, errors: { form: "Couldn't send your message. Try again." } });
    } finally {
      setPending(false);
    }
  }

  if (outcome?.sent) return <p role="status">Sent. We'll reply to {outcome.email}.</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {errors.name ? <p role="alert">{errors.name}</p> : null}
      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} />
      {errors.message ? <p role="alert">{errors.message}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form && !errors.name && !errors.email && !errors.message ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
    </form>
  );
}
```

Versi sisi server mengirim dengan secret key, sehingga Ikiform melewati **Allowed domains** dan pemeriksaan manusianya. Semua kiriman lalu datang dari IP server Anda, dan batas Standard 5 per menit per pengunjung menghitung semua pengunjung Anda sebagai satu. Pilih [rate limit](https://ikiform.com/id/docs/rate-limits.md) Relaxed atau Custom untuk formulir yang Anda kirim lewat server, dan simpan `IKIFORM_SECRET_KEY` di environment variable khusus server. Setiap contoh server meneruskan header `Origin` dari browser jika ada, karena `fetch` milik server sendiri tidak mengirimnya dan Ikiform menghitung kiriman uji gratis dari `localhost` berdasarkan header itu.

**React Router**

`action` route meneruskan pesan dan mengembalikan error field dari Ikiform lewat `actionData`, dengan kode status Ikiform dipertahankan oleh `data()`.

`app/routes/contact.tsx`:

```tsx
import { data, Form, useNavigation } from "react-router";
import type { Route } from "./+types/contact";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email") ?? "").trim();
  const origin = request.headers.get("origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({
      name: String(formData.get("name") ?? "").trim(),
      email,
      message: String(formData.get("message") ?? "").trim(),
      page: "/contact",
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { sent: true as const, email };
  const fields: Record<string, string[]> = result.error.fields ?? {};
  return data(
    { sent: false as const, message: String(result.error.message), fields },
    { status: response.status },
  );
}

export default function Contact({ actionData }: Route.ComponentProps) {
  const navigation = useNavigation();
  const pending = navigation.state === "submitting";

  if (actionData?.sent) return <p role="status">Sent. We'll reply to {actionData.email}.</p>;
  const fields = actionData?.fields ?? {};

  return (
    <Form method="post">
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {fields.name ? <p role="alert">{fields.name[0]}</p> : null}
      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {fields.email ? <p role="alert">{fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} />
      {fields.message ? <p role="alert">{fields.message[0]}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {actionData && !Object.keys(fields).length ? <p role="alert">{actionData.message}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
    </Form>
  );
}
```

Versi sisi server mengirim dengan secret key, sehingga Ikiform melewati **Allowed domains** dan pemeriksaan manusianya. Semua kiriman lalu datang dari IP server Anda, dan batas Standard 5 per menit per pengunjung menghitung semua pengunjung Anda sebagai satu. Pilih [rate limit](https://ikiform.com/id/docs/rate-limits.md) Relaxed atau Custom untuk formulir yang Anda kirim lewat server, dan simpan `IKIFORM_SECRET_KEY` di environment variable khusus server. Setiap contoh server meneruskan header `Origin` dari browser jika ada, karena `fetch` milik server sendiri tidak mengirimnya dan Ikiform menghitung kiriman uji gratis dari `localhost` berdasarkan header itu.

**SvelteKit**

Action default mengembalikan `fail()` dengan error field. `update({ reset: false })` mempertahankan ketikan pengunjung saat server menjawab dengan error. Deklarasikan `IKIFORM_SECRET_KEY` di `src/env.ts` dengan `defineEnvVars` agar `$app/env/private` bisa membacanya.

`src/routes/contact/+page.server.ts`:

```ts
import { fail } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { Actions } from "./$types";

export const actions = {
  default: async ({ request, fetch, url }) => {
    const data = await request.formData();
    const values = {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
    };

    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
        Origin: request.headers.get("origin") ?? url.origin,
      },
      body: JSON.stringify({ ...values, page: "/contact", _gotcha: String(data.get("_gotcha") ?? "") }),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields: Record<string, string[]> = result.error.fields ?? {};
      return fail(response.status, { values, fields, message: String(result.error.message) });
    }
    return { sentTo: values.email };
  },
} satisfies Actions;
```

`src/routes/contact/+page.svelte`:

```svelte
<script lang="ts">
  import { enhance } from "$app/forms";
  import type { PageProps } from "./$types";

  let { form }: PageProps = $props();
  let pending = $state(false);
  const fields = $derived<Record<string, string[]>>(form?.fields ?? {});
</script>

{#if form?.sentTo}
  <p role="status">Sent. We'll reply to {form.sentTo}.</p>
{:else}
  <form
    method="POST"
    use:enhance={() => {
      pending = true;
      return async ({ update }) => {
        await update({ reset: false });
        pending = false;
      };
    }}
  >
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    {#if fields.name}<p role="alert">{fields.name[0]}</p>{/if}
    <label for="email">Your email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    {#if fields.email}<p role="alert">{fields.email[0]}</p>{/if}
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
    {#if fields.message}<p role="alert">{fields.message[0]}</p>{/if}
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if form?.message && !Object.keys(fields).length}<p role="alert">{form.message}</p>{/if}
    <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
  </form>
{/if}
```

Versi sisi server mengirim dengan secret key, sehingga Ikiform melewati **Allowed domains** dan pemeriksaan manusianya. Semua kiriman lalu datang dari IP server Anda, dan batas Standard 5 per menit per pengunjung menghitung semua pengunjung Anda sebagai satu. Pilih [rate limit](https://ikiform.com/id/docs/rate-limits.md) Relaxed atau Custom untuk formulir yang Anda kirim lewat server, dan simpan `IKIFORM_SECRET_KEY` di environment variable khusus server. Setiap contoh server meneruskan header `Origin` dari browser jika ada, karena `fetch` milik server sendiri tidak mengirimnya dan Ikiform menghitung kiriman uji gratis dari `localhost` berdasarkan header itu.

**Nuxt**

Server route menyimpan secret key dan meneruskan pesan Ikiform sebagai `message` milik `createError` dan error field di `data`-nya; `$fetch` mengembalikan keduanya di client sebagai `data.message` dan `data.data.fields`. Nuxt mengisi `runtimeConfig.ikiformSecretKey` dari `NUXT_IKIFORM_SECRET_KEY`.

`server/api/contact.post.ts`:

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ name?: string; email?: string; message?: string; _gotcha?: string }>(event);
  const config = useRuntimeConfig(event);
  const origin = getRequestHeader(event, "origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.ikiformSecretKey}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({
      name: body.name?.trim() ?? "",
      email: body.email?.trim() ?? "",
      message: body.message?.trim() ?? "",
      page: "/contact",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: String(result.error.message),
      data: { fields: result.error.fields ?? {} },
    });
  }
  return { sentTo: body.email };
});
```

`nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  runtimeConfig: {
    ikiformSecretKey: "",
  },
});
```

`app/components/ContactForm.vue`:

```vue
<script setup lang="ts">
const values = reactive({ name: "", email: "", message: "", _gotcha: "" });
const fields = ref<Record<string, string[]>>({});
const error = ref("");
const sentTo = ref("");
const sending = ref(false);

async function send() {
  sending.value = true;
  fields.value = {};
  error.value = "";
  try {
    const result = await $fetch("/api/contact", { method: "POST", body: values });
    sentTo.value = result.sentTo ?? values.email;
  } catch (failure) {
    const data = (failure as { data?: { message?: string; data?: { fields?: Record<string, string[]> } } }).data;
    fields.value = data?.data?.fields ?? {};
    error.value = data?.message ?? "Couldn't send your message. Try again.";
  } finally {
    sending.value = false;
  }
}
</script>

<template>
  <p v-if="sentTo" role="status">Sent. We'll reply to {{ sentTo }}.</p>
  <form v-else @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" v-model="values.name" name="name" autocomplete="name" required />
    <p v-if="fields.name" role="alert">{{ fields.name[0] }}</p>
    <label for="email">Your email</label>
    <input id="email" v-model="values.email" name="email" type="email" autocomplete="email" required />
    <p v-if="fields.email" role="alert">{{ fields.email[0] }}</p>
    <label for="message">Message</label>
    <textarea id="message" v-model="values.message" name="message" rows="6" required minlength="10" />
    <p v-if="fields.message" role="alert">{{ fields.message[0] }}</p>
    <input v-model="values._gotcha" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error && !Object.keys(fields).length" role="alert">{{ error }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Send message" }}</button>
  </form>
</template>
```

Versi sisi server mengirim dengan secret key, sehingga Ikiform melewati **Allowed domains** dan pemeriksaan manusianya. Semua kiriman lalu datang dari IP server Anda, dan batas Standard 5 per menit per pengunjung menghitung semua pengunjung Anda sebagai satu. Pilih [rate limit](https://ikiform.com/id/docs/rate-limits.md) Relaxed atau Custom untuk formulir yang Anda kirim lewat server, dan simpan `IKIFORM_SECRET_KEY` di environment variable khusus server. Setiap contoh server meneruskan header `Origin` dari browser jika ada, karena `fetch` milik server sendiri tidak mengirimnya dan Ikiform menghitung kiriman uji gratis dari `localhost` berdasarkan header itu.

**React**

Tanpa server sendiri, browser mengirim langsung ke Ikiform. Tidak ada key di kode ini; **Allowed domains** yang menentukan situs mana yang boleh mengirim.

`src/ContactForm.tsx`:

```tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

export function ContactForm() {
  const [sentTo, setSentTo] = useState("");
  const [sending, setSending] = useState(false);
  const [fields, setFields] = useState<Record<string, string[]>>({});
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    setSending(true);
    setFields({});
    setError("");
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
      const result = await response.json();
      if (result.ok) {
        setSentTo(String(body.get("email")));
        return;
      }
      setFields(result.error.fields ?? {});
      setError(result.error.message);
    } catch {
      setError("Couldn't reach the server. Try again.");
    } finally {
      setSending(false);
    }
  }

  if (sentTo) return <p role="status">Sent. We'll reply to {sentTo}.</p>;

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {fields.name ? <p role="alert">{fields.name[0]}</p> : null}
      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {fields.email ? <p role="alert">{fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} />
      {fields.message ? <p role="alert">{fields.message[0]}</p> : null}
      <input type="hidden" name="page" value="/contact" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error && !Object.keys(fields).length ? <p role="alert">{error}</p> : null}
      <button disabled={sending}>{sending ? "Sending" : "Send message"}</button>
    </form>
  );
}
```

**Vue**

Kiriman sisi browser yang sama sebagai komponen Vue. Formulir tetap punya `action`, jadi masih terkirim jika skripnya gagal dimuat.

`src/components/ContactForm.vue`:

```vue
<script setup lang="ts">
import { ref } from "vue";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

const sentTo = ref("");
const sending = ref(false);
const fields = ref<Record<string, string[]>>({});
const error = ref("");

async function send(event: SubmitEvent) {
  const body = new FormData(event.target as HTMLFormElement);
  sending.value = true;
  fields.value = {};
  error.value = "";
  try {
    const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      sentTo.value = String(body.get("email"));
      return;
    }
    fields.value = result.error.fields ?? {};
    error.value = result.error.message;
  } catch {
    error.value = "Couldn't reach the server. Try again.";
  } finally {
    sending.value = false;
  }
}
</script>

<template>
  <p v-if="sentTo" role="status">Sent. We'll reply to {{ sentTo }}.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    <p v-if="fields.name" role="alert">{{ fields.name[0] }}</p>
    <label for="email">Your email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    <p v-if="fields.email" role="alert">{{ fields.email[0] }}</p>
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required minlength="10" />
    <p v-if="fields.message" role="alert">{{ fields.message[0] }}</p>
    <input type="hidden" name="page" value="/contact" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error && !Object.keys(fields).length" role="alert">{{ error }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Send message" }}</button>
  </form>
</template>
```

- Ganti `frm_xxxxxxxxxxxxxxxx` dengan ID formulir Anda dari dashboard.
- `name="email"` menjadikan field itu alamat reply-to di email notifikasi. Pertahankan namanya, meskipun labelnya berbunyi "Email Anda".
- `page` adalah field tersembunyi biasa. Field ini muncul di email, jadi Anda tahu dari halaman mana pesan itu berasal.
- `_gotcha` adalah honeypot. Orang tidak pernah melihatnya; bot yang mengisinya disimpan sebagai spam dan tidak mengirim email ke siapa pun.
- `_redirect` hanya berfungsi untuk URL `https:` di salah satu **Allowed domains** formulir. Hilangkan saja dan atur **Redirect after submit** di pengaturan formulir sebagai gantinya, atau Ikiform akan menampilkan halaman terima kasihnya sendiri.

Panduan framework membahas lebih jauh: [Next.js](https://ikiform.com/id/docs/guides/nextjs.md), [Astro](https://ikiform.com/id/docs/guides/astro.md), [SvelteKit](https://ikiform.com/id/docs/guides/sveltekit.md), [Nuxt](https://ikiform.com/id/docs/guides/nuxt.md), [React](https://ikiform.com/id/docs/guides/react.md), [Vue](https://ikiform.com/id/docs/guides/vue.md), dan [HTML biasa](https://ikiform.com/id/docs/html.md). Pembuat situs seperti [Webflow](https://ikiform.com/id/docs/guides/webflow.md) dan [Carrd](https://ikiform.com/id/docs/guides/carrd.md) punya halaman sendiri, dan [template formulir kontak](https://ikiform.com/forms/contact-form) menambahkan field topik.

Definisikan juga field-nya di dashboard, dan Ikiform menolak kiriman yang tidak cocok dengan `422 validation_failed` dan pesan per field, yang ditampilkan kode di atas di samping setiap input. Coba di sini; ini menjalankan validator yang sama di browser Anda:

Formulir: Contact

- **Nama** (`name`, text, wajib)
- **Email Anda** (`email`, email, wajib)
- **Pesan** (`message`, textarea, wajib)

Tempel definisi ini di tab **Fields** formulir. Tanpa definisi, Ikiform menerima nama field apa pun dan mengirim email berisi apa saja yang masuk.

## Seperti apa emailnya

Setiap kiriman yang diterima mengirim email sederhana yang mudah dibaca. Ini versi teks salah satunya:

`Email notifikasi`:

```text
From: Ikiform <notifications@ikiform.com>
To: you@yourcompany.com
Reply-To: ada@example.com
Subject: New submission: Contact

Contact got a new submission on Oct 25, 2026, 9:14 AM UTC.

name: Ada Lovelace
email: ada@example.com
message: Do you ship to Canada? I'd like to order 40 units before December.
page: /contact

View it in Ikiform:
https://ikiform.com/dashboard/submissions?form=frm_xxxxxxxxxxxxxxxx&submission=sub_...
```

- Email datang dari `notifications@ikiform.com`. Anda tidak perlu menyiapkan record DNS atau domain pengirim, karena email dikirim dari domain Ikiform.
- Subjeknya selalu `New submission: <form name>`. `_subject` tidak mengubah subjek email notifikasi. Nilainya diteruskan ke webhook dan API sebagai `meta.subject`. Untuk membedakan formulir, beri setiap formulir nama yang jelas di dashboard, seperti "Kontak, halaman harga".
- Field muncul sesuai urutan kirimnya, diberi label berdasarkan `name`-nya. Field yang diawali `_` tidak ditampilkan.
- Formulir atau jawaban yang sangat panjang dipendekkan, dan email menyebutkan berapa field lagi yang ada di dashboard. [Halaman batas](https://ikiform.com/id/docs/limits.md#notifications) berisi angkanya.
- Field yang terlihat seperti password atau nomor kartu tidak ditampilkan, dan email untuk kiriman yang memuatnya ditahan untuk ditinjau. Jangan kumpulkan kredensial dengan form backend.
- Anggota workspace mendapat tombol **View submission**. Orang di luar workspace mendapat field-nya dan link berhenti berlangganan.

## Siapa yang menerima email

Penerima diatur per formulir di dashboard, tidak pernah oleh formulir itu sendiri. Field yang menentukan penerima akan membuat siapa pun bisa memakai formulir Anda untuk mengirim email ke siapa saja, jadi Ikiform tidak menyediakannya.

| Siapa | Cara menambahkannya | Catatan |
| --- | --- | --- |
| Default | Tidak perlu apa-apa | Anggota yang dipilih sebagai **Notification email** workspace, atau setiap owner workspace jika belum ada yang dipilih |
| Anggota pilihan | Centang mereka di pengaturan **Notifications** formulir | Hanya anggota dengan alamat email terverifikasi |
| Orang di luar workspace | **Add someone outside the workspace**, lalu **Send link** | Mereka baru menerima email setelah mengonfirmasi. Maksimal 5 per formulir, dan masing-masing bisa berhenti berlangganan |

Setiap formulir mengirim email ke maksimal 10 alamat. Setiap penerima mendapat salinannya sendiri, bukan CC bersama, jadi tidak ada yang melihat alamat penerima lain. Jika mailbox penerima memantulkan email (bounce), Ikiform menjeda email ke alamat itu untuk sementara dan dashboard menampilkan **Emails bounced** di sampingnya. [Beberapa penerima](https://ikiform.com/id/guides/send-form-to-email/multiple-recipients.md) membahas perutean berdasarkan topik, yang tidak bisa dilakukan hanya dengan daftar penerima.

## Balas ke orangnya, bukan ke Ikiform

Ikiform menentukan alamat reply-to dari field `_replyto` terlebih dulu, lalu dari field bernama `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. Nilainya harus satu alamat email yang valid, atau akan diabaikan. Dengan reply-to terisi, menekan **Reply** di Gmail, Outlook, atau Apple Mail membuka balasan ke pengunjung, dan jawaban Anda dikirim dari alamat Anda sendiri.

Tanpa reply-to, balasan terkirim ke `notifications@ikiform.com`, yang tidak dibaca siapa pun. Itulah kejutan paling umum, dan biasanya penyebabnya field bernama `user_email` atau `Email Address` dengan spasi. Ganti namanya, atau tambahkan `_replyto`. [Panduan reply-to](https://ikiform.com/id/guides/send-form-to-email/reply-to.md) membahas formulir dengan dua field email dan pembuat situs yang mengganti nama field untuk Anda.

## Saat banyak kiriman datang sekaligus

Setiap formulir mengirim hingga 50 email notifikasi per jam, atau 10 selama workspace berumur kurang dari 7 hari. Lewat dari itu, sisa kiriman di jam tersebut tetap disimpan seperti biasa dan datang sebagai satu email ringkasan setelah jam itu berakhir. Batas ini melindungi inbox Anda, dan reputasi email Ikiform di mata penyedia email Anda, saat sebuah formulir dibagikan di tempat besar atau ada bot yang lolos dari filter.

Diagram: Apa yang terjadi pada email notifikasi di jam yang sibuk

1. 50 kiriman pertama ke sebuah formulir dalam satu jam masing-masing mengirim email seperti biasa.
2. Kiriman berikutnya di jam yang sama disimpan dan langsung tampil di dashboard, tanpa email untuk masing-masing.
3. Setelah jam itu berakhir, setiap penerima mendapat satu ringkasan yang menghitungnya dan menautkan ke dashboard.

Batas dihitung per formulir, jadi lonjakan di satu formulir tidak memperlambat formulir lain. Webhook tidak dibatasi dan tetap terkirim untuk setiap kiriman.

Subjek ringkasan berisi jumlah dan nama formulir, tanpa daftar field dan tanpa reply-to, jadi jawab pesan-pesan itu dari dashboard. Jika Anda sering melewati batas, email bukan saluran yang tepat untuk formulir itu: kirim ke [Slack](https://ikiform.com/blog/form-submissions-to-slack.md) atau inbox bersama lewat [webhook](https://ikiform.com/id/docs/webhooks.md) sebagai gantinya.

## Jauhkan spam dari inbox Anda

Email membuat spam jadi mahal: setiap kiriman bot adalah notifikasi yang dibuka seseorang. Ikiform menyimpan spam dengan jawaban sukses biasa, jadi bot tidak belajar apa-apa, dan tidak mengirim email untuknya. Empat hal menentukan apa yang dianggap spam:

| Pertahanan | Yang ditangkap | Setup |
| --- | --- | --- |
| Honeypot | Bot yang mengisi setiap input | Input tersembunyi `_gotcha` |
| Skor spam | Link di field nama, tiga link atau lebih, frasa spam yang dikenal, alamat sekali pakai, dan kiriman identik berulang. Pada skor 5, kiriman dianggap spam | Tidak ada |
| Domain yang diizinkan | Kiriman dari situs lain | Tambahkan domain Anda di pengaturan formulir |
| Pemeriksaan manusia | Bot yang menjalankan browser sungguhan | Atur **Spam check** ke **Ikiform** |

Sesekali cek filter **Spam** di dashboard. Jika ada pesan asli yang masuk ke sana, tandai sebagai bukan spam dan emailnya akan dikirim saat itu. [Dokumentasi spam](https://ikiform.com/id/docs/spam.md) menjelaskan setiap sinyal, termasuk kapan sebaiknya menyalakan pemeriksaan manusia.

## Yang tidak bisa dilakukan email

Ikiform tidak mengirim email ke orang yang mengisi formulir. Email notifikasi hanya dikirim ke Anda dan tim, jadi tidak ada autoresponder, email konfirmasi, atau double opt-in. Kirim semua itu dari alat email Anda, dipicu oleh [webhook](https://ikiform.com/id/docs/webhooks.md). Jika situs Anda menjanjikan "pesan Anda sudah kami terima", tampilkan itu di halaman setelah formulir dikirim, seperti yang sudah dilakukan kode di atas.

- **Inbox tim bersama.** Tambahkan alamat tim sebagai penerima, atau teruskan dari penyedia email Anda.
- **Kanal chat.** Kirim setiap kiriman ke [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Discord](https://ikiform.com/blog/form-submissions-to-discord.md), atau [Microsoft Teams](https://ikiform.com/blog/form-submissions-to-microsoft-teams.md) lewat webhook. Chat cocok untuk formulir yang menerima lebih dari beberapa pesan per hari.
- **Spreadsheet atau CRM.** [Google Sheets](https://ikiform.com/blog/form-submissions-to-google-sheets.md), atau alat apa pun lewat [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) atau [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).
- **Lampiran.** Unggahan file belum didukung. Kiriman berisi file mendapat `422 files_not_supported`. Minta link ke file yang dibagikan sebagai gantinya.

### Mematikan email

Beberapa formulir lebih baik tanpa email, seperti pendaftaran newsletter yang ramai. Matikan **Email new submissions** di pengaturan **Notifications** formulir. Kiriman tetap disimpan, dan webhook tetap terkirim.

> Batasnya lebih rendah di workspace baru: 10 email per formulir per jam selama 7 hari pertama. Jika Anda menguji dengan skrip, ringkasan akan datang lebih cepat dibanding nanti.

## Pilih situasi Anda

Setup di atas sama di mana saja. Halaman-halaman ini membahas inbox tertentu, stack tertentu, atau alat yang sedang Anda tinggalkan.

- [Tanpa PHP](https://ikiform.com/id/guides/send-form-to-email/without-php.md): Ganti skrip contact.php yang memanggil mail() dengan formulir yang mengirim ke endpoint hosted, dengan nama field, halaman terima kasih, dan balasan tetap sama.
- [Ke Gmail](https://ikiform.com/id/guides/send-form-to-email/gmail.md): Terima kiriman formulir di Gmail tanpa link mailto:, app password, atau Apps Script, lalu beri label, jauhkan dari spam, dan balas dari Gmail.
- [Beberapa penerima](https://ikiform.com/id/guides/send-form-to-email/multiple-recipients.md): Kirim email dari satu formulir ke seluruh tim, atau arahkan setiap pesan ke orang yang tepat berdasarkan topik, tanpa membiarkan formulir sendiri memilih siapa yang menerima email.
- [Balas dari inbox Anda](https://ikiform.com/id/guides/send-form-to-email/reply-to.md): Buat Reply di notifikasi formulir membalas ke orang yang mengisi formulir: nama field yang berfungsi, kapan memakai _replyto, dan mengapa balasan bisa nyasar.
- [Alternatif EmailJS](https://ikiform.com/id/guides/send-form-to-email/emailjs-alternative.md): Pindahkan formulir kontak dari EmailJS: mengapa key email di sisi browser berisiko, apa yang berubah di kode Anda, dan apa yang Anda lepaskan, seperti balasan otomatis.
- [Next.js](https://ikiform.com/id/guides/send-form-to-email/nextjs.md): Dua cara mengirim email dari formulir Next.js dengan Server Action: kirim sendiri dengan Resend atau Nodemailer, atau teruskan ke form backend, dibandingkan secara jujur.

## Pastikan berfungsi

Jalankan langkah-langkah ini sebelum formulir live. Ikiform tidak punya paket gratis. Pengujian gratis: workspace tanpa paket menerima 50 kiriman uji per bulan dari `localhost`. Untuk go live, pilih Pro mulai $6/bln untuk 500 kiriman, atau bayar sekali: 10.000 kiriman seharga $49 atau 50.000 kiriman seharga $149, dengan kredit yang tidak pernah kedaluwarsa. Lihat [harga](https://ikiform.com/id/pricing).

1. Di **Allowed domains** formulir, tambahkan domain situs Anda, ditambah `localhost` selama pengembangan. Biarkan kosong untuk menerima kiriman dari mana saja.
2. Di pengaturan **Notifications** formulir, pastikan **Email new submissions** menyala dan orang yang tepat sudah dicentang.
3. Isi formulir di halaman Anda lalu kirim. Anda mendarat di halaman terima kasih Anda, atau halaman hosted milik Ikiform.
4. Buka **Submissions** di dashboard. Kiriman baru ada di sana dengan setiap field yang Anda kirim.
5. Buka email notifikasi. Subjeknya `New submission:` diikuti nama formulir, dan setiap field yang Anda kirim tercantum.
6. Tekan **Reply**. Baris **To** menampilkan alamat yang Anda ketik di formulir, bukan alamat Ikiform.
7. Kirim dengan honeypot terisi, `_gotcha=bot`. Kiriman tetap mendapat jawaban sukses, tetapi disimpan sebagai spam dan tidak memicu email atau webhook.
8. Tidak ada yang masuk? Ubah filter status di dashboard ke **Spam**. Spam mendapat jawaban sukses yang sama dengan kiriman asli.

## Pemecahan masalah

| Masalah | Yang harus dilakukan |
| --- | --- |
| Kiriman ada di dashboard tetapi tidak ada email yang datang | Cek dulu folder spam atau promosi, lalu tambahkan `notifications@ikiform.com` ke kontak Anda. Setelah itu cek pengaturan **Notifications** formulir: email mungkin mati, alamat luar mungkin masih berstatus **Waiting for confirmation**, atau menampilkan **Emails bounced**. |
| Kiriman ada di dashboard tetapi tidak ada email atau webhook yang datang | Kiriman itu disimpan sebagai spam. Spam tidak mengirim apa pun. Buka di filter **Spam** untuk melihat alasannya, dan tandai sebagai bukan spam untuk mengirim email dan webhook-nya. |
| **Reply** terkirim ke notifications@ikiform.com | Tidak ditemukan reply-to. Beri nama field email salah satu dari `email`, `email_address`, `your_email`, `reply_to`, `contact_email`, atau tambahkan field `_replyto` berisi alamat pengunjung. |
| Email berhenti dan yang datang malah ringkasan | Formulir melewati 50 email dalam satu jam (10 di workspace baru). Semuanya ada di dashboard. Cari lonjakan bot di **Spam**, atau pakai webhook untuk formulir yang ramai. |
| `_subject` tidak mengubah subjek | `_subject` tidak mengubah subjek email notifikasi. Nilainya diteruskan ke webhook dan API sebagai `meta.subject`. |
| `403 origin_not_allowed`, atau setiap kiriman masuk ke spam | Domain halaman tidak ada di **Allowed domains** formulir. Tambahkan, ditambah `localhost` untuk uji lokal. Subdomain dari domain yang terdaftar diizinkan. |
| `422 validation_failed` | Kiriman tidak cocok dengan definisi field formulir. `error.fields` menyebutkan setiap field dan alasannya. Samakan `name` input dengan ID field. |
| `402 test_limit_reached` | Workspace belum punya paket dan sudah memakai 50 uji gratis dari `localhost` bulan ini. Pilih paket, atau pakai **Send test** di dashboard. |

## Pertanyaan

### Bisakah formulir HTML mengirim email tanpa server?

Tidak dengan sendirinya. Formulir hanya membuat request HTTP, sedangkan mengirim email butuh server yang berbicara dengan mail server. Form backend adalah server itu: arahkan action formulir ke sana dan ia mengirimi Anda email, sehingga situs Anda sendiri tetap bisa statis.

### Mengapa tidak memakai mailto: di action formulir?

Action mailto: tidak mengirim apa pun. Ia meminta komputer pengunjung membuka aplikasi email dengan draf, yang gagal bagi siapa pun yang belum menyiapkannya, dan pengunjung tetap harus menekan Send. Banyak orang menyerah di situ, dan Anda tidak pernah tahu mereka sempat mencoba.

### Bisakah saya mengubah baris subjek email notifikasi?

Tidak. Subjeknya selalu "New submission:" diikuti nama formulir, jadi beri setiap formulir nama yang jelas. Field _subject disimpan dan diteruskan ke webhook dan API, tetapi tidak mengubah email.

### Berapa banyak orang yang bisa menerima email?

Hingga 10 per formulir: anggota workspace yang Anda centang di pengaturan formulir, ditambah hingga 5 alamat luar yang mengonfirmasi lewat link. Masing-masing mendapat salinannya sendiri.

### Bisakah Ikiform mengirim email ke orang yang mengisi formulir?

Tidak. Ikiform mengirim email ke Anda dan tim, bukan ke pengirim formulir, jadi tidak ada autoresponder atau email konfirmasi. Kirim webhook ke alat email Anda dan kirim konfirmasinya dari sana.

### Apakah email formulir akan masuk spam?

Email dikirim dari domain Ikiform sendiri, jadi Anda tidak butuh record SPF atau DKIM untuk domain Anda. Email pertama tetap bisa masuk spam atau promosi; tambahkan notifications@ikiform.com ke kontak Anda atau buat filter agar tidak pernah terjadi.

### Apakah Ikiform gratis?

Tidak, tidak ada paket gratis. Pengujian gratis: Anda bisa mengirim 50 kiriman uji per bulan dari localhost sebelum membayar. Untuk go live, Pro mulai $6/bln untuk 500 kiriman, atau Anda bisa bayar sekali untuk 10.000 kiriman seharga $49 atau 50.000 kiriman seharga $149, dengan kredit yang tidak pernah kedaluwarsa.

## Panduan lainnya

- [Tanpa PHP](https://ikiform.com/id/guides/send-form-to-email/without-php.md): Ganti skrip contact.php yang memanggil mail() dengan formulir yang mengirim ke endpoint hosted, dengan nama field, halaman terima kasih, dan balasan tetap sama.
- [Ke Gmail](https://ikiform.com/id/guides/send-form-to-email/gmail.md): Terima kiriman formulir di Gmail tanpa link mailto:, app password, atau Apps Script, lalu beri label, jauhkan dari spam, dan balas dari Gmail.
- [Beberapa penerima](https://ikiform.com/id/guides/send-form-to-email/multiple-recipients.md): Kirim email dari satu formulir ke seluruh tim, atau arahkan setiap pesan ke orang yang tepat berdasarkan topik, tanpa membiarkan formulir sendiri memilih siapa yang menerima email.
- [Balas dari inbox Anda](https://ikiform.com/id/guides/send-form-to-email/reply-to.md): Buat Reply di notifikasi formulir membalas ke orang yang mengisi formulir: nama field yang berfungsi, kapan memakai _replyto, dan mengapa balasan bisa nyasar.
- [Alternatif EmailJS](https://ikiform.com/id/guides/send-form-to-email/emailjs-alternative.md): Pindahkan formulir kontak dari EmailJS: mengapa key email di sisi browser berisiko, apa yang berubah di kode Anda, dan apa yang Anda lepaskan, seperti balasan otomatis.
- [Next.js](https://ikiform.com/id/guides/send-form-to-email/nextjs.md): Dua cara mengirim email dari formulir Next.js dengan Server Action: kirim sendiri dengan Resend atau Nodemailer, atau teruskan ke form backend, dibandingkan secara jujur.
- [Hentikan spam formulir kontak](https://ikiform.com/id/guides/spam-protection.md): Hentikan spam formulir kontak dengan lapisan yang tidak membebani orang sungguhan: honeypot, waktu pengisian, skor konten, rate limit, dan pemeriksaan manusia hanya bila perlu.
- [Melampaui formulir builder](https://ikiform.com/id/guides/website-builder-forms.md): Saat formulir Webflow, Netlify, Carrd, atau Framer mentok di batas atau kekurangan fitur, pertahankan formulir yang Anda desain dan arahkan ke backend formulir.
- [Formulir feedback untuk website atau aplikasi](https://ikiform.com/id/guides/feedback-form.md): Pasang formulir feedback di halaman mana pun atau di dalam aplikasi Anda: rating, komentar, halaman asalnya, filter spam, dan peringatan saat skornya rendah.
