Cold Leads

Verify or find e-mail addresses for new HubSpot and Pipedrive contacts

Who it is for
Sales operations teams on HubSpot or Pipedrive
The problem
Contacts arrive from forms, imports and manual entry with typos, dead domains or no address at all, and nobody checks them until a campaign bounces.
The solution
Check each contact when it is created: a custom code action in a HubSpot workflow, or a small webhook receiver for Pipedrive. A contact with an e-mail is verified; a contact without one but with a name and a company website gets a suggested address that is clearly marked as a guess.
What you get
Contact properties in HubSpot or person fields in Pipedrive that show the Cold Leads status, score and reason, or a suggested address with its method and confidence.

Addresses, IDs and results in the examples are illustrative. example.com is reserved for documentation, so a real check of these addresses returns invalid.

How it fits together

  • There is no built-in integration: the workflow or the receiver calls the Cold Leads API with your secret key, and the answer is written back by the CRM's own tools. The API call does not add the contact to Cold Leads.
  • Contact with an e-mail: POST /api/v1/verify (1 credit) returns status (valid, risky or invalid), score and reason codes.
  • Contact without an e-mail but with a first or last name and a company website: POST /api/v1/find (1 credit) returns the most likely address. Its method is verified only when a mail server confirmed the mailbox, otherwise pattern: a guess from common address formats. Keep it in a separate field and review it; never overwrite the e-mail field with a guess.

HubSpot: a custom code action in a contact workflow

  1. Create contact properties to hold the results, for example Cold Leads status, Cold Leads score, Cold Leads reason and Suggested e-mail.
  2. Go to Automation > Workflows and create a contact workflow. Trigger: the event Object created (category CRM) with a refinement filter such as Email is known, or a filter trigger (Met filter criteria) on Email is known. Refinement filters are evaluated only at the moment of the event, so a contact created without an e-mail and given one later does not enrol through the event trigger.
  3. Click the + icon, search for Custom code and select it. Custom code needs Data Hub Professional or Enterprise.
  4. Keep Node.js as the language. Click Add secret, enter the secret name COLDLEADS_API_KEY and your sk_ key as the value, save, and tick the secret.
  5. Under Properties to include in code, add Email, First name, Last name and Website URL with the names email, firstname, lastname and website.
  6. Paste the code below. Under Data outputs, add the outputs listed at the top of the code with their data types.
  7. Use Test action on a test contact. The test runs the real code against the contact you pick, so it spends a credit.
  8. Add an Edit record action (+ icon, CRM, Edit record), choose a property, then under Action data click More action data, the custom code action and the output. Repeat for each property.
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 gives a custom code action 20 seconds and 128 MB. When the code throws after a 429 or 5xx error from axios, HubSpot retries the action for up to three days, starting a minute later; that is why the code rethrows those errors and turns every other error, such as 402 no_credits, into an output value.

Pipedrive: a webhook receiver

  1. Create two person text fields, for example E-mail check and Suggested e-mail, and copy their API keys under Company settings > Data fields > Person, in each field's menu (Copy API key). For the finder, also note the key of a person field that holds the company website or domain.
  2. Run the receiver below on any Node.js 18+ host with an HTTPS address, with the environment variables it lists.
  3. In Pipedrive open Settings > Tools and apps > Webhooks and create a webhook: Event action create, Event object person, a User permission level whose visibility covers new persons, a Webhook name, the Endpoint URL of your receiver (its HTTPS address followed by /pipedrive/person), and an HTTP Auth username and password that match HOOK_USER and HOOK_PASSWORD.
  4. Add a person with an e-mail address; E-mail check fills in shortly afterwards.
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 counts any 2xx answer as delivered, waits up to 10 seconds, and retries a failed delivery after 3, 30 and 150 seconds. The receiver answers at once and ignores event ids it has already handled, so a retry is not charged twice. The set of ids lives in memory; use a database if you run several instances.
  • Webhooks created in the Pipedrive interface use the v2 format: meta describes the event, data holds the person with first_name, last_name, emails and custom_fields.
  • Person fields are updated with PATCH /api/v2/persons/{id} and a custom_fields object, authenticated with the x-api-token header. The older PUT /v1/persons endpoint is out of support since August 1, 2026.
  • The webhook fires only when a person is created, so the update it makes does not trigger it again. If you also subscribe to changes, skip events whose meta.change_source is api.

What to write back, and what to do with it

ResultSuggested handling in the CRM
valid, reason okReady for outreach: a mail server accepted the mailbox.
valid, reason smtp_unreachableThe domain accepts mail, but the mailbox was not checked. Send with care and remove hard bounces.
risky (catch_all, smtp_unknown, timeout, or a disposable domain)Review before sending.
invalid (mailbox_missing, no_mx, syntax)Do not e-mail; correct the address or remove it.
finder, method verifiedA mail server confirmed the suggested mailbox.
finder, method patternA guess from common address formats: confirm it before use.
finder, method noneNo candidate: the domain has no mail server, or the name could not be used.

Limits and costs

  • 1 Cold Leads credit per verification or finder call, whatever the result. The Business plan ($99 a month) includes the API and 10,000 credits a month; extra packs of 1,000 credits cost $5.
  • 120 requests per minute per Cold Leads key; a 429 carries Retry-After: 60.
  • The HubSpot code asks Cold Leads for a 12-second budget (timeout_ms) to stay inside HubSpot's 20 seconds. The finder has no budget parameter; on a slow domain the action can run out of time, and the credit is spent.
  • The finder removes accents from names (Novák becomes novak) and drops other characters outside a to z, so names in non-Latin scripts need a transliteration first.
  • Pipedrive meters its API in tokens per day per company; updating a person with PATCH /api/v2/persons/{id} costs 5 tokens.

FAQ

Which HubSpot plan do I need?

Custom code actions (and webhook actions) in HubSpot workflows need Data Hub Professional or Enterprise. On Cold Leads' side, the API needs the Business plan.

Why not write the suggested address straight into the e-mail field?

Because with method pattern it is a guess from common formats, not a confirmed mailbox. Keeping it in its own field lets someone confirm it before a sequence uses it.

Can the finder look up the decision-makers of a company?

No. Cold Leads has no people database. The finder needs a person you already know by name, plus the company domain.

Does this work with Pipedrive automations instead of a webhook?

Pipedrive automations can also call a webhook (on Growth and higher plans), but they send a body you define yourself, so the receiver's parsing has to match that body instead of the v2 webhook format.