Cold Leads

Verifique o encuentre direcciones de correo para los contactos nuevos de HubSpot y Pipedrive

Para quién
Equipos de operaciones de ventas en HubSpot o Pipedrive
El problema
Los contactos llegan de formularios, importaciones y altas manuales con erratas, dominios muertos o sin dirección, y nadie los comprueba hasta que una campaña rebota.
La solución
Compruebe cada contacto al crearse: una acción de código personalizado en un flujo de trabajo de HubSpot o un pequeño receptor de webhooks para Pipedrive. Un contacto con correo se verifica; un contacto sin correo pero con nombre y web de la empresa recibe una dirección sugerida, claramente marcada como suposición.
Qué obtiene
Propiedades de contacto en HubSpot o campos de persona en Pipedrive que muestran el estado, la puntuación y el motivo de Cold Leads, o una dirección sugerida con su método y su confianza.

Las direcciones, los identificadores y los resultados de los ejemplos son ilustrativos. example.com está reservado para documentación, así que una comprobación real de estas direcciones devuelve invalid.

Los ejemplos de código son iguales en todos los idiomas; sus comentarios están en inglés.

Cómo encaja todo

  • No hay integración nativa: el flujo de trabajo o el receptor llama a la API de Cold Leads con su clave secreta, y la respuesta se escribe de vuelta con las propias herramientas del CRM. La llamada a la API no añade el contacto a Cold Leads.
  • Contacto con correo: POST /api/v1/verify (1 crédito) devuelve el estado (valid, risky o invalid), la puntuación y los códigos de motivo.
  • Contacto sin correo pero con nombre o apellido y web de la empresa: POST /api/v1/find (1 crédito) devuelve la dirección más probable. Su method es verified solo cuando un servidor de correo confirmó el buzón; si no, pattern: una suposición basada en formatos de dirección habituales. Guárdela en un campo aparte y revísela; nunca sobrescriba el campo de correo con una suposición.

HubSpot: una acción de código personalizado en un flujo de contactos

  1. Cree propiedades de contacto para guardar los resultados, por ejemplo «Estado Cold Leads», «Puntuación Cold Leads», «Motivo Cold Leads» y «Correo sugerido».
  2. Vaya a Automation > Workflows y cree un flujo de trabajo de contactos. Disparador: el evento Object created (categoría CRM) con un filtro de refinamiento como Email is known, o un disparador por filtro (Met filter criteria) sobre Email is known. Los filtros de refinamiento solo se evalúan en el momento del evento, así que un contacto creado sin correo al que se añade uno más tarde no se inscribe mediante el disparador de evento.
  3. Haga clic en el icono +, busque Custom code y selecciónelo. Custom code necesita Data Hub Professional o Enterprise.
  4. Deje Node.js como lenguaje. Haga clic en Add secret, introduzca el nombre de secreto COLDLEADS_API_KEY y su clave sk_ como valor, guarde y marque el secreto.
  5. En Properties to include in code, añada Email, First name, Last name y Website URL con los nombres email, firstname, lastname y website.
  6. Pegue el código de abajo. En Data outputs, añada las salidas indicadas al principio del código con sus tipos de datos.
  7. Use Test action con un contacto de prueba. La prueba ejecuta el código real sobre el contacto que elija, así que gasta un crédito.
  8. Añada una acción Edit record (icono +, CRM, Edit record), elija una propiedad y después, en Action data, haga clic en More action data, en la acción de código personalizado y en la salida. Repita para cada propiedad.
Custom code (Node.js)
// HubSpot workflow, Custom code action (Node.js)
// Secret: COLDLEADS_API_KEY (your Cold Leads secret key, sk_...)
// Properties to include in code: email, firstname, lastname, website
// Data outputs (add them in the action): verify_status, verify_reason, suggested_email, find_method (String);
// verify_score, find_confidence (Number); mailbox_checked (Boolean)
const axios = require("axios");

const coldleads = axios.create({
  baseURL: "https://coldleads.app/api/v1",
  headers: { "x-api-key": process.env.COLDLEADS_API_KEY },
  timeout: 17000, // the action has 20 seconds in total
});

