# Buat formulir lead capture yang cepat sampai ke Anda

> Formulir lead capture di situs Anda sendiri yang mengirimi Anda email dengan reply-to, memberi tahu chat tim, dan mengisi CRM, agar Anda menjawab selagi lead masih ada.

Terbit 2026-10-25 · https://ikiform.com/id/guides/lead-capture

Taruh formulir singkat di halaman Anda sendiri yang mengirim nama, email kantor, dan satu pertanyaan kualifikasi ke backend formulir. Aktifkan email notifikasi agar setiap lead masuk ke inbox Anda dengan **Reply** mengarah ke lead tersebut, dan kirim webhook bertanda tangan ke chat tim dan CRM Anda. Lalu ukur menit dari submit hingga balasan pertama, karena angka itu lebih menentukan kesepakatan daripada desain formulirnya.

Formulir lead adalah formulir kontak dengan satu tugas: memulai percakapan penjualan. Orang yang mengisinya sedang membandingkan Anda dengan pihak lain, sering di tab lain. Tim yang menjawab pertama, dengan balasan berguna dari orang sungguhan, biasanya mendapat panggilannya. Panduan ini membangun formulir di situs Anda sendiri, mengantarkan setiap lead kepada Anda dalam hitungan detik, dan membiarkan CRM opsional.

## Perjalanan sebuah lead

Formulir mengirim ke Ikiform. Ikiform memeriksa kiriman dari spam, menyimpannya, lalu melakukan tiga hal sekaligus: mengirim email ke tim Anda dengan lead sebagai alamat reply-to, mengirim webhook bertanda tangan, dan menampilkan lead di dashboard. Webhook adalah cara lead sampai ke chat tim dan CRM Anda.

Diagram: Bagaimana lead berpindah dari formulir Anda ke orang yang menjawabnya

1. Pengunjung mengisi formulir lead di situs Anda, dan formulir mengirim ke endpoint Ikiform Anda.
2. Ikiform memeriksa honeypot, skor spam, dan rate limit, lalu menyimpan lead.
3. Beberapa detik kemudian setiap penerima notifikasi mendapat email, dan menekan Balas akan menulis ke lead.
4. Lead muncul di dashboard, tempat Anda bisa mencari, memfilter, dan mengekspornya.
5. Ikiform mengirim webhook submission.created yang bertanda tangan.
6. Relay atau alat otomasi mengirim lead ke chat tim dan membuat kontak di CRM Anda.

Email adalah jalur tercepat tanpa penyiapan. Webhook menambahkan tempat-tempat yang sudah dipantau tim Anda.

## Empat cara menangkap lead

Setiap penyiapan umum unggul di sesuatu. Pilih berdasarkan di mana Anda ingin lead disimpan dan siapa yang merawat formulirnya.

