Skip to navigation

Lead Enrichment

Batch-enrich a list of leads with LinkedIn URLs and verified emails

A worked example: take a CSV of name,company_domain rows and enrich each with the LinkedIn profile URL and verified professional email.

What this example does

  1. Reads a list of leads.
  2. For each lead, calls get_linkedin_profile_url (5 credits) and get_email (5 credits) in parallel.
  3. Writes the enriched output as JSON.

Cost: 10 credits per lead (~$0.10).

Code

import { readFile, writeFile } from "node:fs/promises";
const KEY = process.env.GTM_TOOLS_API_KEY!;
interface Lead {
name: string;
company_domain: string;
lead_id?: string;
}
interface EnrichedLead extends Lead {
linkedin_profile_url?: string;
email?: string;
email_status?: string;
email_reason?: string;
}
async function call<T>(tool: string, body: unknown): Promise<T | null> {
const res = await fetch(`https://api.gtm-tools.sh/api/v0/${tool}`, {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
console.error(`${tool} ${res.status}: ${await res.text()}`);
return null;
}
return res.json();
}
async function enrich(lead: Lead): Promise<EnrichedLead> {
const [profile, email] = await Promise.all([
call<{ url?: string }>("get_linkedin_profile_url", {
name: lead.name,
domain: lead.company_domain,
}),
call<{ email?: string; status?: string; reason?: string }>("get_email", {
name: lead.name,
domain: lead.company_domain,
input_parameters: { lead_id: lead.lead_id },
}),
]);
return {
...lead,
linkedin_profile_url: profile?.url,
email: email?.email,
email_status: email?.status,
email_reason: email?.reason,
};
}
async function batch(leads: Lead[], concurrency = 5): Promise<EnrichedLead[]> {
const out: EnrichedLead[] = [];
for (let i = 0; i < leads.length; i += concurrency) {
const slice = leads.slice(i, i + concurrency);
const enriched = await Promise.all(slice.map(enrich));
out.push(...enriched);
console.log(`enriched ${out.length}/${leads.length}`);
}
return out;
}
const leads: Lead[] = [
{ lead_id: "1", name: "Justin Mares", company_domain: "kettleandfire.com" },
{ lead_id: "2", name: "Sara Blakely", company_domain: "spanx.com" },
{ lead_id: "3", name: "Brian Chesky", company_domain: "airbnb.com" },
];
const enriched = await batch(leads);
await writeFile("./out.json", JSON.stringify(enriched, null, 2));

Sample output

[
{
"lead_id": "1",
"name": "Justin Mares",
"company_domain": "kettleandfire.com",
"linkedin_profile_url": "https://linkedin.com/in/justinmares",
"email": "justin@kettleandfire.com",
"email_status": "ok"
},
{
"lead_id": "2",
"name": "Sara Blakely",
"company_domain": "spanx.com",
"linkedin_profile_url": "https://linkedin.com/in/sarablakely",
"email": "sara@spanx.com",
"email_status": "ok"
}
]

Notes

  • Concurrency: 5 is safe. Going above 10 risks 429s on the email endpoint.
  • Non-ok results: email is null whenever status isn’t ok, and the reason says why (catch_all, no_mx, unverifiable_provider, no_pattern_verified). Branch on those rather than treating a missing address as a transient failure. See Why is get_email returning not_found?.
  • Failed calls still cost: the credit cost is debited before the tool reaches its provider, so an upstream 5xx or a try_again_later is charged. Honor retry_after_seconds instead of retrying tightly.
  • Cost: 10 credits per lead × 100 leads = 1,000 credits = $10.

Next steps