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 tokens) and get_email (5 tokens) in parallel.
  3. Writes the enriched output as JSON.

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

Code

1import { readFile, writeFile } from "node:fs/promises";
2
3const KEY = process.env.GTM_TOOLS_API_KEY!;
4
5interface Lead {
6 name: string;
7 company_domain: string;
8 lead_id?: string;
9}
10
11interface EnrichedLead extends Lead {
12 linkedin_profile_url?: string;
13 email?: string;
14 email_status?: string;
15 email_reason?: string;
16}
17
18async function call<T>(tool: string, body: unknown): Promise<T | null> {
19 const res = await fetch(`https://api.gtm-tools.sh/api/v0/${tool}`, {
20 method: "POST",
21 headers: {
22 "Authorization": `Bearer ${KEY}`,
23 "Content-Type": "application/json",
24 },
25 body: JSON.stringify(body),
26 });
27 if (!res.ok) {
28 console.error(`${tool} ${res.status}: ${await res.text()}`);
29 return null;
30 }
31 return res.json();
32}
33
34async function enrich(lead: Lead): Promise<EnrichedLead> {
35 const [profile, email] = await Promise.all([
36 call<{ url?: string }>("get_linkedin_profile_url", {
37 name: lead.name,
38 domain: lead.company_domain,
39 }),
40 call<{ email?: string; status?: string; reason?: string }>("get_email", {
41 name: lead.name,
42 domain: lead.company_domain,
43 input_parameters: { lead_id: lead.lead_id },
44 }),
45 ]);
46
47 return {
48 ...lead,
49 linkedin_profile_url: profile?.url,
50 email: email?.email,
51 email_status: email?.status,
52 email_reason: email?.reason,
53 };
54}
55
56async function batch(leads: Lead[], concurrency = 5): Promise<EnrichedLead[]> {
57 const out: EnrichedLead[] = [];
58 for (let i = 0; i < leads.length; i += concurrency) {
59 const slice = leads.slice(i, i + concurrency);
60 const enriched = await Promise.all(slice.map(enrich));
61 out.push(...enriched);
62 console.log(`enriched ${out.length}/${leads.length}`);
63 }
64 return out;
65}
66
67const leads: Lead[] = [
68 { lead_id: "1", name: "Justin Mares", company_domain: "kettleandfire.com" },
69 { lead_id: "2", name: "Sara Blakely", company_domain: "spanx.com" },
70 { lead_id: "3", name: "Brian Chesky", company_domain: "airbnb.com" },
71];
72
73const enriched = await batch(leads);
74await writeFile("./out.json", JSON.stringify(enriched, null, 2));

Sample output

1[
2 {
3 "lead_id": "1",
4 "name": "Justin Mares",
5 "company_domain": "kettleandfire.com",
6 "linkedin_profile_url": "https://linkedin.com/in/justinmares",
7 "email": "justin@kettleandfire.com",
8 "email_status": "ok"
9 },
10 {
11 "lead_id": "2",
12 "name": "Sara Blakely",
13 "company_domain": "spanx.com",
14 "linkedin_profile_url": "https://linkedin.com/in/sarablakely",
15 "email": "sara@spanx.com",
16 "email_status": "ok"
17 }
18]

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 token 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 tokens per lead × 100 leads = 1,000 tokens = $10.

Next steps