exports.main = async (event, callback) => {
  const { email, firstname, lastname, website } = event.inputFields;
  try {
    if (email) {
      // 1 credit; timeout_ms keeps the check inside HubSpot's time limit
      const { data } = await coldleads.post("/verify", { email, timeout_ms: 12000 });
      const reason = data.reasons[data.reasons.length - 1];
      return callback({
        outputFields: {
          verify_status: data.status,
          verify_score: data.score,
          verify_reason: reason,
          mailbox_checked: ["ok", "catch_all", "mailbox_missing"].includes(reason),
        },
      });
    }
    if ((firstname || lastname) && website) {
      // 1 credit; website may be a URL, Cold Leads reduces it to the domain
      const { data } = await coldleads.post("/find", { first: firstname || "", last: lastname || "", domain: website });
      return callback({
        outputFields: {
          suggested_email: data.email || "",
          find_method: data.method, // "verified" only when a mail server confirmed the mailbox, otherwise "pattern"
          find_confidence: data.confidence,
        },
      });
    }
    return callback({ outputFields: { verify_status: "skipped" } });
  } catch (err) {
    const status = err.response ? err.response.status : 0;
    // rethrow rate limits and server errors: HubSpot retries them (for up to three days)
    if (status === 429 || status >= 500) throw err;
    const code = err.response && err.response.data && err.response.data.error;
    return callback({ outputFields: { verify_status: "error", verify_reason: code || err.code || String(status) } });
  }
};

HubSpot da a una acción de código personalizado 20 segundos y 128 MB. Cuando el código lanza una excepción tras un error 429 o 5xx de axios, HubSpot reintenta la acción durante un máximo de tres días, empezando un minuto después; por eso el código relanza esos errores y convierte cualquier otro error, como 402 no_credits, en un valor de salida.

Pipedrive: un receptor de webhooks

  1. Cree dos campos de texto de persona, por ejemplo «Comprobación de correo» y «Correo sugerido», y copie sus claves de API en Company settings > Data fields > Person, desde el menú de cada campo (Copy API key). Para la búsqueda de direcciones, anote también la clave de un campo de persona que contenga la web o el dominio de la empresa.
  2. Ejecute el receptor de abajo en cualquier host con Node.js 18+ y una dirección HTTPS, con las variables de entorno que enumera.
  3. En Pipedrive abra Settings > Tools and apps > Webhooks y cree un webhook: Event action create, Event object person, un User permission level cuya visibilidad cubra las personas nuevas, un Webhook name, la Endpoint URL de su receptor (su dirección HTTPS seguida de /pipedrive/person) y un usuario y una contraseña de HTTP Auth que coincidan con HOOK_USER y HOOK_PASSWORD.
  4. Añada una persona con dirección de correo; «Comprobación de correo» se rellena poco después.
pipedrive_receiver.mjs
// npm install express   (Node.js 18 or newer)
// Pipedrive webhook: event action "create", event object "person", endpoint https://<your-host>/pipedrive/person,
// HTTP Auth username and password = HOOK_USER and HOOK_PASSWORD below.
import express from "express";

const {
  COLDLEADS_API_KEY, // Cold Leads secret key (sk_...)
  PIPEDRIVE_API_TOKEN, // Pipedrive personal API token
  HOOK_USER,
  HOOK_PASSWORD,
  STATUS_FIELD, // 40-character key of a person text field, e.g. "E-mail check"
  SUGGESTED_FIELD, // 40-character key of a person text field, e.g. "Suggested e-mail"
  DOMAIN_FIELD, // optional: key of a person field that holds the company website or domain
} = process.env;

