Formulario de captación de leads

Formulario de captación de leads7 min de lectura

Envía los leads de un formulario a HubSpot o Pipedrive

Convierte cada webhook firmado de Ikiform en el upsert de contactos de HubSpot o en las llamadas de personas y leads de Pipedrive, sin contactos duplicados cuando se reintenta una entrega.

Preet SutharFundador, Ikiform

Apunta un webhook de Ikiform a un pequeño relay que verifique la firma y traslade el lead a la API de tu CRM: el upsert por lotes de HubSpot con el email como clave, o la búsqueda de personas, la creación de personas y la creación de leads de Pipedrive. El cuerpo del webhook de Ikiform es fijo, así que el relay hace la conversión. Haz que cada paso se pueda repetir sin problemas, porque una entrega fallida se reintenta con el mismo webhook-id.

Qué tiene de distinto enviar leads a un CRM

No hay integraciones de un clic. Cada herramienta se conecta mediante un webhook firmado, directamente o a través de una herramienta de automatización como Zapier o n8n. El cuerpo del webhook es el mismo para todos los destinos: data.fields contiene los valores del formulario, y data.meta contiene el origen, el referrer, el país y la dirección de respuesta. Ikiform no te deja cambiar su forma, así que algo intermedio lo convierte en la llamada que espera tu CRM.

Dos cosas hacen que un CRM sea más difícil que un canal de chat:

  • Un CRM guarda registros, no mensajes. Publicar el mismo lead dos veces en Slack es ruido. Crear el mismo contacto dos veces es un lío que alguien tiene que fusionar a mano. El relay debe buscar antes de crear.
  • Los reintentos son parte del contrato. Si tu relay o el CRM fallan, Ikiform reintenta la entrega durante unos 3 días con el mismo webhook-id. Un relay que a la primera tuvo éxito a medias no debe duplicar nada a la segunda.

Relay o herramienta de automatización

OpciónPunto fuerteInconvenientes
Zapier, Make o n8nPasos de HubSpot y Pipedrive ya hechos, con acciones de buscar o crear y sin códigoPrecio por tarea, y una cuenta más que guarda las credenciales de tu CRM
Tu propio relayCorrespondencia de campos exacta, tus propias reglas de duplicados, nada facturado por leadLo despliegas y lo vigilas tú. Unas 60 líneas por CRM, como se ve abajo

En ambos casos, la configuración del webhook es la misma: añade el endpoint en Webhooks, copia su secreto whsec_ y envía una prueba. La documentación de webhooks cubre el payload y los reintentos, y tu propio servidor explica con más detalle cómo verificar las entregas.

HubSpot: upsert del contacto por email

HubSpot trata el email como identificador único de un contacto, y su endpoint de upsert por lotes acepta idProperty: "email". Una sola llamada crea el contacto si la dirección es nueva y lo actualiza si no, así que un reintento no puede crear un duplicado.

Cómo convierte el relay un envío de Ikiform en un contacto de HubSpotEvento de IkiformContacto de HubSpotfields.email, en minúsculasemail, y la clave del upsertfields.name, primera palabrafirstnamefields.name, el restolastnamefields.companycompanyfields.interestikiform_interest, una propiedadque creas túpropietario y etapa del ciclo devida, según las reglas deHubSpotCómo convierte el relay un envío de Ikiform en un contacto de HubSpotEvento de IkiformContacto de HubSpotfields.email,en minúsculasemail, y laclave delupsertfields.name,primerapalabrafirstnamefields.name,el restolastnamefields.companycompanyfields.interestikiform_interest,una propiedadque creas túpropietario yetapa delciclo de vida,según lasreglas deHubSpot
  1. fields.email, en minúsculas, pasa a ser email y la clave del upsert.
  2. fields.name, la primera palabra, pasa a ser firstname.
  3. fields.name, el resto, pasa a ser lastname.
  4. fields.company pasa a ser company.
  5. fields.interest pasa a ser ikiform_interest, una propiedad que creas tú.
  6. HubSpot asigna el propietario y la etapa del ciclo de vida con sus propias reglas; el relay no los toca.
Todo lo de la izquierda sale del cuerpo fijo del webhook de Ikiform. La fila gris se deja a los workflows de HubSpot en lugar de que la fije el relay.

