Formulir lead capture

Formulir lead capture6 menit baca

Kirim lead dari formulir ke HubSpot atau Pipedrive

Petakan setiap webhook Ikiform bertanda tangan ke contact upsert HubSpot atau panggilan person dan lead Pipedrive, tanpa kontak duplikat saat pengiriman dicoba ulang.

Preet SutharPendiri, Ikiform

Arahkan webhook Ikiform ke relay kecil yang memverifikasi tanda tangan dan memetakan lead ke API CRM Anda: batch upsert HubSpot dengan kunci email, atau person search, person create, dan lead create di Pipedrive. Body webhook Ikiform tetap, jadi relay yang melakukan pemetaan. Buat setiap langkah aman untuk diulang, karena pengiriman yang gagal dicoba ulang dengan webhook-id yang sama.

Apa yang berbeda saat mengirim lead ke CRM

Tidak ada integrasi sekali klik. Setiap alat dijangkau lewat webhook bertanda tangan, langsung atau melalui alat otomasi seperti Zapier atau n8n. Body webhook sama untuk setiap tujuan: data.fields berisi nilai formulir, dan data.meta berisi origin, referrer, negara, dan alamat reply-to. Ikiform tidak memungkinkan Anda mengubah bentuknya, jadi sesuatu di antaranya mengubahnya menjadi panggilan yang diharapkan CRM Anda.

Dua hal membuat CRM lebih sulit daripada channel chat:

  • CRM menyimpan record, bukan pesan. Mengirim lead yang sama ke Slack dua kali hanyalah noise. Membuat kontak yang sama dua kali adalah kekacauan yang harus digabungkan seseorang secara manual. Relay harus mencari dulu sebelum membuat.
  • Percobaan ulang adalah bagian dari kontrak. Jika relay atau CRM Anda gagal, Ikiform mencoba ulang pengiriman selama sekitar 3 hari dengan webhook-id yang sama. Relay yang setengah berhasil pada percobaan pertama tidak boleh membuat duplikat pada percobaan kedua.

Relay, atau alat otomasi

OpsiUnggul dalamKekurangan
Zapier, Make, atau n8nLangkah HubSpot dan Pipedrive siap pakai, dengan action find-or-create dan tanpa kodeHarga per tugas, dan satu akun lagi yang memegang kredensial CRM Anda
Relay Anda sendiriPemetaan field yang persis, aturan duplikat Anda sendiri, tidak ada tagihan per leadAnda yang men-deploy dan memantaunya. Sekitar 60 baris per CRM, ditampilkan di bawah

Apa pun pilihannya, penyiapan webhook sama: tambahkan endpoint di Webhooks, salin secret whsec_-nya, dan kirim uji. Dokumentasi webhook membahas payload dan percobaan ulang, dan server Anda sendiri membahas verifikasi pengiriman lebih dalam.

HubSpot: upsert kontak berdasarkan email

HubSpot memperlakukan email sebagai identifier unik kontak, dan endpoint batch upsert-nya menerima idProperty: "email". Satu panggilan membuat kontak jika alamatnya baru dan memperbaruinya jika tidak, jadi percobaan ulang tidak bisa membuat duplikat.

Bagaimana relay memetakan kiriman Ikiform ke kontak HubSpotEvent IkiformKontak HubSpotfields.email, huruf kecilemail, dan kunci upsertfields.name, kata pertamafirstnamefields.name, sisanyalastnamefields.companycompanyfields.interestikiform_interest, properti yangAnda buatowner dan lifecycle stage, dariaturan HubSpot sendiriBagaimana relay memetakan kiriman Ikiform ke kontak HubSpotEvent IkiformKontak HubSpotfields.email,huruf kecilemail, dankunci upsertfields.name,kata pertamafirstnamefields.name,sisanyalastnamefields.companycompanyfields.interestikiform_interest,properti yangAnda buatowner danlifecyclestage, dariaturanHubSpotsendiri
  1. fields.email, huruf kecil menjadi email, dan kunci upsert.
  2. fields.name, kata pertama menjadi firstname.
  3. fields.name, sisanya menjadi lastname.
  4. fields.company menjadi company.
  5. fields.interest menjadi ikiform_interest, properti yang Anda buat.
  6. HubSpot menetapkan owner dan lifecycle stage dengan aturannya sendiri; relay tidak menyentuhnya.
