Formulir lead capture5 menit baca
Buat formulir permintaan demo yang langsung memesan panggilan
Formulir pesan demo yang menyimpan lead, lalu mengarahkan pengunjung ke Cal.com atau Calendly dengan nama dan email yang sudah terisi.
Kirim permintaan demo ke backend formulir lebih dulu, agar lead tersimpan dan tim Anda menerima email meskipun pengunjung tidak pernah memilih waktu. Lalu arahkan browser ke halaman booking Anda dengan name dan email di query string, yang dipakai Calendly dan Cal.com untuk mengisi formulirnya. Arahkan tim kecil ke tempat lain, seperti tur rekaman, agar kalender Anda berisi panggilan yang bisa closing.
Apa yang berbeda dari permintaan demo
Orang yang meminta demo menginginkan waktu di kalender, bukan utas email. Setiap putaran "Selasa bisa?" adalah kesempatan bagi mereka untuk memesan dengan kompetitor. Jadi tugas formulirnya singkat: tangkap cukup informasi untuk kualifikasi, lalu sodorkan kalender selagi tab-nya masih terbuka.
Tiga batasan membentuk penyiapannya:
- Lead harus disimpan sebelum penjadwal terbuka. Banyak pengunjung membuka halaman booking lalu menutupnya. Jika satu-satunya catatan tentang mereka ada di penjadwal, lead itu hilang. Kirim ke Ikiform lebih dulu dan redirect hanya jika berhasil.
_redirecttidak bisa membawa apa yang mereka ketik. Nilainya URL tetap, dan Ikiform hanya mengikutinya untuk URLhttps:di salah satu Allowed domains Anda. Menambahkancal.comataucalendly.comdi sana juga akan meloloskan kiriman dari situs-situs itu. Susun tautan booking di kode Anda sendiri.- Tidak setiap permintaan layak mendapat panggilan langsung. Founder solo yang meminta demo produk untuk tim lebih terbantu dengan tur dan tautan pendaftaran. Satu pertanyaan pilihan tetap menentukan ke mana browser pergi berikutnya.
- Pengunjung mengisi nama, email kantor, perusahaan, dan ukuran tim, lalu formulir mengirim ke Ikiform.
- Ikiform menyimpan permintaan dan mengirim email ke tim Anda. Tidak ada hal lain yang terjadi sampai Ikiform menjawab ok.
- Kode Anda membaca ukuran tim yang dipilih pengunjung.
- Tim berisi 11 orang atau lebih diarahkan ke halaman booking, dengan nama dan email di query string.
- Tim berisi 1 sampai 10 orang diarahkan ke tur rekaman dengan tautan pendaftaran.
Empat field, satu untuk routing
| Field | Tipe | Alasan |
|---|---|---|
name | teks | Mengisi formulir booking, jadi pengunjung cukup mengetiknya sekali |
email | Mengisi formulir booking dan menjadi reply-to di email notifikasi | |
company | teks, wajib di sini | Sales Anda mencarinya sebelum panggilan |
team_size | select | Menentukan halaman booking atau tur, dan tiba sebagai salah satu dari empat nilai yang dikenal |
Simpan kasus penggunaan, jadwal, dan nomor telepon untuk panggilannya sendiri. Penjadwal bisa mengajukan satu pertanyaan sendiri jika Anda butuh konteks sebelum rapat, dan Calendly maupun Cal.com juga bisa mengisi pertanyaan kustom.
Kirim, lalu serahkan
Setiap versi mengirim permintaan, menunggu jawaban Ikiform, dan baru kemudian menyusun URL berikutnya. Contohnya memakai Cal.com; untuk Calendly, ganti dengan tautan event Anda. Calendly mendokumentasikan name dan email sebagai parameter prefill, dan Cal.com mengisi field booking apa pun berdasarkan identifier-nya, termasuk name dan email.
Formulir dan script-nya sekaligus. Tanpa JavaScript, formulir tetap mengirim dan mendarat di halaman Redirect after submit Anda, jadi taruh tautan booking di sana sebagai cadangan.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" id="demo-form">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required />
<label for="email">Work email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="company">Company</label>
<input id="company" name="company" autocomplete="organization" required />
<label for="team_size">Team size</label>
<select id="team_size" name="team_size" required>
<option value="1_10">1 to 10</option>
<option value="11_50">11 to 50</option>
<option value="51_200">51 to 200</option>
<option value="201_plus">201 or more</option>
</select>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<p role="alert" id="demo-error" hidden></p>
<button>Pick a time</button>
</form>
<script>
const BOOKING = "https://cal.com/acme/demo";
const SELF_SERVE = "/tour";
const form = document.getElementById("demo-form");
const error = document.getElementById("demo-error");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const data = new FormData(form);
form.querySelector("button").disabled = true;
error.hidden = true;
try {
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: data,
});
const result = await response.json();
if (!result.ok) throw new Error(Object.values(result.error.fields ?? {})[0]?.[0] ?? result.error.message);
const small = data.get("team_size") === "1_10";
const next = new URL(small ? SELF_SERVE : BOOKING, location.href);
next.searchParams.set("name", String(data.get("name")));
next.searchParams.set("email", String(data.get("email")));
location.assign(next);
} catch (failure) {
error.textContent = failure instanceof Error ? failure.message : "Couldn't send. Try again.";
error.hidden = false;
form.querySelector("button").disabled = false;
}
});
</script>Server action memanggil redirect() dengan URL booking begitu Ikiform menjawab ok. Next.js menerima URL absolut di sana, dan formulir bekerja sebelum hydration karena server action menjawab form post biasa dengan 303. Jaga redirect() di luar blok try, karena fungsi itu melempar exception untuk menjalankan tugasnya.
"use server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
const BOOKING = "https://cal.com/acme/demo";
export async function requestDemo(_previous: string, formData: FormData): Promise<string> {
const lead = {
name: String(formData.get("name") ?? "").trim(),
email: String(formData.get("email") ?? "").trim(),
company: String(formData.get("company") ?? "").trim(),
team_size: String(formData.get("team_size") ?? ""),
_gotcha: String(formData.get("_gotcha") ?? ""),
};
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(lead),
});
const result = await response.json().catch(() => null);
if (!result?.ok) {
const fields: Record<string, string[]> = result?.error?.fields ?? {};
return Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.";
}
const next = new URL(lead.team_size === "1_10" ? "https://acme.example/tour" : BOOKING);
next.searchParams.set("name", lead.name);
next.searchParams.set("email", lead.email);
redirect(next.toString());
}"use client";
import { useActionState } from "react";
import { requestDemo } from "./actions";
export function DemoForm() {
const [error, action, pending] = useActionState(requestDemo, "");
return (
<form action={action}>
<label htmlFor="name">Name</label>
<input id="name" name="name" autoComplete="name" required />
<label htmlFor="email">Work email</label>
<input id="email" name="email" type="email" autoComplete="email" required />
<label htmlFor="company">Company</label>
<input id="company" name="company" autoComplete="organization" required />
<label htmlFor="team_size">Team size</label>
<select id="team_size" name="team_size" required>
<option value="1_10">1 to 10</option>
<option value="11_50">11 to 50</option>
<option value="51_200">51 to 200</option>
<option value="201_plus">201 or more</option>
</select>
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{error ? <p role="alert">{error}</p> : null}
<button disabled={pending}>{pending ? "Finding times" : "Pick a time"}</button>
</form>
);
}Form action mengembalikan fail() dengan pesan pertama saat error dan memanggil redirect(303, url, { external: true }) saat berhasil; SvelteKit 3 menolak redirect ke situs lain tanpa opsi itu. Deklarasikan IKIFORM_SECRET_KEY di src/env.ts dengan defineEnvVars agar $app/env/private bisa membacanya. Pasangkan dengan formulir +page.svelte mana pun yang mengirim ke default action; tanpa JavaScript browser mengikuti 303 dengan sendirinya.
import { fail, redirect } 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 lead = Object.fromEntries(
["name", "email", "company", "team_size", "_gotcha"].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(lead),
});
const result = await response.json();
if (!result.ok) {
const fields: Record<string, string[]> = result.error.fields ?? {};
return fail(response.status, { lead, message: Object.values(fields)[0]?.[0] ?? result.error.message });
}
const next = new URL(lead.team_size === "1_10" ? "https://acme.example/tour" : "https://cal.com/acme/demo");
next.searchParams.set("name", lead.name);
next.searchParams.set("email", lead.email);
redirect(303, next.toString(), { external: true });
},
} satisfies Actions;Di single-page app, browser mengirim langsung ke Ikiform dan berpindah dengan location.assign. Gunakan Allowed domains untuk melindungi formulir, jangan pernah secret key.
import { type FormEvent, useState } from "react";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const BOOKING = "https://cal.com/acme/demo";
export function DemoForm() {
const [error, setError] = useState("");
const [pending, setPending] = useState(false);
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
setPending(true);
setError("");
try {
const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body: data });
const result = await response.json();
if (!result.ok) {
const fields: Record<string, string[]> = result.error.fields ?? {};
setError(Object.values(fields)[0]?.[0] ?? result.error.message);
setPending(false);
return;
}
const next = new URL(data.get("team_size") === "1_10" ? "/tour" : BOOKING, window.location.href);
next.searchParams.set("name", String(data.get("name")));
next.searchParams.set("email", String(data.get("email")));
window.location.assign(next);
} catch {
setError("Couldn't reach the server. Try again.");
setPending(false);
}
}
return (
<form action={ENDPOINT} method="post" onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<input id="name" name="name" autoComplete="name" required />
<label htmlFor="email">Work email</label>
<input id="email" name="email" type="email" autoComplete="email" required />
<label htmlFor="company">Company</label>
<input id="company" name="company" autoComplete="organization" required />
<label htmlFor="team_size">Team size</label>
<select id="team_size" name="team_size" required>
<option value="1_10">1 to 10</option>
<option value="11_50">11 to 50</option>
<option value="51_200">51 to 200</option>
<option value="201_plus">201 or more</option>
</select>
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{error ? <p role="alert">{error}</p> : null}
<button disabled={pending}>{pending ? "Finding times" : "Pick a time"}</button>
</form>
);
}IKIFORM_SECRET_KEY di environment variable khusus server. Versi server meneruskan header Origin pengunjung, karena fetch di sisi server tidak mengirimnya dan Ikiform mendeteksi uji localhost gratis dari header itu.Email kantor, tanpa memblokir Gmail
Ikiform tidak punya pengaturan yang menolak penyedia email gratis. Opsi allowed_domains pada field email adalah allowlist: berguna untuk formulir internal yang dibatasi ke domain perusahaan Anda sendiri, keliru untuk formulir demo publik. Itu tidak masalah, karena memblokir Gmail juga memblokir founder, konsultan, dan pembeli yang mengisi formulir dari ponsel.
Beri tag pada permintaan itu saja, di tempat tim Anda melihatnya. Fungsi ini berjalan di relay yang mengirim setiap permintaan ke chat atau CRM Anda, dan memberi sales petunjuk sebelum panggilan:
const FREE_MAIL = new Set(["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "yahoo.com", "icloud.com", "proton.me"]);
export function demoTag(fields: Record<string, unknown>) {
const domain = String(fields.email ?? "").split("@")[1]?.toLowerCase() ?? "";
const size = String(fields.team_size ?? "");
if (FREE_MAIL.has(domain)) return "personal address, check the company";
if (size === "51_200" || size === "201_plus") return "priority";
return "standard";
}Panggil dengan event.data.fields setelah verifyWebhook, lalu tambahkan tag ke pesan chat atau properti CRM. Lead ke HubSpot atau Pipedrive menunjukkan relay lengkapnya.
Pastikan berfungsi
- Tambahkan domain situs Anda dan
localhostdi Allowed domains. - Kirim dengan ukuran tim 51 to 200. Halaman booking terbuka dengan nama dan email Anda sudah terisi.
- Tutup halaman booking tanpa memilih waktu. Permintaan tetap ada di Submissions, dan email notifikasi ada di inbox Anda.
- Kirim lagi dengan 1 to 10 dan pastikan Anda mendarat di tur.
- Kirim dengan email yang tidak valid. Error tampil di formulir, dan halaman booking tidak terbuka.
- Pesan rapat uji, lalu pastikan konfirmasi dari penjadwal sampai ke alamat yang Anda masukkan.
Hal yang sering menjebak
- Dua record per demo yang dipesan. Ikiform menyimpan permintaannya; penjadwal menyimpan rapatnya. Cocokkan berdasarkan email di CRM Anda, bukan nama.
- Tidak ada konfirmasi dari Ikiform. Ikiform tidak mengirim email ke orang yang mengisi formulir. Konfirmasi dan undangan kalender dari penjadwal mencakup panggilan yang dipesan. Untuk permintaan yang tidak pernah memesan, balas dari email notifikasi di hari yang sama.
- Query string terlihat. Nama dan email di URL booking muncul di riwayat browser dan di analitik penjadwal. Jangan kirim hal lain di sana.
- Embed. Jika Anda meng-embed penjadwal di halaman
/bookAnda sendiri, Cal.com hanya meneruskan parameter query halaman Anda saatforwardQueryParamsdiaktifkan. Calendly mendokumentasikan prefill untuk embed lewat objekprefilldi embed JavaScript-nya, jadi gunakan itu alih-alih iframe biasa. - Pemeriksaan spam. Dengan Spam check diatur ke Ikiform, versi browser harus memanggil
challenge()dari@ikiform/sdksebelum mengirim, atau mendapat403 challenge_required. Versi server melewati pemeriksaan ini.
Pertanyaan
Tambahkan ke query string tautan booking, seperti ?name=Ada%20Lovelace&email=ada%40example.com. Susun URL di kode Anda sendiri setelah backend formulir menjawab ok, lalu arahkan browser ke sana.
Banyak pengunjung membuka penjadwal lalu pergi tanpa memesan, dan penjadwal hanya mencatat orang yang memesan. Formulir singkat di awal berarti tim Anda punya setiap permintaan, dengan alamat reply-to, dan bisa menindaklanjuti yang tidak memilih waktu.
Hanya ke URL tetap di salah satu allowed domains formulir, dan URL itu tidak bisa menyertakan apa yang diketik pengunjung. Untuk mengisi formulir booking, susun URL di script halaman atau server action Anda.
Tidak ada daftar blokir bawaan. Opsi allowed domains pada field email hanya mengizinkan domain yang terdaftar, yang cocok untuk formulir internal. Terima setiap alamat dan beri tag pada permintaan dengan email gratis di relay atau CRM Anda.
Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.