| Pendekatan | Unggul dalam | Kekurangan |
| --- | --- | --- |
| Formulir bawaan CRM Anda, seperti HubSpot forms atau web form Pipedrive | Lead masuk ke CRM sebagai kontak tanpa perekat apa pun. Pelacakan dan lead scoring ada di satu produk. | Formulir di-embed dengan script dan gaya milik CRM. Pindah CRM berarti membangun ulang setiap formulir. Beberapa fitur bergantung pada paket. |
| Embed form builder, seperti [Typeform](https://ikiform.com/alternatives/typeform) atau [Tally](https://ikiform.com/alternatives/tally) | Formulir multi-langkah yang rapi dengan logika dan tanpa kode. | Embed yang terlihat seperti builder-nya kecuali Anda membayar untuk mengubah gayanya. Webhook dan batas respons bergantung pada paket. |
| Route API dan database Anda sendiri | Logika apa pun yang Anda inginkan, seperti lead scoring sebelum menyimpan apa pun. | Anda membangun sendiri filter spam, rate limit, peringatan email, percobaan ulang, dan ekspor, serta menjaga server tetap berjalan untuk satu formulir. |
| Form backend, seperti Ikiform | HTML Anda sendiri di situs apa pun. Filter spam, peringatan email dengan reply-to, webhook bertanda tangan, dan ekspor CSV tanpa server. | Tanpa lead scoring, tanpa record CRM, dan tanpa email ke lead. Semua itu datang dari CRM atau alat email Anda, lewat webhook. |

Jika seluruh proses penjualan Anda berjalan di dalam satu CRM dan Anda puas dengan formulirnya, gunakan itu. Backend formulir cocok saat formulir harus sesuai dengan situs Anda, saat Anda mungkin berganti CRM, atau saat Anda ingin formulir yang sama sampai ke email, chat, dan CRM tanpa harus memilih salah satu.

## Tanya sedikit, kualifikasi sekali

Setiap field yang Anda tambahkan mengurangi jumlah kiriman. Sebagian besar formulir lead butuh empat hal: kepada siapa membalas, di mana mereka bekerja, apa yang mereka inginkan, dan ruang untuk menjelaskan. Satu pertanyaan pilihan tetap melakukan kualifikasi, karena jawabannya datang sebagai salah satu dari beberapa nilai yang dikenal sehingga bisa dipercaya oleh routing Anda. Coba di bawah; preview menjalankan validator Ikiform di browser Anda dan menampilkan apa yang akan disimpan, atau `422` yang akan Anda terima.

Formulir: Talk to sales

- **Nama** (`name`, text, wajib)
- **Email kantor** (`email`, email, wajib)
- **Perusahaan** (`company`, text)
- **Apa yang bisa kami bantu?** (`interest`, select, wajib)
- **Ada yang perlu kami ketahui?** (`message`, textarea)

Tempel definisi ini di tab **Fields** formulir dan Ikiform menegakkannya di server. [Template formulir kontak](https://ikiform.com/forms/contact-form) punya versi lebih ringan dengan topik alih-alih minat.

| Field | Alasan | Perhatikan |
| --- | --- | --- |
| `name` | Balasan yang dimulai dengan nama mereka | Pertahankan ID `name`. Tautan di field nama adalah sinyal spam yang kuat |
| `email` | Tujuan **Reply** | Pertahankan ID `email`. Memberi label **Work email** (email kantor) mendorong orang memakainya, tetapi tidak mewajibkannya |
| `company` | Cari tahu tentang mereka sebelum Anda menjawab | Opsional. Mewajibkannya mengurangi kiriman dari freelancer |
| `interest` | Routing dan prioritas | `select` menyimpan satu nilai yang terdaftar atau menolak kiriman, jadi otomasi bisa bercabang berdasarkan nilainya |
| `message` | Konteks untuk balasan pertama | Opsional. Kotak esai yang wajib diisi adalah field tempat orang menyerah |

Nomor telepon, ukuran tim, dan anggaran cocok di formulir yang jawabannya mengubah langkah Anda berikutnya. [Template permintaan penawaran](https://ikiform.com/forms/quote-request) adalah versi lengkapnya, dengan layanan, anggaran, dan jadwal sebagai pilihan tetap. Jika sales menelepon setiap lead, minta nomor telepon. Jika Anda melakukan routing berdasarkan ukuran tim, tanyakan sebagai `select`. Selain itu, biarkan untuk balasan pertama.

## Formulirnya

Pilih stack Anda. Setiap versi menampilkan pesan server di bawah field yang bersangkutan dan menyimpan apa yang diketik pengunjung, karena kehilangan pesan yang setengah ditulis gara-gara satu salah ketik adalah cara lead menyerah. Status sukses menyebutkan siapa yang membalas dan kapan, sehingga tidak ada yang bertanya-tanya apakah berhasil.

**HTML**

Formulir mengirim dengan cara biasa dan me-redirect ke halaman terima kasih Anda, jadi bekerja sebelum script apa pun termuat. Setiap field punya slot kosong untuk error-nya.

`contact-sales.html`:

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

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

  <label for="company">Company</label>
  <input id="company" name="company" autocomplete="organization" />

  <label for="interest">What can we help with?</label>
  <select id="interest" name="interest" required>
    <option value="pricing">Pricing for my team</option>
    <option value="demo">A live demo</option>
    <option value="custom">A custom plan or contract</option>
    <option value="other">Something else</option>
  </select>

  <label for="message">Anything we should know?</label>
  <textarea id="message" name="message" rows="4"></textarea>
  <p class="error" data-error-for="message" hidden></p>

  <input type="hidden" name="page" value="pricing" />
  <input type="hidden" name="_redirect" value="https://example.com/thanks" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Talk to sales</button>
</form>
```

Script menjaga pengunjung tetap di halaman. Script menampilkan error dari server di bawah field masing-masing, dan mengganti formulir dengan baris yang menyebutkan siapa yang membalas dan kapan.

`lead-form.js`:

```js
const form = document.getElementById("lead-form");

function showErrors(fields) {
  form.querySelectorAll("[data-error-for]").forEach((slot) => {
    const message = fields[slot.dataset.errorFor]?.[0];
    slot.textContent = message ?? "";
    slot.hidden = !message;
  });
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button");
  button.disabled = true;
  button.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 thanks = document.createElement("p");
      thanks.setAttribute("role", "status");
      thanks.textContent = `Thanks, ${form.elements.name.value.trim().split(" ")[0]}. A person on our team will reply to ${form.elements.email.value} within one working day.`;
      form.replaceWith(thanks);
      return;
    }
    showErrors(result.error.fields ?? {});
    if (!result.error.fields) alert(result.error.message);
  } catch {
    alert("Something went wrong. Email sales@example.com and we'll pick it up.");
  }
  button.disabled = false;
  button.textContent = "Talk to sales";
});
```

**Next.js**

Server action meneruskan lead dengan secret key Anda dan mengembalikan error pertama untuk setiap field, plus nilai yang diketik pengunjung, sehingga tidak ada yang hilang saat `422`.

`app/contact-sales/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface LeadState {
  sent: boolean;
  firstName?: string;
  errors: Record<string, string>;
  values: Record<string, string>;
}

const KEYS = ["name", "email", "company", "interest", "message", "page", "_gotcha"] as const;

