Cold Leads

=VERIFY_EMAIL() and =FIND_EMAIL() in Google Sheets

Who it is for
People who keep lead lists in Google Sheets
The problem
Exporting a sheet to a verification tool and pasting the results back is tedious. A naive custom function calls the API again, and spends a credit again, every time Sheets runs the formula again.
The solution
Two custom functions call the Cold Leads API with UrlFetchApp, read the key from the script properties and keep each answer in the script cache for up to six hours, so running the same formula again does not spend another credit for the same input.
What you get
Three result cells per row: status, score and reason from =VERIFY_EMAIL, or address, method and confidence from =FIND_EMAIL.

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

Set it up

  1. In the spreadsheet open Extensions → Apps Script and replace the contents of Code.gs with the script below. Save.
  2. Click Project Settings, then under Script Properties click Add script property. Property: COLDLEADS_API_KEY, value: your Cold Leads secret key (sk_…). Click Save script properties.
  3. Back in the sheet, type =VERIFY_EMAIL(A2) in B2. The result fills B2, C2 and D2 (status, score, reason), so leave those two cells to the right empty. Fill the formula down.
  4. For addresses you do not have yet, type =FIND_EMAIL(B2, C2, D2) with first name, last name and company domain or website. It fills the address, the method and the confidence.

The script

Code.gs
// Cold Leads custom functions for Google Sheets (Extensions → Apps Script, paste into Code.gs).
// Key: Project Settings → Script Properties → Add script property COLDLEADS_API_KEY = sk_...
const COLDLEADS_API = 'https://coldleads.app/api/v1';
const CACHE_SECONDS = 21600; // 6 hours, the longest CacheService keeps an entry

/**
 * Verifies an e-mail address with Cold Leads. Fills three cells: status, score, last reason code.
 * Costs 1 Cold Leads credit unless the same address was answered from this script's cache.
 *
 * @param {string} email The address, for example A2.
 * @return Status, score and reason.
 * @customfunction
 */
function VERIFY_EMAIL(email) {
  const address = String(email || '').trim().toLowerCase();
  if (!address) return '';
  const r = cachedCall_('verify:' + address, '/verify', { email: address, timeout_ms: 20000 }, function (res) {
    return res.reasons[res.reasons.length - 1] !== 'timeout'; // timeouts are worth checking again
  });
  return [[r.status, r.score, r.reasons[r.reasons.length - 1]]];
}

/**
 * Finds the most likely e-mail address for a person at a company domain. Fills three cells:
 * address, method ("verified" only when a mail server confirmed the mailbox, otherwise "pattern"), confidence.
 * Costs 1 Cold Leads credit unless answered from this script's cache.
 *
 * @param {string} first First name, for example B2.
 * @param {string} last Last name, for example C2.
 * @param {string} domain Company domain or website, for example D2.
 * @return Address, method and confidence.
 * @customfunction
 */
function FIND_EMAIL(first, last, domain) {
  const body = { first: String(first || '').trim(), last: String(last || '').trim(), domain: String(domain || '').trim().toLowerCase() };
  if (!body.domain || !(body.first || body.last)) return '';
  const key = 'find:' + [body.first, body.last, body.domain].join('|').toLowerCase();
  const r = cachedCall_(key, '/find', body, function () { return true; });
  return [[r.email || '', r.method, r.confidence]];
}

function cachedCall_(cacheKey, path, payload, cacheable) {
  const cache = CacheService.getScriptCache();
  // cache keys are limited to 250 characters, so store a hash of the key
  const id = Utilities.base64Encode(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, cacheKey, Utilities.Charset.UTF_8));
  const hit = cache.get(id);
  if (hit) return JSON.parse(hit);
  const apiKey = PropertiesService.getScriptProperties().getProperty('COLDLEADS_API_KEY');
  if (!apiKey) throw new Error('Add the script property COLDLEADS_API_KEY (Project Settings → Script Properties).');
  const res = UrlFetchApp.fetch(COLDLEADS_API + path, {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-api-key': apiKey },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  });
  const data = JSON.parse(res.getContentText() || '{}');
  if (res.getResponseCode() !== 200) throw new Error('Cold Leads: ' + (data.error || 'HTTP ' + res.getResponseCode()));
  if (cacheable(data)) cache.put(id, JSON.stringify(data), CACHE_SECONDS);
  return data;
}

How it behaves

  • Sheets runs a custom function again when you edit the formula, copy it into another cell or change a cell it refers to. Without a cache, each run is a new API call and a new credit.
  • The script cache keeps each answer for up to 6 hours and is shared by everyone who uses the script. Google may drop entries earlier when the cache is full, and it notes that the Cache service works in custom functions but is "not particularly useful" there; here it only prevents paying twice for the same input.
  • Answers with the reason timeout are not cached, so the next run checks the address again.
  • A custom function must finish within 30 seconds. =VERIFY_EMAIL asks Cold Leads for a 20-second budget. =FIND_EMAIL has no budget and can exceed 30 seconds on a domain where several candidate addresses are checked; the cell then shows an error, and the credit has already been spent.
  • Anyone who can edit the spreadsheet can open its Apps Script project, and with it the key. Give edit access only to people who may use the key, and rotate the key in Cold Leads (Settings → API keys) if that changes.

Reading the results

FunctionResultMeaning
VERIFY_EMAILvalid, 97 or 80, okA mail server accepted the mailbox (80 for role addresses such as info@).
VERIFY_EMAILvalid, 75 or 65, smtp_unreachableThe domain accepts mail, but the mailbox itself was not checked.
VERIFY_EMAILrisky, catch_all or smtp_unknownThe server accepts any address, or gave no definite answer.
VERIFY_EMAILrisky, timeoutThe check ran out of time; it runs again next time.
VERIFY_EMAILinvalid, mailbox_missing, no_mx or syntaxDo not use the address.
FIND_EMAILaddress, verifiedA mail server confirmed the mailbox.
FIND_EMAILaddress, patternA guess from common address formats; confirm it before use.
FIND_EMAILempty, noneNo candidate: the domain has no mail server, or the name could not be used.

Limits and costs

  • 1 Cold Leads credit per call that is not answered from the script cache. Cold Leads charges results from its own 30-day cache too, so the script cache is what saves credits.
  • Apps Script allows 20,000 URL Fetch calls a day on consumer accounts and 100,000 on Google Workspace, and 30 seconds per custom function call. Filling a formula into thousands of rows at once can fail with Script invoked too many times per second; fill in smaller blocks, or use a bulk job for large lists.
  • Cold Leads allows 120 requests per minute per key. The API is part of the Business plan: $99 a month with 10,000 credits a month; extra packs of 1,000 credits cost $5.

FAQ

The cell shows #ERROR!. What happened?

Hover over the cell to read the message: a missing COLDLEADS_API_KEY property, a Cold Leads error such as no_credits or rate_limited, or the 30-second limit of custom functions.

Does reopening the spreadsheet cost credits?

Sheets runs a custom function again mainly when its formula or input cells change. If it does run again with the same input within six hours, the script cache answers without calling Cold Leads.

How do I check thousands of addresses?

Use a bulk job instead of a custom function: up to 5,000 addresses per request, polled until done. The n8n and Python pages on this site show complete versions.