/* Apollo client. Two facts drive this whole file: 1. POST /api/v1/mixed_people/api_search costs 0 credits and DOES NOT return email addresses or phone numbers. Emails only come from the enrichment endpoints, and those DO cost credits. 2. So we search wide, score cheap, and enrich only the prospects that survive the ICP filter. Enriching all 100 hits when 40 pass is 60 wasted credits per trip. Auth header is `x-api-key`. Search needs the api_search scope or a master key; a plain key on the older /mixed_people/search path 403s. */ const BASE = 'https://api.apollo.io/api/v1'; async function call(path, body, key) { const r = await fetch(BASE + path, { method: 'POST', headers: { 'x-api-key': key, 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify(body) }); if (r.status === 429) throw new Error('apollo_rate_limited'); if (!r.ok) throw new Error(`apollo_${r.status}: ${(await r.text()).slice(0, 300)}`); return r.json(); } /* Turn "founders and CMOs at crypto companies in New York, 15-500 people" into the search body. Apollo caps paging at 100 per page / 500 pages. */ export function buildQuery(icp) { const q = { person_titles: icp.titles, person_locations: icp.locations, organization_num_employees_ranges: icp.headcount, per_page: 100, page: 1 }; if (icp.keywords?.length) q.q_organization_keyword_tags = icp.keywords; if (icp.industries?.length) q.organization_industry_tag_ids = icp.industries; return q; } export async function search(icp, want, key) { const q = buildQuery(icp); const out = []; for (let page = 1; out.length < want && page <= 500; page++) { const d = await call('/mixed_people/api_search', { ...q, page }, key); const rows = d.people || []; if (!rows.length) break; out.push(...rows.map(shape)); if (rows.length < q.per_page) break; } return out.slice(0, want); } /* Only the fields we actually use downstream. Note there is no email here, by design, and pretending otherwise is how people get surprised. */ function shape(p) { const o = p.organization || {}; return { apollo_id: p.id, first_name: p.first_name, last_name: p.last_name, title: p.title, seniority: p.seniority, linkedin_url: p.linkedin_url, city: p.city, state: p.state, company: o.name, domain: o.primary_domain || o.website_url, company_linkedin: o.linkedin_url, headcount: o.estimated_num_employees, industry: o.industry, founded_year: o.founded_year, email: null, email_status: 'not_requested' }; } /* Costs credits. Called only for prospects a human is about to look at. Bulk endpoint takes up to 10 records per call. */ export async function enrich(people, key) { const done = []; for (let i = 0; i < people.length; i += 10) { const batch = people.slice(i, i + 10); const d = await call('/people/bulk_match', { reveal_personal_emails: false, details: batch.map((p) => ({ first_name: p.first_name, last_name: p.last_name, domain: p.domain, linkedin_url: p.linkedin_url })) }, key); (d.matches || []).forEach((m, n) => { const p = batch[n]; done.push({ ...p, email: m?.email || null, email_status: m?.email_status || 'not_found' }); }); } return done; }