Semua di sebelah kiri berasal dari body webhook Ikiform yang tetap. Baris abu-abu diserahkan ke workflow HubSpot alih-alih diatur oleh relay.

Setup

  1. Di HubSpot, buat private app (HubSpot kini menyebutnya legacy app di beberapa akun) dengan scope crm.objects.contacts.write, dan salin access token-nya ke HUBSPOT_TOKEN.
  2. Buat properti kontak dengan nama internal ikiform_interest, atau hapus baris itu. HubSpot menolak properti yang tidak ada.
  3. Deploy route di bawah dan tambahkan URL-nya sebagai endpoint webhook di Ikiform.
app/api/hubspot/route.ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

const UPSERT = "https://api.hubapi.com/crm/v3/objects/contacts/batch/upsert";

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 fields = event.data.fields;
  const email = String(fields.email ?? "").trim().toLowerCase();
  if (!email) return new Response("No email, nothing to upsert", { status: 200 });
  const [firstname = "", ...rest] = String(fields.name ?? "").trim().split(/\s+/);

  const response = await fetch(UPSERT, {
    method: "POST",
    headers: {
      authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      inputs: [
        {
          idProperty: "email",
          id: email,
          properties: {
            email,
            firstname,
            lastname: rest.join(" "),
            company: String(fields.company ?? ""),
            phone: String(fields.phone ?? ""),
            ikiform_interest: String(fields.interest ?? ""),
          },
        },
      ],
    }),
  });

  if (response.status === 429) {
    return new Response("HubSpot rate limit", { status: 429, headers: { "retry-after": "10" } });
  }
  if (response.status === 207 || !response.ok) {
    return new Response(await response.text(), { status: 502 });
  }
  return new Response(null, { status: 204 });
}
  • 429 dari HubSpot menjadi 429 dengan Retry-After, yang dipatuhi Ikiform sebelum mencoba lagi.
  • Kegagalan lainnya, termasuk 207 dengan error per record, menjadi 502 dengan jawaban HubSpot sebagai body. 4 KB pertamanya masuk ke log pengiriman Ikiform, jadi Anda bisa membaca pesan HubSpot tanpa log sendiri.
  • Upsert mengirim setiap properti setiap kali, karena HubSpot tidak mendukung upsert parsial dengan kunci email. company kosong di formulir kedua menimpa nilai yang sudah diketahui; buang nilai kosong dari properties jika itu penting bagi Anda.
Referensi HubSpot kini juga menerbitkan path API berversi tanggal dan mencantumkan path v3 ini di bagian legacy. Periksa referensi kontak terbaru saat Anda membangunnya, dan biarkan body request tetap sama.

Pipedrive: cari person, lalu tambahkan lead

Pipedrive memisahkan person dari lead. Lead harus terhubung ke person atau organization, jadi relay mencari atau membuat person, lalu menambahkan lead dengan judul. Person dan person search memakai API v2; lead dan note adalah endpoint v1. Keduanya menerima API token Anda di header x-api-token.

Relay Pipedrive, dengan setiap langkah aman untuk diulangwebhookwebhook-idpernahdilihat?cek KVpersoncari berdasarkanemail, atau buatleadtitle, person_idjawab 200tidak ada yangperlu dilakukantidakyaRelay Pipedrive, dengan setiap langkah aman untuk diulangwebhookwebhook-idpernahdilihat?cek KVpersoncari berdasarkanemail, atau buatleadtitle, person_idjawab 200tidak adayang perludilakukantidakya
  1. Sebuah pengiriman tiba dengan webhook-id-nya.
  2. Relay memeriksa ID itu di key-value store.
  3. Jika sudah pernah ditangani, relay menjawab 200 dan berhenti.
  4. Jika belum, relay mencari person berdasarkan email persis dan hanya membuat yang baru jika tidak ada yang cocok.
  5. Relay menambahkan lead yang terhubung ke person itu, lalu note berisi pesannya, dan mencatat webhook-id.
Pencarian person membuat percobaan ulang aman sebelum lead ada. webhook-id yang disimpan membuatnya aman sesudahnya.
src/index.ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