Configuración

  1. En HubSpot, crea una private app (en algunas cuentas HubSpot ya las clasifica como legacy apps) con el scope crm.objects.contacts.write y copia su token de acceso en HUBSPOT_TOKEN.
  2. Crea una propiedad de contacto con el nombre interno ikiform_interest, o quita esa línea. HubSpot rechaza las propiedades que no existen.
  3. Despliega la ruta de abajo y añade su URL como endpoint de webhook en 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 });
}
  • Un 429 de HubSpot se convierte en un 429 con Retry-After, que Ikiform respeta antes de volver a intentarlo.
  • Cualquier otro fallo, incluido un 207 con errores por registro, se convierte en un 502 con la respuesta de HubSpot como cuerpo. Los primeros 4 KB quedan en el registro de entregas de Ikiform, así que puedes leer el mensaje de HubSpot sin tus propios logs.
  • El upsert envía todas las propiedades cada vez, porque HubSpot no admite upserts parciales por email. Un company vacío en un segundo formulario sobrescribe uno conocido; quita los valores vacíos de properties si eso te importa.
La referencia de HubSpot ahora también publica rutas de API versionadas por fecha y lista esta ruta v3 en su sección legacy. Consulta la referencia actual de contactos cuando lo construyas y mantén igual el cuerpo de la petición.

Pipedrive: busca la persona y luego añade un lead

Pipedrive separa las personas de los leads. Un lead tiene que estar vinculado a una persona o a una organización, así que el relay busca o crea la persona y luego añade un lead con un título. Las personas y la búsqueda de personas usan la API v2; los leads y las notas son endpoints v1. Ambos reciben tu token de API en la cabecera x-api-token.

El relay de Pipedrive, con cada paso seguro de repetirwebhookwebhook-id¿yavisto?consulta enKVpersonabuscar poremail, o crearleadtítulo,person_idresponder200nada que hacernosíEl relay de Pipedrive, con cada paso seguro de repetirwebhookwebhook-id¿ya visto?consulta en KVpersonabuscar por email, ocrearleadtítulo, person_idresponder200nada quehacernosí
  1. Llega una entrega con su webhook-id.
  2. El relay busca ese id en un almacén clave-valor.
  3. Si ya se procesó, el relay responde 200 y se detiene.
  4. Si no, busca personas por email exacto y crea una solo cuando no hay ninguna coincidencia.
  5. Añade un lead vinculado a esa persona, luego una nota con el mensaje, y registra el webhook-id.
La búsqueda de la persona hace que un reintento sea seguro antes de que exista el lead. El webhook-id guardado lo hace seguro después.
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 es el subdominio de tu empresa, el acme de acme.pipedrive.com. El token está en tus preferencias personales, en la pestaña API.
  • SEEN es un namespace de Workers KV vinculado al Worker. El id se guarda solo después de crear el lead, así que un fallo a mitad no deja nada que bloquee el reintento.
  • origin_id guarda el id del envío de Ikiform, para que puedas rastrear un lead hasta el envío guardado.
  • Los leads creados mediante la API tienen API como origen en Pipedrive.

Comprueba que funciona

  1. Elige Send test en el endpoint. El relay responde 204 e ignora el evento endpoint.test.
  2. Envía el formulario con una dirección nueva. En segundos aparece un contacto en HubSpot o una persona y un lead en Pipedrive.
  3. Vuelve a enviarlo con la misma dirección. HubSpot actualiza el mismo contacto; Pipedrive añade un segundo lead a la persona existente.
  4. Abre la entrega en Webhooks y elige Retry. No se crea nada nuevo.
  5. Rompe el token y envía. La entrega muestra el error del CRM en el registro; arregla el token y reinténtala.

Errores comunes

  • Un lead por envío, no por persona. Suele ser lo correcto en Pipedrive, donde cada consulta es su propio lead. Si prefieres asociar las repeticiones a un lead abierto, busca primero los leads de la persona.
  • 3 endpoints por espacio de trabajo. Un solo relay puede llamar a los dos CRM y al chat, lo que deja endpoints libres para otros formularios.
  • El spam nunca llega al CRM. El spam no envía webhook. Cuando marcas un envío como no spam, su webhook sale en ese momento y el lead aparece más tarde.
  • Los valores no son de fiar. Los valores de los campos vienen del público. Envíalos como valores JSON y nunca construyas query strings ni fórmulas con ellos sin codificarlos.

Preguntas

¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.