export async function sendLead(_previous: LeadState, formData: FormData): Promise<LeadState> {
  const values = Object.fromEntries(KEYS.map((key) => [key, String(formData.get(key) ?? "").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),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { sent: true, firstName: values.name.split(" ")[0], errors: {}, values: {} };

  const fields: Record<string, string[]> = result?.error?.fields ?? {};
  const errors = Object.fromEntries(Object.entries(fields).map(([key, messages]) => [key, messages[0] ?? ""]));
  if (!Object.keys(errors).length) errors.form = result?.error?.message ?? "Something went wrong. Email sales@example.com and we'll pick it up.";
  return { sent: false, errors, values };
}
```

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

```tsx
"use client";

import { useActionState } from "react";
import { type LeadState, sendLead } from "./actions";

const initial: LeadState = { sent: false, errors: {}, values: {} };

export function LeadForm() {
  const [state, action, pending] = useActionState(sendLead, initial);
  const { errors, values } = state;

  if (state.sent) {
    return <p role="status">Thanks, {state.firstName}. A person on our team will reply within one working day.</p>;
  }

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

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

      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" defaultValue={values.company} />

      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest" defaultValue={values.interest ?? "pricing"}>
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>

      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} defaultValue={values.message} />
      {errors.message ? <p role="alert">{errors.message}</p> : null}

      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
    </form>
  );
}
```

Ingin lebih jauh dengan Next.js? Baca [Buat formulir lead dengan kualifikasi di Next.js](https://ikiform.com/id/guides/lead-capture/nextjs.md).

**Astro**

Zod memeriksa field sebelum apa pun keluar dari server Anda, dan `isInputError` memberi Anda pesannya per field. Halaman harus dirender on demand agar formulir bisa mengirim ke action.

`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 = {
  sendLead: defineAction({
    accept: "form",
    input: z.object({
      name: z.string().trim().min(1, "Tell us who to reply to."),
      email: z.email("Enter a work email we can reply to."),
      company: z.string().trim().optional(),
      interest: z.enum(["pricing", "demo", "custom", "other"]),
      message: z.string().trim().max(5000).optional(),
      page: z.string().default("pricing"),
      _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),
      });
      const result = await response.json();
      if (!result.ok) throw new ActionError({ code: "BAD_REQUEST", message: result.error.message });
      return { firstName: input.name.split(" ")[0] };
    },
  }),
};
```

`src/pages/contact-sales.astro`:

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

export const prerender = false;

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

{result?.data ? (
  <p role="status">Thanks, {result.data.firstName}. A person on our team will reply within one working day.</p>
) : (
  <form method="POST" action={actions.sendLead}>
    <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">Work email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    {fields.email && <p role="alert">{fields.email[0]}</p>}

    <label for="company">Company</label>
    <input id="company" name="company" autocomplete="organization" />

    <label for="interest">What can we help with?</label>
    <select id="interest" name="interest">
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>

    <label for="message">Anything we should know?</label>
    <textarea id="message" name="message" rows="4"></textarea>

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

Ingin lebih jauh dengan Astro? Baca [Buat formulir lead dengan kualifikasi di Astro](https://ikiform.com/id/guides/lead-capture/astro.md).

**TanStack Start**

Server function membaca `FormData` di `.validator()`, meneruskannya, dan mengembalikan nama depan atau peta error per field yang dirender route di bawah setiap input.

`src/routes/contact-sales.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 Result = { ok: true; firstName: string } | { ok: false; errors: Record<string, string> };

const sendLead = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    const read = (key: string) => String(data.get(key) ?? "").trim();
    return {
      name: read("name"),
      email: read("email"),
      company: read("company"),
      interest: read("interest"),
      message: read("message"),
      page: read("page"),
      _gotcha: read("_gotcha"),
    };
  })
  .handler(async ({ data }): Promise<Result> => {
    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),
    });
    const result = await response.json();
    if (result.ok) return { ok: true, firstName: data.name.split(" ")[0] ?? "" };
    const fields: Record<string, string[]> = result.error.fields ?? { form: [result.error.message] };
    return { ok: false, errors: Object.fromEntries(Object.entries(fields).map(([key, list]) => [key, list[0] ?? ""])) };
  });

export const Route = createFileRoute("/contact-sales")({ component: ContactSales });

function ContactSales() {
  const send = useServerFn(sendLead);
  const [result, setResult] = useState<Result | null>(null);
  const [pending, setPending] = useState(false);
  const errors = result && !result.ok ? result.errors : {};

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    try {
      setResult(await send({ data: new FormData(event.currentTarget) }));
    } catch {
      setResult({ ok: false, errors: { form: "Something went wrong. Email sales@example.com and we'll pick it up." } });
    } finally {
      setPending(false);
    }
  }

  if (result?.ok) {
    return <p role="status">Thanks, {result.firstName}. A person on our team will reply within one working day.</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">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email}</p> : null}
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" />
      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest">
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>
      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} />
      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
    </form>
  );
}
```

Ingin lebih jauh dengan TanStack Start? Baca [Buat formulir lead dengan kualifikasi di TanStack Start](https://ikiform.com/id/guides/lead-capture/tanstack-start.md).

**React Router**

Route action meneruskan kiriman dan mengembalikan error field beserta kode status Ikiform lewat `data()`. Komponen membacanya dari `actionData`.

`app/routes/contact-sales.tsx`:

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

const KEYS = ["name", "email", "company", "interest", "message", "page", "_gotcha"];

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const values = Object.fromEntries(KEYS.map((key) => [key, String(formData.get(key) ?? "").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(values),
  });
  const result = await response.json();
  if (result.ok) return { sent: true as const, firstName: values.name.split(" ")[0] };
  const fields: Record<string, string[]> = result.error.fields ?? { form: [result.error.message] };
  return data(
    { sent: false as const, errors: Object.fromEntries(Object.entries(fields).map(([key, list]) => [key, list[0]])) },
    { status: response.status },
  );
}

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

  if (actionData?.sent) {
    return <p role="status">Thanks, {actionData.firstName}. A person on our team will reply within one working day.</p>;
  }
  const errors: Record<string, string | undefined> = actionData?.errors ?? {};

  return (
    <Form method="post">
      <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">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email}</p> : null}
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" />
      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest">
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>
      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} />
      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
    </Form>
  );
}
```

