If you're building outbound sales tooling, a CRM integration, or any pipeline that needs real mobile phone numbers, here's how to do it in TypeScript with a single API call.
We'll use the Million Phones API to look up a verified phone number from a LinkedIn profile handle.
Setup
Initialize a project and install dependencies:
mkdir phone-lookup && cd phone-lookup
npm init -y
npm install tsx
No extra HTTP libraries needed — we'll use the native fetch available in Node 18+.
The Lookup Function
Create lookup.ts:
interface PhoneResponse {
phone_numbers: string[];
}
async function lookupPhone(socialUrl: string): Promise<string[] | null> {
const apiKey = process.env.MILLIONPHONES_API_KEY;
if (!apiKey) {
throw new Error("Missing MILLIONPHONES_API_KEY environment variable");
}
const url = `https://millionphones.com/v1/phone?social_url=${encodeURIComponent(socialUrl)}`;
const response = await fetch(url, {
headers: {
"x-api-key": apiKey,
},
});
if (!response.ok) {
console.error(`Lookup failed: ${response.status} ${response.statusText}`);
return null;
}
const data = (await response.json()) as PhoneResponse;
return data.phone_numbers?.length ? data.phone_numbers : null;
}
Using It
Add a simple runner at the bottom of the same file:
async function main() {
const handle = process.argv[2];
if (!handle) {
console.log("Usage: npx tsx lookup.ts <linkedin-handle>");
process.exit(1);
}
console.log(`Looking up: ${handle}`);
const phones = await lookupPhone(handle);
if (phones) {
phones.forEach((phone) => console.log(`✅ ${phone}`));
} else {
console.log("❌ No verified number found");
}
}
main();
Run it:
export MILLIONPHONES_API_KEY=your_key_here
npx tsx lookup.ts williamhgates
Output:
Looking up: williamhgates
✅ +1-833-457-0192
Batch Lookups
Most real pipelines need to process a list, not a single handle. Here's a version that reads from a file and respects rate limits:
import { readFileSync, writeFileSync } from "fs";
async function batchLookup(inputFile: string, outputFile: string) {
const handles = readFileSync(inputFile, "utf-8")
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
console.log(`Processing ${handles.length} handles...`);
const results: { handle: string; phones: string[] | null }[] = [];
for (const handle of handles) {
const phones = await lookupPhone(handle);
results.push({ handle, phones });
// Simple rate limiting — 200ms between requests
await new Promise((resolve) => setTimeout(resolve, 200));
}
writeFileSync(outputFile, JSON.stringify(results, null, 2));
console.log(`Results written to ${outputFile}`);
}
Create a handles.txt with one LinkedIn handle per line, then:
npx tsx lookup.ts --batch handles.txt results.json
Error Handling in Production
For anything beyond a quick script, add retry logic:
async function lookupWithRetry(
socialUrl: string,
retries = 3
): Promise<string[] | null> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const result = await lookupPhone(socialUrl);
if (result) return result;
} catch (err) {
console.error(`Attempt ${attempt} failed:`, err);
if (attempt < retries) {
await new Promise((r) => setTimeout(r, 1000 * attempt));
}
}
}
return null;
}
What's Next
This is the building block for a composable GTM stack — no spreadsheet UI, no credit-based pricing, just a script that does what you need. Plug this into a Supabase database, sync results to Google Sheets for your SDRs, and you've replaced most of what tools like Clay charge $150+/month for.
Full API docs at millionphones.com/docs. Free tier includes 50 credits to test with.
Built by Jack — building verified phone data for sales teams at Million Phones.
Top comments (0)