interface Env {
  IKIFORM_WEBHOOK_SECRET: string;
  PIPEDRIVE_TOKEN: string;
  PIPEDRIVE_DOMAIN: string;
  SEEN: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env) {
    const payload = await request.text();
    let event;
    try {
      event = await verifyWebhook({ payload, headers: request.headers, secret: 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 });
    if (await env.SEEN.get(event.id)) return new Response("Already handled", { status: 200 });

    const base = `https://${env.PIPEDRIVE_DOMAIN}.pipedrive.com/api`;
    const headers = { "x-api-token": env.PIPEDRIVE_TOKEN, "content-type": "application/json" };
    const call = async (path: string, init?: RequestInit) => {
      const response = await fetch(`${base}${path}`, { ...init, headers });
      if (!response.ok) throw new Error(`Pipedrive ${path} answered ${response.status}: ${await response.text()}`);
      return (await response.json()) as { data: any };
    };

    const fields = event.data.fields;
    const email = String(fields.email ?? "").trim().toLowerCase();
    const name = String(fields.name ?? email);

    try {
      const found = await call(`/v2/persons/search?term=${encodeURIComponent(email)}&fields=email&exact_match=true`);
      let personId: number | undefined = found.data?.items?.[0]?.item?.id;
      if (!personId) {
        const created = await call("/v2/persons", {
          method: "POST",
          body: JSON.stringify({ name, emails: [{ value: email, primary: true, label: "work" }] }),
        });
        personId = created.data.id;
      }

      const lead = await call("/v1/leads", {
        method: "POST",
        body: JSON.stringify({
          title: `${fields.company || name}: ${fields.interest ?? "website lead"}`,
          person_id: personId,
          origin_id: event.data.submission_id,
        }),
      });

      if (fields.message) {
        await call("/v1/notes", {
          method: "POST",
          body: JSON.stringify({ lead_id: lead.data.id, content: String(fields.message) }),
        });
      }
    } catch (error) {
      return new Response(String(error), { status: 502 });
    }

    await env.SEEN.put(event.id, "1", { expirationTtl: 60 * 60 * 24 * 5 });
    return new Response(null, { status: 204 });
  },
};
  • PIPEDRIVE_DOMAIN adalah subdomain perusahaan Anda, acme di acme.pipedrive.com. Token ada di preferensi pribadi Anda, di tab API.
  • SEEN adalah namespace Workers KV yang di-bind ke Worker. ID hanya disimpan setelah lead dibuat, jadi kegagalan di tengah jalan tidak meninggalkan apa pun yang menghalangi percobaan ulang.
  • origin_id berisi ID kiriman Ikiform, jadi Anda bisa melacak lead kembali ke kiriman yang tersimpan.
  • Lead yang dibuat lewat API mendapat API sebagai sumbernya di Pipedrive.

Pastikan berfungsi

  1. Pilih Send test di endpoint. Relay menjawab 204 dan mengabaikan event endpoint.test.
  2. Kirim formulir dengan alamat baru. Kontak HubSpot atau person dan lead Pipedrive muncul dalam hitungan detik.
  3. Kirim lagi dengan alamat yang sama. HubSpot memperbarui satu kontak itu; Pipedrive menambahkan lead kedua ke person yang sudah ada.
  4. Buka pengiriman di Webhooks dan pilih Retry. Tidak ada yang baru dibuat.
  5. Rusak token-nya lalu kirim. Pengiriman menampilkan error CRM di log; perbaiki token dan coba ulang.

Hal yang sering menjebak

  • Satu lead per kiriman, bukan per orang. Itu biasanya tepat di Pipedrive, tempat setiap inquiry adalah lead tersendiri. Jika Anda lebih suka melampirkan pengulangan ke lead yang masih terbuka, cari lead berdasarkan person lebih dulu.
  • 3 endpoint per workspace. Satu relay bisa memanggil kedua CRM dan chat, sehingga endpoint tetap tersedia untuk formulir lain.
  • Spam tidak pernah sampai ke CRM. Spam tidak mengirim webhook. Saat Anda menandai kiriman sebagai bukan spam, webhook-nya dikirim saat itu, dan lead muncul terlambat.
  • Nilai tidak tepercaya. Nilai field berasal dari publik. Kirim sebagai nilai JSON, jangan pernah menyusun query string atau formula darinya tanpa encoding.

Pertanyaan

Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.