Ingin lebih jauh dengan React Router? Baca [Buat formulir lead dengan kualifikasi di React Router](https://ikiform.com/id/guides/lead-capture/react-router.md).

**SvelteKit**

Form action mengembalikan `fail()` dengan nilai dan error-nya, dan `update({ reset: false })` menjaga apa yang diketik pengunjung tetap di layar selagi mereka memperbaiki satu field. Deklarasikan `IKIFORM_SECRET_KEY` di `src/env.ts` dengan `defineEnvVars` agar `$app/env/private` bisa membacanya.

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

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

const KEYS = ["name", "email", "company", "interest", "message", "page", "_gotcha"];

export const actions = {
  default: async ({ request, fetch, url }) => {
    const data = await request.formData();
    const values = Object.fromEntries(KEYS.map((key) => [key, String(data.get(key) ?? "").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),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields: Record<string, string[]> = result.error.fields ?? { form: [result.error.message] };
      const errors = Object.fromEntries(Object.entries(fields).map(([key, list]) => [key, list[0]]));
      return fail(response.status, { values, errors });
    }
    return { firstName: values.name.split(" ")[0] };
  },
} satisfies Actions;
```

`src/routes/contact-sales/+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 errors = $derived<Record<string, string | undefined>>(form?.errors ?? {});
</script>

{#if form?.firstName}
  <p role="status">Thanks, {form.firstName}. A person on our team will reply within one working day.</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 value={form?.values?.name ?? ""} />
    {#if errors.name}<p role="alert">{errors.name}</p>{/if}

    <label for="email">Work email</label>
    <input id="email" name="email" type="email" autocomplete="email" required value={form?.values?.email ?? ""} />
    {#if errors.email}<p role="alert">{errors.email}</p>{/if}

    <label for="company">Company</label>
    <input id="company" name="company" autocomplete="organization" value={form?.values?.company ?? ""} />

    <label for="interest">What can we help with?</label>
    <select id="interest" name="interest" value={form?.values?.interest ?? "pricing"}>
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>

    <label for="message">Anything we should know?</label>
    <textarea id="message" name="message" rows="4">{form?.values?.message ?? ""}</textarea>

    <input type="hidden" name="page" value="pricing" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if errors.form}<p role="alert">{errors.form}</p>{/if}
    <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
  </form>
{/if}
```

Ingin lebih jauh dengan SvelteKit? Baca [Buat formulir lead dengan kualifikasi di SvelteKit](https://ikiform.com/id/guides/lead-capture/sveltekit.md).

**Nuxt**

Server route menjauhkan key dari klien dan meneruskan error field dari Ikiform di `data` milik error. Atur `NUXT_IKIFORM_SECRET_KEY` di environment.

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

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<Record<string, string | undefined>>(event);
  const config = useRuntimeConfig(event);
  const origin = getRequestHeader(event, "origin");
  const keys = ["name", "email", "company", "interest", "message", "page", "_gotcha"];
  const values = Object.fromEntries(keys.map((key) => [key, String(body[key] ?? "").trim()]));

  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(values),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: result.error.message,
      data: { fields: result.error.fields ?? {} },
    });
  }
  return { firstName: values.name?.split(" ")[0] };
});
```

`nuxt.config.ts`:

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

`app/components/LeadForm.vue`:

```vue
<script setup lang="ts">
const lead = reactive({ name: "", email: "", company: "", interest: "pricing", message: "", _gotcha: "" });
const errors = ref<Record<string, string[]>>({});
const failure = ref("");
const sending = ref(false);
const firstName = ref("");

async function send() {
  sending.value = true;
  errors.value = {};
  failure.value = "";
  try {
    const result = await $fetch("/api/lead", { method: "POST", body: { ...lead, page: "pricing" } });
    firstName.value = result.firstName ?? "there";
  } catch (error) {
    const data = (error as { data?: { message?: string; data?: { fields?: Record<string, string[]> } } }).data;
    errors.value = data?.data?.fields ?? {};
    if (!Object.keys(errors.value).length) failure.value = data?.message ?? "Something went wrong. Email sales@example.com and we'll pick it up.";
  } finally {
    sending.value = false;
  }
}
</script>

<template>
  <p v-if="firstName" role="status">Thanks, {{ firstName }}. A person on our team will reply within one working day.</p>
  <form v-else @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" v-model="lead.name" name="name" autocomplete="name" required />
    <p v-if="errors.name" role="alert">{{ errors.name[0] }}</p>
    <label for="email">Work email</label>
    <input id="email" v-model="lead.email" name="email" type="email" autocomplete="email" required />
    <p v-if="errors.email" role="alert">{{ errors.email[0] }}</p>
    <label for="company">Company</label>
    <input id="company" v-model="lead.company" name="company" autocomplete="organization" />
    <label for="interest">What can we help with?</label>
    <select id="interest" v-model="lead.interest" name="interest">
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>
    <label for="message">Anything we should know?</label>
    <textarea id="message" v-model="lead.message" name="message" rows="4" />
    <input v-model="lead._gotcha" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="failure" role="alert">{{ failure }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Talk to sales" }}</button>
  </form>
</template>
```

Ingin lebih jauh dengan Nuxt? Baca [Buat formulir lead dengan kualifikasi di Nuxt](https://ikiform.com/id/guides/lead-capture/nuxt.md).

**React**

Tanpa server sendiri, formulir mengirim langsung ke Ikiform dari browser. Jauhkan secret key dari kode ini; **Allowed domains** yang melindungi formulir sebagai gantinya.

`src/LeadForm.tsx`:

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

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

export function LeadForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
  const [errors, setErrors] = useState<Record<string, string[]>>({});
  const [firstName, setFirstName] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    setStatus("sending");
    setErrors({});
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
      const result = await response.json();
      if (result.ok) {
        setFirstName(String(body.get("name")).trim().split(" ")[0] ?? "");
        setStatus("sent");
        return;
      }
      setErrors(result.error.fields ?? { form: [result.error.message] });
    } catch {
      setErrors({ form: ["Something went wrong. Email sales@example.com and we'll pick it up."] });
    }
    setStatus("idle");
  }

  if (status === "sent") {
    return <p role="status">Thanks, {firstName}. A person on our team will reply within one working day.</p>;
  }

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {errors.name ? <p role="alert">{errors.name[0]}</p> : null}
      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email[0]}</p> : null}
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" />
      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest">
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>
      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} />
      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form[0]}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Talk to sales"}</button>
    </form>
  );
}
```

**Vue**

Kiriman sisi browser yang sama sebagai komponen Vue. Formulir tetap punya `action`, jadi pengunjung yang script-nya gagal dimuat tetap bisa menghubungi Anda.

`src/components/LeadForm.vue`:

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

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

const status = ref<"idle" | "sending" | "sent">("idle");
const errors = ref<Record<string, string[]>>({});
const firstName = ref("");

async function send(event: SubmitEvent) {
  const body = new FormData(event.target as HTMLFormElement);
  status.value = "sending";
  errors.value = {};
  try {
    const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      firstName.value = String(body.get("name")).trim().split(" ")[0] ?? "";
      status.value = "sent";
      return;
    }
    errors.value = result.error.fields ?? { form: [result.error.message] };
  } catch {
    errors.value = { form: ["Something went wrong. Email sales@example.com and we'll pick it up."] };
  }
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'sent'" role="status">Thanks, {{ firstName }}. A person on our team will reply within one working day.</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="errors.name" role="alert">{{ errors.name[0] }}</p>
    <label for="email">Work email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    <p v-if="errors.email" role="alert">{{ errors.email[0] }}</p>
    <label for="company">Company</label>
    <input id="company" name="company" autocomplete="organization" />
    <label for="interest">What can we help with?</label>
    <select id="interest" name="interest">
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>
    <label for="message">Anything we should know?</label>
    <textarea id="message" name="message" rows="4" />
    <input type="hidden" name="page" value="pricing" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="errors.form" role="alert">{{ errors.form[0] }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Talk to sales" }}</button>
  </form>
</template>
```