async function coldleads(path, body) {
  const res = await fetch(`https://coldleads.app/api/v1${path}`, {
    method: "POST",
    headers: { "x-api-key": COLDLEADS_API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`Cold Leads ${res.status} ${data.error}`);
  return data;
}

async function updatePerson(id, customFields) {
  const res = await fetch(`https://api.pipedrive.com/api/v2/persons/${id}`, {
    method: "PATCH",
    headers: { "x-api-token": PIPEDRIVE_API_TOKEN, "Content-Type": "application/json" },
    body: JSON.stringify({ custom_fields: customFields }),
  });
  if (!res.ok) throw new Error(`Pipedrive ${res.status} ${await res.text()}`);
}

// webhook payloads carry custom fields as typed objects ({ type, value })
const plain = (v) => (v && typeof v === "object" && "value" in v ? v.value : v);

async function handlePerson(person) {
  const emails = Array.isArray(person.emails) ? person.emails : [];
  const primary = emails.find((e) => e && e.primary) ?? emails[0];
  const email = typeof primary === "string" ? primary : primary?.value;
  if (email) {
    const r = await coldleads("/verify", { email, timeout_ms: 15000 }); // 1 credit
    const reason = r.reasons[r.reasons.length - 1];
    await updatePerson(person.id, { [STATUS_FIELD]: `${r.status} (${reason}, score ${r.score})` });
    return;
  }
  const domain = DOMAIN_FIELD ? plain(person.custom_fields?.[DOMAIN_FIELD]) : "";
  if (domain && (person.first_name || person.last_name)) {
    const r = await coldleads("/find", { first: person.first_name ?? "", last: person.last_name ?? "", domain }); // 1 credit
    if (r.email) await updatePerson(person.id, { [SUGGESTED_FIELD]: `${r.email} (${r.method}, confidence ${r.confidence})` });
  }
}

const seen = new Set(); // a retried delivery must not be charged twice
const app = express();
app.post("/pipedrive/person", express.json(), (req, res) => {
  const expected = "Basic " + Buffer.from(`${HOOK_USER}:${HOOK_PASSWORD}`).toString("base64");
  if (req.get("authorization") !== expected) return res.sendStatus(401);
  const { meta, data } = req.body ?? {};
  res.sendStatus(200); // answer at once: Pipedrive waits 10 seconds, then retries
  if (meta?.entity !== "person" || !data?.id || seen.has(meta.id)) return;
  seen.add(meta.id);
  handlePerson(data).catch((e) => console.error(`person ${data.id}:`, e.message));
});
app.listen(process.env.PORT ?? 3000);
  • Pipedrive da por entregada cualquier respuesta 2xx, espera hasta 10 segundos y reintenta una entrega fallida a los 3, 30 y 150 segundos. El receptor responde al instante e ignora los id de evento que ya ha procesado, así que un reintento no se cobra dos veces. El conjunto de id vive en memoria; use una base de datos si ejecuta varias instancias.
  • Los webhooks creados en la interfaz de Pipedrive usan el formato v2: meta describe el evento y data contiene la persona con first_name, last_name, emails y custom_fields.
  • Los campos de persona se actualizan con PATCH /api/v2/persons/{id} y un objeto custom_fields, con autenticación mediante la cabecera x-api-token. El endpoint anterior PUT /v1/persons no tiene soporte desde el 1 de agosto de 2026.
  • El webhook solo se dispara cuando se crea una persona, así que la actualización que hace no lo vuelve a disparar. Si también se suscribe a cambios, omita los eventos cuyo meta.change_source sea api.

Qué escribir de vuelta y qué hacer con ello

ResultadoTratamiento sugerido en el CRM
valid, motivo okListo para contactar: un servidor de correo aceptó el buzón.
valid, motivo smtp_unreachableEl dominio acepta correo, pero el buzón no se comprobó. Envíe con cuidado y elimine los rebotes duros.
risky (catch_all, smtp_unknown, timeout o un dominio desechable)Revisar antes de enviar.
invalid (mailbox_missing, no_mx, syntax)No enviar correos; corregir la dirección o eliminarla.
búsqueda, method verifiedUn servidor de correo confirmó el buzón sugerido.
búsqueda, method patternUna suposición basada en formatos de dirección habituales: confírmela antes de usarla.
búsqueda, method noneNingún candidato: el dominio no tiene servidor de correo o no se pudo usar el nombre.

Límites y costes

  • 1 crédito de Cold Leads por verificación o llamada de búsqueda, sea cual sea el resultado. El plan Business ($99 al mes) incluye la API y 10 000 créditos al mes; los paquetes extra de 1 000 créditos cuestan $5.
  • 120 peticiones por minuto por clave de Cold Leads; un 429 lleva Retry-After: 60.
  • El código de HubSpot pide a Cold Leads un margen de 12 segundos (timeout_ms) para no pasarse de los 20 segundos de HubSpot. La búsqueda de direcciones no tiene parámetro de margen; en un dominio lento la acción puede quedarse sin tiempo, y el crédito se gasta.
  • La búsqueda de direcciones quita los acentos de los nombres (Novák pasa a ser novak) y descarta los demás caracteres que no estén entre la a y la z, así que los nombres en alfabetos no latinos necesitan antes una transliteración.
  • Pipedrive mide el uso de su API en tokens al día por empresa; actualizar una persona con PATCH /api/v2/persons/{id} cuesta 5 tokens.

Preguntas frecuentes

¿Qué plan de HubSpot necesito?

Las acciones de código personalizado (y las acciones de webhook) en los flujos de trabajo de HubSpot necesitan Data Hub Professional o Enterprise. Por parte de Cold Leads, la API necesita el plan Business.

¿Por qué no escribir la dirección sugerida directamente en el campo de correo?

Porque con method pattern es una suposición basada en formatos habituales, no un buzón confirmado. Tenerla en su propio campo permite que alguien la confirme antes de que la use una secuencia.

¿Puede la búsqueda de direcciones encontrar a quienes deciden en una empresa?

No. Cold Leads no tiene una base de datos de personas. La búsqueda de direcciones necesita una persona que ya conozca por su nombre, más el dominio de la empresa.

¿Funciona con las automatizaciones de Pipedrive en lugar de un webhook?

Las automatizaciones de Pipedrive también pueden llamar a un webhook (en el plan Growth y superiores), pero envían un cuerpo que usted mismo define, así que el análisis del receptor tiene que ajustarse a ese cuerpo en lugar de al formato de webhook v2.