- Ganti `frm_xxxxxxxxxxxxxxxx` dengan ID formulir Anda dari dashboard.
- Input tersembunyi `_gotcha` adalah honeypot. Orang tidak pernah melihatnya; bot yang mengisi setiap input menandai dirinya sendiri sebagai spam.
- Input tersembunyi `page` mencatat halaman asal lead: harga, halaman fitur, landing page kampanye. Jika Anda mendefinisikan field, tambahkan `page` sebagai field teks opsional, atau kiriman ditolak dengan `Unknown field`.
- Versi server meneruskan header `Origin` pengunjung, karena fetch di sisi server tidak mengirimnya dan Ikiform mendeteksi uji `localhost` gratis dari header itu.
- Formulir HTML biasa butuh **Redirect after submit**, atau input `_redirect` di salah satu **Allowed domains** Anda, agar mendarat di halaman terima kasih Anda sendiri.

> 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.

Dokumentasi membahas penyiapan setiap framework secara mendalam: [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), [mengirim dari server](https://ikiform.com/id/docs/guides/server.md), dan [HTML biasa](https://ikiform.com/id/docs/html.md).

## Hubungi lead dalam hitungan menit

Formulir adalah bagian yang mudah. Yang membedakan formulir lead yang menjual dari yang sekadar mengumpulkan adalah waktu antara submit dan balasan dari seseorang. Beginilah jalur itu saat berjalan baik:

Diagram: Menit-menit antara submit dari lead dan balasan pertama Anda

1. Lead mengirim formulir.
2. Ikiform menyimpannya dan menjawab browser dalam waktu jauh di bawah satu detik.
3. Email notifikasi dan webhook keluar dari antrean beberapa detik kemudian.
4. Seseorang di tim menekan Balas di inbox-nya, atau tautan mailto di chat, dan balasannya menuju ke lead.

Semua hingga email hanya butuh hitungan detik. Panah terakhir adalah yang Anda kendalikan, dan yang memenangkan kesepakatan.

### Membalas dari inbox Anda

Setiap email notifikasi mengatur alamat reply-to ke lead. Ikiform mengambilnya dari field `_replyto` lebih dulu, lalu dari field bernama `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. Pertahankan ID field `email` dan **Reply** menuju ke lead, bukan ke Ikiform. Subjeknya adalah `New submission:` ditambah nama formulir, jadi beri nama formulir sesuai fungsinya, seperti **Talk to sales**, dan filter berdasarkan subjek itu untuk memberi label atau bintang pada lead.

Tambahkan hingga 10 penerima per formulir di pengaturan notifikasi formulir: anggota workspace, atau alamat luar yang mengonfirmasi lewat email terlebih dahulu. Kirim ke orang yang membalas, bukan ke alias bersama yang tidak dimiliki siapa pun: lead yang ada di lima inbox dan tidak diklaim siapa pun menunggu lebih lama daripada lead di satu inbox.

> Email notifikasi dibatasi 50 per formulir per jam, atau 10 selama workspace berumur kurang dari 7 hari, lalu satu ringkasan untuk sisa jam itu. Formulir lead normal tidak pernah mendekati angka itu. Jika Anda menjalankan kampanye yang mungkin mencapainya, teruskan juga lead ke chat lewat webhook.

### Beri tahu tim di tempat mereka berada

Email berjalan baik saat ada yang memegang inbox-nya. Tim yang hidup di chat menjawab lebih cepat saat lead muncul di sana. Tambahkan webhook ke Slack, Teams, atau Discord mengikuti [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Microsoft Teams](https://ikiform.com/blog/form-submissions-to-microsoft-teams.md), atau [Discord](https://ikiform.com/blog/form-submissions-to-discord.md). Taruh tautan `mailto:` di pesan agar orang yang mengklaimnya bisa membalas dengan satu klik.

### Routing berdasarkan apa yang mereka minta

Karena `interest` hanya menerima nilai yang terdaftar, relay kecil bisa bercabang berdasarkan nilai itu dengan yakin. Route ini mengirim permintaan demo dan pertanyaan kontrak ke channel masing-masing dan semua yang lain ke channel sales utama:

`app/api/leads/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

const CHANNELS: Record<string, string | undefined> = {
  demo: process.env.SLACK_DEMO_WEBHOOK,
  custom: process.env.SLACK_ENTERPRISE_WEBHOOK,
};

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({ payload, headers: request.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const lead = event.data.fields;
  const interest = String(lead.interest ?? "other");
  const target = CHANNELS[interest] ?? process.env.SLACK_SALES_WEBHOOK!;
  const text = [
    `*${lead.name}* from ${lead.company || "an unknown company"} wants: ${interest}`,
    `Reply: mailto:${lead.email}`,
    lead.message ? `> ${String(lead.message).slice(0, 300)}` : "",
  ]
    .filter(Boolean)
    .join("\n");

  const response = await fetch(target, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ text }),
  });
  if (!response.ok) return new Response(`Slack answered ${response.status}`, { status: 502 });
  return new Response(null, { status: 204 });
}
```

- `verifyWebhook` memeriksa tanda tangan dan timestamp, jadi tidak ada orang lain yang bisa mengirim lead palsu ke channel Anda.
- `502` saat Slack down membuat Ikiform mencoba ulang pengiriman yang sama, dengan `webhook-id` yang sama, selama sekitar 3 hari.
- Ubah label opsi sesuka Anda. Jaga nilainya tetap stabil begitu relay atau otomasi bergantung padanya.

## Jauhkan sampah dari pipeline

Formulir lead menarik jenis spam tertentu: orang yang menjual jasa SEO, link building, dan outsourcing development kepada Anda. Spam itu membuang balasan tercepat yang Anda punya, jadi saring sebelum sampai ke siapa pun.

| Perlindungan | Yang dihentikan | Setup |
| --- | --- | --- |
| Honeypot | Bot yang mengisi setiap input | Input tersembunyi `_gotcha` |
| Skor spam | Frasa penawaran penjualan yang dikenal seperti `seo services` (+2), tautan di nama (+3), tiga tautan atau lebih (+2), pengulangan (+3). Pada skor 5 dianggap spam | Tidak ada |
| Rate limit | Skrip yang mengirim berulang-ulang | Atur formulir ke Strict, 2 per 10 menit per pengunjung. Lead sungguhan jarang mengirim dua kali dalam sepuluh menit |
| Domain yang diizinkan | Kiriman dari situs lain | Tambahkan domain Anda di **Allowed domains** |
| Pemeriksaan manusia | Bot di browser sungguhan | Atur **Spam check** ke **Ikiform**. Versi `fetch` kemudian butuh token dari `challenge()` |

Spam mendapat jawaban sukses yang sama dengan lead asli, tidak mengirim email atau webhook, dan tidak dihitung dalam pemakaian. Itu juga berarti lead yang salah dinilai tidak bersuara. Periksa filter **Spam** seminggu sekali; menandai satu sebagai bukan spam akan mengirim email dan webhook-nya saat itu. [Dokumentasi spam](https://ikiform.com/id/docs/spam.md#scoring) mendaftar setiap sinyal.

## Ketahui asal lead

Setiap kiriman sudah mencatat origin dan referrer halaman, yang muncul di webhook sebagai `meta.origin` dan `meta.referrer`. Tag kampanye butuh sedikit bantuan, karena pengunjung sering mendarat di satu halaman dan mengirim dari halaman lain. Potongan kode ini menyalin parameter `utm_` ke input tersembunyi dan mengingatnya selama sesi:

`attribution.html`:

```html
<input type="hidden" name="utm_source" />
<input type="hidden" name="utm_medium" />
<input type="hidden" name="utm_campaign" />

<script>
  const params = new URLSearchParams(location.search);
  for (const key of ["utm_source", "utm_medium", "utm_campaign"]) {
    let value = params.get(key);
    try {
      if (value) sessionStorage.setItem(key, value);
      else value = sessionStorage.getItem(key);
    } catch {}
    const input = document.querySelector(`#lead-form [name="${key}"]`);
    if (input && value) input.value = value.slice(0, 200);
  }
</script>
```

Di framework, baca parameter yang sama dari URL di server dan render sebagai input tersembunyi. Jika field didefinisikan, tambahkan setiap ID `utm_` sebagai field teks opsional. Nama yang diawali `_` tidak pernah disimpan, jadi jangan dipakai untuk data yang ingin Anda simpan.

## Apa yang dilihat lead selanjutnya

Ikiform tidak mengirim email ke orang yang mengisi formulir. Email notifikasi hanya dikirim ke Anda dan tim Anda, 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). Untuk formulir lead itu sering kali tidak masalah: balasan pribadi yang cepat mengalahkan balasan otomatis. Jika Anda tetap ingin konfirmasi instan, kirim dari CRM atau alat email Anda saat webhook membuat kontak, agar balasan dan unsubscribe dilacak di satu tempat.

Apa pun yang Anda kirim, pesan sukses di halaman adalah janjinya. Sebutkan siapa yang membalas dan kapan, lalu tepati. Formulir yang menyebut satu hari kerja dan menjawab dalam sepuluh menit mendapat kepercayaan; yang menyebut sepuluh menit dan menjawab besok kehilangan kepercayaan.

## Lead di CRM Anda

Tidak ada integrasi sekali klik. Setiap alat dijangkau lewat [webhook](https://ikiform.com/id/docs/webhooks.md) bertanda tangan, langsung atau melalui alat otomasi seperti [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) atau [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md). Untuk CRM, artinya sebuah relay memetakan body webhook Ikiform yang tetap ke panggilan create-contact milik CRM. [Lead ke HubSpot atau Pipedrive](https://ikiform.com/id/guides/lead-capture/leads-to-crm.md) membahas keduanya, termasuk duplikat dan percobaan ulang. Sampai Anda butuh CRM, dashboard dan [ekspor CSV](https://ikiform.com/id/docs/guides/export.md) mingguan sudah menjadi pipeline yang cukup.

## Pilih situasi Anda

Penyiapan di atas berlaku untuk lead apa pun. Halaman-halaman ini membahas apa yang berubah untuk jenis lead tertentu, dan untuk memasukkan lead ke CRM.

- [Formulir permintaan demo](https://ikiform.com/id/guides/lead-capture/demo-request.md): Formulir pesan demo yang menyimpan lead, lalu mengarahkan pengunjung ke Cal.com atau Calendly dengan nama dan email yang sudah terisi.
- [Formulir permintaan penawaran](https://ikiform.com/id/guides/lead-capture/quote-request.md): Formulir minta penawaran yang mengumpulkan brief sebagai tautan bersama alih-alih unggahan, memberi tenggat pada setiap permintaan, dan menjauhkan lead yang penuh tautan dari spam.
- [Formulir permintaan konsultasi](https://ikiform.com/id/guides/lead-capture/consultation-request.md): Formulir permintaan konsultasi untuk akuntan, pengacara, coach, dan klinik yang menanyakan cara dan waktu menghubungi, dan menyimpan detail sensitif untuk panggilannya.
- [Formulir inquiry proyek](https://ikiform.com/id/guides/lead-capture/project-inquiry.md): Formulir mulai proyek untuk studio dan freelancer yang memilah inquiry menjadi hot, warm, dan tidak cocok berdasarkan anggaran dan waktu sebelum ada yang membaca brief-nya.
- [Lead ke HubSpot atau Pipedrive](https://ikiform.com/id/guides/lead-capture/leads-to-crm.md): Petakan setiap webhook Ikiform bertanda tangan ke contact upsert HubSpot atau panggilan person dan lead Pipedrive, tanpa kontak duplikat saat pengiriman dicoba ulang.
- [Formulir lead Next.js](https://ikiform.com/id/guides/lead-capture/nextjs.md): Formulir lead App Router Next.js yang memvalidasi dengan Zod di server action, menyimpan UTM first-touch di cookie, dan mengirim lead yang lolos kualifikasi ke kalender Anda.
- [Formulir lead Astro](https://ikiform.com/id/guides/lead-capture/astro.md): Formulir lead Astro berbasis Actions: validasi Zod, UTM first-touch di situs statis, halaman kontak on demand, dan redirect ke penjadwal Anda.
- [Formulir lead TanStack Start](https://ikiform.com/id/guides/lead-capture/tanstack-start.md): Formulir lead TanStack Start: global request middleware untuk UTM first-touch, server function dengan Zod, dan redirect yang dilempar ke penjadwal Anda.
- [Formulir lead React Router](https://ikiform.com/id/guides/lead-capture/react-router.md): Formulir lead React Router framework mode: root middleware dengan cookie first-touch bertanda tangan, route action yang memvalidasi, dan redirect ke penjadwal Anda.
- [Formulir lead SvelteKit](https://ikiform.com/id/guides/lead-capture/sveltekit.md): Formulir lead SvelteKit: hook handle mencatat UTM first-touch, form action memvalidasi dengan Valibot, dan redirect() mengarahkan lead yang lolos kualifikasi ke kalender.
- [Formulir lead Nuxt](https://ikiform.com/id/guides/lead-capture/nuxt.md): Formulir lead Nuxt: server middleware mencatat UTM first-touch, satu server route tervalidasi Zod melayani fetch dan kiriman biasa, dan lead yang bagus memesan panggilan.

## Pastikan berfungsi

Periksa daftar ini sebelum Anda mengarahkan trafik ke halaman. 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. Isi formulir di halaman Anda lalu kirim. Anda mendarat di halaman terima kasih Anda, atau halaman hosted milik Ikiform.
3. Buka **Submissions** di dashboard. Kiriman baru ada di sana dengan setiap field yang Anda kirim.
4. Periksa inbox setiap penerima. Subjeknya adalah `New submission:` ditambah nama formulir, dan **Reply** membuka pesan ke alamat yang Anda masukkan.
5. Jika Anda menambahkan webhook, buka endpoint-nya di **Webhooks**. Pengiriman menampilkan `succeeded` dengan jawaban dari server Anda.
6. Kirim dengan honeypot terisi, `_gotcha=bot`. Kiriman tetap mendapat jawaban sukses, tetapi disimpan sebagai spam dan tidak memicu email atau webhook.
7. Tidak ada yang masuk? Ubah filter status di dashboard ke **Spam**. Spam mendapat jawaban sukses yang sama dengan kiriman asli.
8. Ukur waktunya: kirim dari ponsel Anda dan catat berapa lama sampai email dan pesan chat tiba.

## Pemecahan masalah

| Masalah | Yang harus dilakukan |
| --- | --- |
| **Reply** menuju alamat Ikiform, bukan lead | Formulir tidak punya field yang dikenali Ikiform sebagai email lead. Ganti nama field menjadi `email`, atau kirim field `_replyto` dengan alamatnya. Nama yang diterima: `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. |
| `422` dengan `Unknown field` untuk `page` atau `utm_source` | Formulir punya definisi field dan input tersembunyi itu tidak ada di dalamnya. Tambahkan sebagai field teks opsional, atau hapus inputnya. |
| `422 validation_failed` | Kiriman tidak cocok dengan definisi field formulir. `error.fields` menyebutkan setiap field dan alasannya. Samakan `name` input dengan ID field. |
| `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. |
| `429 rate_limited` | Satu pengunjung mengirim melebihi rate limit formulir. Standard mengizinkan 5 per menit per pengunjung. Tunggu sesuai `Retry-After`, atau pilih Relaxed atau Custom untuk formulir yang mengirim dari satu IP bersama. |
| `403 challenge_required` | Spam check formulir diatur ke **Ikiform** dan kiriman `fetch` tidak membawa token. Ambil token dengan `challenge(formId)` dari `@ikiform/sdk` sebelum setiap pengiriman, atau matikan pemeriksaannya. |
| 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. |
| Penerima tidak mendapat email notifikasi | Alamat di luar workspace harus mengonfirmasi lebih dulu, dari email yang dikirim Ikiform kepadanya. Pastikan sudah dikonfirmasi, lalu periksa folder spam penerima. |

## Pertanyaan

### Apa itu formulir lead capture?

Formulir di website Anda yang mengumpulkan detail kontak calon pelanggan dan apa yang mereka inginkan, agar tim Anda bisa menindaklanjutinya. Bedanya dari formulir kontak umum adalah formulir ini mengkualifikasi lead, biasanya dengan satu pertanyaan pilihan tetap, dan dirangkai agar cepat sampai ke sales.

### Bagaimana cara mendapatkan kiriman formulir lead ke email saya?

Arahkan formulir ke endpoint Ikiform Anda dan biarkan email notifikasi aktif untuk formulir itu. Setiap lead tiba dalam hitungan detik, ke hingga 10 penerima, dan menekan Balas akan menulis ke lead selama field email bernama email atau formulir mengirim _replyto.

### Berapa banyak field yang sebaiknya dimiliki formulir lead capture?

Sesedikit mungkin selama Anda masih bisa membalas dengan baik: biasanya nama, email, satu pertanyaan kualifikasi, dan pesan opsional. Tambahkan telepon, anggaran, atau ukuran tim hanya jika jawabannya mengubah langkah tim Anda berikutnya.

### Bisakah Ikiform mengirim lead ke HubSpot, Pipedrive, atau Salesforce?

Lewat webhook, bisa. Tidak ada integrasi sekali klik, jadi relay kecil atau alat otomasi seperti Zapier, Make, atau n8n memetakan setiap webhook bertanda tangan ke API CRM dan membuat kontak atau lead.

### Apakah lead mendapat email konfirmasi?

Tidak dari Ikiform. Ikiform mengirim email ke tim Anda, bukan ke orang yang mengisi formulir. Kirim konfirmasi apa pun dari CRM atau alat email Anda, dipicu oleh webhook, atau balas secara pribadi dari email notifikasi.

### Bisakah saya memblokir alamat email gratis seperti Gmail?

Tidak dengan pengaturan bawaan. Opsi allowed domains pada field email adalah allowlist, yang cocok untuk formulir internal, bukan publik. Terima setiap alamat dan pilah lead dengan email gratis di relay atau CRM Anda; banyak pembeli sungguhan memakai alamat pribadi.

### 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

- [Formulir permintaan demo](https://ikiform.com/id/guides/lead-capture/demo-request.md): Formulir pesan demo yang menyimpan lead, lalu mengarahkan pengunjung ke Cal.com atau Calendly dengan nama dan email yang sudah terisi.
- [Formulir permintaan penawaran](https://ikiform.com/id/guides/lead-capture/quote-request.md): Formulir minta penawaran yang mengumpulkan brief sebagai tautan bersama alih-alih unggahan, memberi tenggat pada setiap permintaan, dan menjauhkan lead yang penuh tautan dari spam.
- [Formulir permintaan konsultasi](https://ikiform.com/id/guides/lead-capture/consultation-request.md): Formulir permintaan konsultasi untuk akuntan, pengacara, coach, dan klinik yang menanyakan cara dan waktu menghubungi, dan menyimpan detail sensitif untuk panggilannya.
- [Formulir inquiry proyek](https://ikiform.com/id/guides/lead-capture/project-inquiry.md): Formulir mulai proyek untuk studio dan freelancer yang memilah inquiry menjadi hot, warm, dan tidak cocok berdasarkan anggaran dan waktu sebelum ada yang membaca brief-nya.
- [Lead ke HubSpot atau Pipedrive](https://ikiform.com/id/guides/lead-capture/leads-to-crm.md): Petakan setiap webhook Ikiform bertanda tangan ke contact upsert HubSpot atau panggilan person dan lead Pipedrive, tanpa kontak duplikat saat pengiriman dicoba ulang.
- [Formulir lead Next.js](https://ikiform.com/id/guides/lead-capture/nextjs.md): Formulir lead App Router Next.js yang memvalidasi dengan Zod di server action, menyimpan UTM first-touch di cookie, dan mengirim lead yang lolos kualifikasi ke kalender Anda.
- [Formulir lead Astro](https://ikiform.com/id/guides/lead-capture/astro.md): Formulir lead Astro berbasis Actions: validasi Zod, UTM first-touch di situs statis, halaman kontak on demand, dan redirect ke penjadwal Anda.
- [Formulir lead TanStack Start](https://ikiform.com/id/guides/lead-capture/tanstack-start.md): Formulir lead TanStack Start: global request middleware untuk UTM first-touch, server function dengan Zod, dan redirect yang dilempar ke penjadwal Anda.
- [Formulir lead React Router](https://ikiform.com/id/guides/lead-capture/react-router.md): Formulir lead React Router framework mode: root middleware dengan cookie first-touch bertanda tangan, route action yang memvalidasi, dan redirect ke penjadwal Anda.
- [Formulir lead SvelteKit](https://ikiform.com/id/guides/lead-capture/sveltekit.md): Formulir lead SvelteKit: hook handle mencatat UTM first-touch, form action memvalidasi dengan Valibot, dan redirect() mengarahkan lead yang lolos kualifikasi ke kalender.
- [Formulir lead Nuxt](https://ikiform.com/id/guides/lead-capture/nuxt.md): Formulir lead Nuxt: server middleware mencatat UTM first-touch, satu server route tervalidasi Zod melayani fetch dan kiriman biasa, dan lead yang bagus memesan panggilan.
- [Formulir pendaftaran newsletter](https://ikiform.com/id/guides/newsletter-signup.md): Buat formulir pendaftaran newsletter dengan markup Anda sendiri, catat persetujuan, blokir pendaftaran bot, dan teruskan setiap subscriber ke Kit, MailerLite, beehiiv, atau alat apa pun.
- [Pengiriman formulir HTML](https://ikiform.com/id/guides/html-form-submission.md): Apa yang dilakukan browser saat formulir dikirim: field mana yang dikirim, bagaimana di-encode, apa yang seharusnya dijawab server, dan bagaimana Ikiform menjawab.
- [Pendaftaran acara](https://ikiform.com/id/guides/event-registration.md): Terima RSVP di halaman acara Anda sendiri: formulir, batas tamu, jumlah peserta yang jujur, menutup saat penuh, dan daftar check-in, tanpa aplikasi tiket.
