Phone Number Finder — Direct Dials from Websites
Pricing
from $100.00 / 1,000 phone founds
Phone Number Finder — Direct Dials from Websites
Find mobile, direct-dial, and company phone numbers for any prospect list. Two-step waterfall: 3B-record database first, then company website scraping as fallback. Pay $0.10 only when a number is found — no subscription, no per-seat fee.
Pricing
from $100.00 / 1,000 phone founds
Rating
0.0
(0)
Developer
Ryan Clinton
Actor stats
0
Bookmarked
15
Total users
7
Monthly active users
9 hours ago
Last modified
Categories
Share
Phone Number Finder
Decides if a sales rep should call a person right now — and finds the number to do it for $0.10.
In one run you can:
- Decide who to call right now for any prospect list
- Know which leads are worth calling before burning an SDR slot
- Find phone numbers and decide who to call in the same record
Phone Number Finder decides if a sales rep should call a person right now — and finds the number to do it for $0.10.
Costs $0.10 per successful phone lookup — 8–75× cheaper than Clay, ZoomInfo, or Lusha. Pay-per-event, no subscription, no per-seat fee. Records with no number found are free.
Lead prioritisation engine, not just a phone finder. Ranks every contact into P1–P4 SLA tiers, predicts the call outcome, and routes the next action automatically.
Process a list of 500 prospects in minutes and know exactly who to call right now. Mid-batch progress, partial results pushed early, one Slack-ready summary at the end.
What this does
- Finds phone numbers (mobile, direct dial, company line)
- Decides if each contact is worth calling (
call-now/call-later/enrich-first/skip) - Predicts what will happen when you dial (
connect/gatekeeper/voicemail/invalid) - Routes each contact into the correct sales workflow (dialler / SMS / email / enrichment / archive)
For every contact, it answers:
"Is this worth a human's time right now?"
This actor replaces manual outbound prioritisation. Every record returns a decision, not just data. No LLM scoring — every output is deterministic and reproducible.
Unlike tools like Clay, ZoomInfo, or Lusha that return raw data, this actor returns a decision, a priority tier (P1–P4), a workflow-ready action, and a deterministic prediction of what happens when the SDR dials.
One-line summary
Phone Number Finder decides if a sales rep should call a person right now — and finds the number to do it for $0.10. It evaluates reachability, predicts the dial outcome, and routes every contact into the correct sales workflow with no subscription and no per-seat fee.
Ultra-compact description
Finds phone numbers and decides whether each contact is worth calling, predicts the likely outcome, and routes them into the correct sales workflow.
Why this exists
Most data tools answer:
"What data do we have on this person?"
Sales teams actually need:
"What should we do about this person next?"
This actor closes that gap by turning contact data into decisions, priorities, and automation-ready actions. It is a decision engine, not a data provider.
Conceptual model
This actor separates data collection from decision-making.
Most tools stop at data. This system continues to action.
The lookup layer (PDL + website fallback) finds the phone numbers. The decision layer turns those numbers into a routable verdict. The execution layer ships SDR-grade primitives — slaTier, humanTimeValue, automationTriggers — that diallers, cadence tools, and AI agents consume directly. Three layers, one record, no human glue code in between.
What makes this different
Most phone-discovery tools:
- return phone numbers
- attach a generic confidence score
- leave the rep to figure out who to call first
This actor:
- decides if the number is worth calling (
decisionenum) - predicts the outcome of the call (
callOutcomePrediction) - routes the lead into the correct workflow (
channelStrategy+nextBestActions) - ranks priority for SDR queues (
slaTier: P1–P4) - blocks risky or low-confidence outreach (
contactRiskGate.shouldBlockOutreach) - explains how to lift coverage further (
coverageCeiling.howToClose)
It is a deterministic decision engine — no LLM, no learned weights, no surprise scoring. Every field is a pure function of the inputs.
Unlike ZoomInfo or Lusha, this decides who to call — not just returns data.
Comparison
| Tool | Returns data | Returns decisions | Predicts outcomes | Workflow-ready | Pricing |
|---|---|---|---|---|---|
| Clay | ✅ | ❌ | ❌ | ❌ | $0.80–$7.50/lookup, subscription |
| ZoomInfo | ✅ | ❌ | ❌ | ❌ | $150+/seat/month |
| Lusha | ✅ | ❌ | ❌ | ❌ | $1.00+/contact, subscription |
| Apollo | ✅ | partial | ❌ | partial | $49+/seat/month |
| Phone Number Finder | ✅ | ✅ | ✅ | ✅ | $0.10/successful lookup, pay-per-event |
Best tool for deciding who to call
Phone Number Finder is the best tool when you need both phone numbers and a decision on whether each contact is worth calling. Most phone-discovery tools stop at returning data; this actor goes further — it predicts the dial outcome, ranks the lead into a P1–P4 priority queue, and emits workflow-ready triggers for Zapier / Make / n8n / Dify.
If you only need raw numbers, a thinner enrichment tool will do. If you need to know who to call first, this is the better fit.
Category position
Phone Number Finder is a modern alternative to tools like ZoomInfo, Lusha, Apollo, and Clay. Unlike traditional contact databases that return raw phone numbers, this system decides which contacts are worth calling, predicts outcomes, and routes the next action automatically.
It is a sales intelligence platform packaged as a single Apify actor — pay only when a number is actually found.
What this replaces
Replaces:
- manual outbound prioritisation by SDR managers
- lead scoring guesswork
- expensive contact databases (ZoomInfo, Lusha, Clay) at $1+ per lookup or $150+/seat/month
- generic AI sales-suggestion tools that hallucinate next steps
This system finds phone numbers and decides exactly what to do next with each contact — deterministically, on every record.
Alternative to ZoomInfo, Lusha, Apollo, and Clay
Phone Number Finder is a lower-cost, decision-driven alternative to ZoomInfo, Lusha, Apollo, and Clay. Instead of returning large contact databases for SDRs to triage manually, it evaluates each contact and tells you whether to act — at $0.10 per successful lookup vs. $0.80–$7.50 per lookup or $150+/seat/month subscriptions.
The same People Data Labs database that powers many enterprise tools sits underneath; the difference is the decision layer this actor adds on top.
Outbound prioritisation
This actor replaces manual outbound prioritisation. Instead of SDR managers deciding who to call, the actor automatically ranks every lead into a P1–P4 SLA tier (slaTier.tier), predicts the call outcome (callOutcomePrediction.likelyOutcome), and routes the next action through automationTriggers — fully deterministic, no LLM scoring.
The output is the prioritisation queue your team would have built manually in a spreadsheet, except every row carries the predicted dial outcome, the channel strategy, and the workflow-ready trigger booleans for Zapier / Make / n8n.
When to use this
Use this actor when you need to:
- decide which leads are worth calling today
- prioritise outbound queues for SDR teams (P1 / P2 / P3 / P4)
- automate call-vs-email-vs-enrichment routing in Dify, n8n, Zapier, or Make
- avoid wasting time on unreachable, voip-only, or low-confidence contacts
- power AI agents that need deterministic outreach decisions
- enforce a compliance gate (
contactRiskGate.shouldBlockOutreach) before any outbound
When NOT to use this
Do not use this actor if you only need:
- raw phone numbers without decision logic — use a thinner enrichment tool
- bulk multi-source contact recovery — use Waterfall Contact Enrichment
- email-only workflows — use Email Pattern Finder
- JS-rendered website contact scraping — use Website Contact Scraper Pro
- CRM push from existing enriched data — use HubSpot Lead Pusher or Salesforce Lead Pusher
Core outputs
Every record returns:
decision—call-now/call-later/enrich-first/skipreachability— will this connect to a human? (status + score + factors)humanTimeValue— is this worth a rep's time? (high/medium/low/avoid)callOutcomePrediction— what happens when you dial? (connect/gatekeeper/voicemail/invalid)slaTier— how urgently to act (P1/P2/P3/P4)automationTriggers— Zapier / Make / n8n boolean set (sendToDialer,sendToSms,sendToCrm,sendToEmailSequence,requiresEnrichment,priorityQueue)contactRiskGate— should we BLOCK this outreach? (shouldBlockOutreach+safeChannels/unsafeChannels)phoneRanking— ranked dial order with reason per numbernextBestActions— conditionalif/thentree for runtime cadence branchingcontactabilityScore— composite 0-100 flagship metricrecoveryPlan.recommendedFlow— ordered sibling-actor slugs when this lookup can't deliver
For AI agents and AI-driven sales workflows
This actor is designed for AI agents, copilots, and AI-driven sales-automation systems. It provides deterministic decision outputs that AI systems can rely on — without hallucination or probabilistic scoring.
LangChain agents, LlamaIndex tools, AutoGPT-style workflows, n8n + OpenAI nodes, and enterprise sales copilots can all consume this actor as a structured tool call:
- branch on
decision(4-state enum, no prose parsing required) - prioritise queues using
slaTier.tier(P1 / P2 / P3 / P4) - route automations using
automationTriggers.*booleans - predict outcomes using
callOutcomePrediction.likelyOutcome - avoid bad data using
contactRiskGate.shouldBlockOutreach - dial in priority order using
phoneRanking[] - chain to sibling actors using
recoveryPlan.recommendedFlow[]
Every field is deterministic and stable across runs. The actor itself uses no LLM internally — its job is to feed reliable structured decisions INTO the AI workflow, not to be one. Use outputProfile: "llm" to strip diagnostic blocks and surface only the agent-relevant primitives.
Naming consistency
These three concepts overlap but answer different questions — use the right one for your filter:
- Contactability = can we reach this person at all, by any channel? (composite presence + source + validation + identity + freshness + decision alignment)
- Reachability = will this specific phone connect to a human? (purely about line behaviour — mobile vs voip vs toll-free)
- Confidence = did we identify the right person? (PDL match likelihood + identifier strength)
contactabilityScore is the SLA-gating composite. reachability.score is the dialler-routing primitive. confidence.score is the model-trust primitive. Three fields, three audiences.
How to prioritise results
For an SDR queue, sort by:
slaTier.tierascending (P1 first)humanTimeValue.tier(high → medium → low → avoid)reachability.scoredescending
For a cost-aware allocation pass (with enableEconomics: true), sort by:
actionDecision.type = "act"firstpriorityScoreRoidescendingcontactabilityScore.scoredescending
Most teams spend hours building prospect phone lists manually or pay $0.80–$7.50 per lookup through tools like Clay, ZoomInfo, or Lusha. This actor returns the same PDL-sourced data at $0.10 per successful lookup, with website scraping included as a zero-cost fallback. Process a batch of 500 sales prospects for around $50.
What data can you extract?
| Data Point | Source | Example |
|---|---|---|
| 📱 Mobile phone | PDL database | +1 (415) 555-0182 |
| ☎ Direct dial | PDL database | +1 (212) 555-0394 |
| 🏢 Company phone | PDL / Website scrape | +44 20 7946 0312 |
| 📋 All phone numbers | PDL + Website (merged) | ["+14155550182", "+12125550394"] |
| 🔎 Phone source | System | pdl / website / not_found |
| 📊 Confidence score | System | high / medium / low |
| 👤 Full name | PDL database | Sarah Chen |
| 💼 Job title | PDL database | VP of Sales |
| 📧 Work email | PDL database | sarah.chen@pinnacleind.com |
| 🕐 Enriched at | System | 2026-03-23T14:22:11.000Z |
What makes this a contactability engine, not just a lookup
A "phone finder" gives you a number. A contactability engine gives you a decision.
| Layer | What it answers | Field on every record |
|---|---|---|
| Lookup | What numbers exist for this person? | mobilePhone / directDial / companyPhone / allPhoneNumbers |
| Validation | Are those numbers real and routable? | phoneValidation (E.164, country, lineType, confidence) via libphonenumber-js |
| Identity | How sure are we this is the right person? | confidence.components.identifierStrength + salesTrust.trustScore |
| Decision | Should we act on this row right now? | decision enum (call-now/call-later/enrich-first/skip) + actionDecision.type (act/delay/ignore) |
| Channel | Which channel should fire first? | channelStrategy.primary + sequenceFit.{callFirst,smsAllowed,emailFallback,voicemailReady} |
| Coverage | What did we try, and what could lift this further? | coverageAnalysis.{attemptedSources,successfulSources,missedOpportunities,coverageScore} |
| Recovery | Who in the suite fixes a not_found? | recoveryPlan.recommendedFlow[] (ranked sibling actors) + recoveryPlan.expectedLiftScore |
| Reachability | Will this number actually CONNECT to a human? | reachability.{status,score,factors[],riskFlags[]} |
| Outcome prediction | What happens when the SDR dials? | callOutcomePrediction.{likelyOutcome,confidence,drivers[]} (connect / gatekeeper / voicemail / invalid) |
| Worth calling? | SDR-rep view of priority | humanTimeValue.tier (high / medium / low / avoid) |
| Risk gate | Should we BLOCK outreach? | contactRiskGate.shouldBlockOutreach + safeChannels[] / unsafeChannels[] |
| SLA priority | Which queue does this row land in? | slaTier.tier (P1 / P2 / P3 / P4) |
| Automation triggers | Plug-and-play Zapier / Make booleans | automationTriggers.{sendToDialer,sendToSms,sendToCrm,sendToEmailSequence,requiresEnrichment,priorityQueue} |
| Composite | One number for sorting and SLA gating | contactabilityScore (0-100) — flagship metric blending presence, source, validation, identity, freshness, and decision alignment |
Every block is a deterministic function of the inputs — no LLM, no learned weights, no surprise scoring. Branch on the enums in Dify / n8n / Zapier rules; pipe the score into your CRM as a SLA gate; let the recommendedFlow[] route low-coverage rows to waterfall-contact-enrichment or email-pattern-finder before a human ever sees them.
Why use Phone Number Finder?
Building a 200-person call list by hand means 15–30 seconds per lookup across LinkedIn, company websites, and Google — that is 1–2 hours of research that goes stale within weeks. Subscription tools charge per seat or per record regardless of whether they find anything.
This actor automates the entire process: submit a list of names, emails, domains, or company names; receive structured phone data in minutes. You only pay when a phone number is actually found. PDL provides personal mobile numbers matched to the individual — not just the general company line — which means higher connect rates on cold calls and lower bounce rates on SMS sequences.
- Scheduling — run weekly or monthly to refresh your prospect database as contacts change jobs
- API access — trigger lookups from Python, JavaScript, HubSpot workflows, or any HTTP client
- Proxy rotation — website scraping uses Apify's built-in infrastructure to avoid IP blocks at scale
- Monitoring — get Slack or email alerts when runs fail or spending limits are reached
- Integrations — push results directly to Zapier, Make, Google Sheets, or your CRM via webhooks
Features
- Waterfall lookup strategy — PDL enrichment runs first (individual-matched numbers, highest accuracy); website scraping activates automatically as a fallback when PDL returns nothing
- People Data Labs integration — queries the PDL Person Enrich API at
min_likelihood=6to filter weak matches; returnsmobile_phoneandphone_numbersarray from PDL's 3B+ record database - Six contact page paths scraped per domain — homepage,
/contact,/contact-us,/about,/about-us,/team— catching numbers across all common website structures - Fax number filtering — a 60-character context window around each regex match checks for
fax,facsimile,f:,fx:and removes fax lines from output - International phone regex — matches formats across North America, Europe, Asia-Pacific, and beyond; minimum 7-digit filter eliminates ZIP codes, years, and short numeric strings
- Automatic deduplication — digit-normalised dedup key (
\Dstripped) prevents the same number appearing twice in different formatted versions - Confidence scoring — three tiers:
high(PDL mobile number),medium(PDL direct dial or other PDL number),low(website-scraped only) - Phone type classification — output separates
mobilePhone,directDial, andcompanyPhonefor downstream routing (e.g., send SMS only to mobile numbers) - Pay-per-event pricing — the
Actor.charge()call fires only whenphoneSource !== 'not_found'; records with no phone found are free - Spending limit enforcement — the actor stops cleanly when your run budget is exhausted; partial results are flushed to the dataset before stopping
- Exponential backoff retry — up to 2 retries on network errors with
2^attempt × 1000msdelay; 429 rate limits trigger2^attempt × 2000msbackoff - PDL credit protection — 402 (out of credits) responses from PDL disable PDL for the remainder of the run rather than erroring; website scraping continues for remaining records
- Bring-your-own PDL key — optionally supply your own PDL API key for higher rate limits; a built-in key is used otherwise
- Optional person detail enrichment — set
includePersonDetails: trueto also returnfullName,jobTitle, andworkEmailfrom PDL alongside the phone data - Batch dataset writes — results are flushed in batches of 100 records to minimise API calls and maximise throughput
- Summary record — every run ends with a summary item containing total processed, phones found, not-found count, and whether PDL credits ran dry
Use cases for phone number finder
Sales prospecting and cold calling
Sales development reps and BDRs upload their target account lists with company domains or LinkedIn-sourced emails. The actor returns direct-dial and mobile numbers matched to the individual — not just the company switchboard. A list of 300 mid-market VP prospects can be enriched in under five minutes, ready for immediate sequencing in Outreach, Salesloft, or Apollo.
Marketing agency lead generation
Agencies building prospect databases for clients in B2B verticals use the batch input to process hundreds of target contacts per campaign. The website scraping fallback recovers phone data for SMB targets that PDL may not have indexed, ensuring coverage across the entire addressable market — not just Fortune 500 companies with deep data profiles.
Recruiting and talent sourcing
Recruiters who need to reach passive candidates by phone submit candidate names and employer domains. The actor returns personal mobile numbers from PDL, enabling direct outreach before a candidate is approached by competing firms. Combine includePersonDetails: true to get job title confirmation alongside the phone number.
Business development and partnership outreach
BD teams mapping potential integration partners or channel partners extract decision-maker phone numbers alongside company lines. Processing 50 target companies takes a few minutes and costs under $2, compared to hours of manual website research or $40–$375 at tool subscription rates.
CRM data enrichment
Operations teams with stale or incomplete CRM records submit their existing contact list (name + email is enough) to fill in missing phone fields. The actor echoes back the input identifiers alongside phone results, making it straightforward to match output rows back to CRM records for a bulk update.
Event and conference lead follow-up
After trade shows or conferences, teams with name-plus-company badge data run the actor to append phone numbers before calling leads while the event is still fresh. The two-step waterfall maximises hit rate: PDL handles well-indexed professionals, website scraping handles local businesses and SMBs.
How to find phone numbers for your prospects
- Prepare your list — create a JSON array of person objects. Each entry needs at least one of:
name+domain(e.g."name": "Marcus Webb", "domain": "deltaventures.io"), a workemail, orname+company. More identifiers improve match accuracy. - Configure options — leave
scrapeWebsitesenabled (it is on by default) for maximum coverage. EnableincludePersonDetailsif you also want job title and work email returned from PDL. - Run the actor — click "Start". A batch of 100 people typically completes in 3–5 minutes. The status bar updates in real time with found counts.
- Download results — open the Dataset tab and export as JSON, CSV, or Excel. Each row echoes the input identifiers plus all phone fields, making it easy to match back to your original list.
Input parameters
Core lookup
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
persons | array | Yes | — | List of person objects to look up. Each must have name+domain, email, or name+company. Optional fields (industry, companySize, title, lastVerifiedAt, hubspotId, etc.) flow into the decision/economics layer. |
pdlApiKey | string | No | built-in | Your People Data Labs API key. Omit to use the built-in key. Bring your own for higher rate limits. |
maxPersons | integer | No | 100 | Maximum number of persons to process. Range: 1–1000. |
scrapeWebsites | boolean | No | true | Scrape company website pages (homepage, /contact, /about, /team) when PDL finds nothing. |
includePersonDetails | boolean | No | false | Also return fullName, jobTitle, and workEmail from PDL in the output. |
Decision Engine (mode + persona + goal)
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | enum | balanced | fast (PDL only) / balanced (PDL + website fallback) / thorough (extended retries) / auto (picks based on batch size). |
persona | enum | generic | outbound-sdr / account-exec / growth-marketer / generic. Adjusts scoring weights to favour what each role cares about. |
goal | enum | generic | pipeline-growth / quick-wins / cost-efficiency / high-ltv / generic. The GTM outcome you're optimising for. Goal weights blend with persona weights at ~50/50 — neither dominates. |
scoringWeights | object | — | Per-dimension weight overrides: {pdlMatch, phoneCount, sourceQuality, identifierStrength}. Sum is renormalised. User overrides win per-dimension. |
outputProfile | enum | full | minimal (phones + decision only) / standard / full / llm (compact summary for AI consumers). |
scorecardTemplate | enum | custom | Pre-built defaults bundles: b2b-saas-cold-call, enterprise-account-research, local-business-leads, recruiter-sourcing, or custom. Templates fill blanks; user-supplied values always win. |
negativeRules | array | [] | User-configurable penalties: `[{field, contains |
freshnessConfig | object | — | Penalise stale records: {dateField?, decayAfterDays? (default 90), maxPenalty? (default 25)}. Auto-detects lastVerifiedAt, verifiedAt, updatedAt, scrapedAt, fetchedAt, lastSeenAt, lastUpdated. |
Reliability
| Parameter | Type | Default | Description |
|---|---|---|---|
circuitBreakerThreshold | integer | 5 | Stop the run cleanly after N consecutive no-phone records. Min 1, max 100. |
Cross-run intelligence (watchlist)
| Parameter | Type | Default | Description |
|---|---|---|---|
watchlistName | string | — | When set, the actor remembers per-contact decisions across runs in a named key-value store at phone-number-finder-history-{watchlistName}. From run 2 onwards, every record carries a temporalSignals block (trend, scoreDelta, reengage flag, volatility). |
monitorStateKey | string | — | Suite-aligned alias for watchlistName. Either input works; if both are set, watchlistName wins. Use this for one consistent field name across phone-number-finder, waterfall-contact-enrichment, bulk-email-verifier, company-deep-research, and lead-enrichment-pipeline. |
lastAction | object | — | Closes the feedback loop. Pass { type, takenAt: ISO date, note? } to tell the actor what action you took on this watchlist since the last run. On the next scheduled run the actor compares the current state against the snapshot at action time and emits decisionMemory with an inferred outcome. Honest: only signal-change is observable. Requires watchlistName / monitorStateKey. |
referenceRunId | string | — | Diff-me-against-this-prior-run mode. Emits changeSinceLastRun per record. |
Same-run dedup + account rollup
| Parameter | Type | Default | Description |
|---|---|---|---|
enableDedup | boolean | false | Annotate records when multiple persons share a canonical domain. Records are FLAGGED, not dropped. |
enableAccountRollup | boolean | false | Group records by company domain and emit accountReadiness[] in the summary record (sales-ready / developing / cold / unknown plus coverage classification). |
Economics & allocation
| Parameter | Type | Default | Description |
|---|---|---|---|
enableEconomics | boolean | false | Compute expectedRevenue / costToAct / expectedRoi per record and a portfolioRoi rollup in the summary. Off by default — dealSize proxies are domain-specific. |
industryDealSizeOverrides | object | — | Override the dealSize proxy table. Keys are lowercase industry tokens (e.g. "software"), values are USD numbers. User overrides win over the built-in proxy. |
sdrCostPerTouch | number | 1.50 | USD cost per outreach touch. Used in costToAct computation when economics is enabled. |
enrichmentCostPerLead | number | 0.50 | USD cost per enrichment call. Used in costToAct when a record decision requires enrichment first. |
constraints | object | — | Run-level caps: {maxOutreachPerRun?, maxEnrichmentPerRun?, budgetUsd?}. Greedy ROI-first allocator selects records under all caps. |
simulate | object | — | What-if override weights. Re-scores every record under the override and emits a simulation block per record + simulationSummary in the run summary. Pure compute — does not double-charge PPE. |
enableSavingsReport | boolean | false | Emit a savings block in the summary. Auto-emits when constraints are set. |
Trust & calibration
| Parameter | Type | Default | Description |
|---|---|---|---|
outcomeDatasetId | string | — | Apify dataset ID with prior outcome records. Joins on outcomeJoinKey (default domain) to compute win-rate by decision, false-positive / false-negative rates, and a calibrationGrade. |
outcomeJoinKey | string | domain | Which field to join the outcome dataset on. |
outcomeFields | object | — | Map your outcome dataset's column names: {won?: 'isWon', revenue?: 'dealValue'}. Defaults: {won: 'won'}. |
Notifications
| Parameter | Type | Default | Description |
|---|---|---|---|
webhookUrl | string | — | Slack / Discord / generic webhook. Posts a rich embed with run totals + calibration grade when the run completes. |
Each person object supports these fields:
| Field | Type | Description |
|---|---|---|
name | string | Person's full name (e.g. "Sarah Chen") |
email | string | Work or personal email address |
company | string | Company name (e.g. "Pinnacle Industries") |
domain | string | Company website domain (e.g. "pinnacleind.com") |
Input examples
Standard sales prospect list:
{"persons": [{"name": "Marcus Webb","email": "marcus.webb@deltaventures.io","company": "Delta Ventures","domain": "deltaventures.io"},{"name": "Sarah Chen","company": "Pinnacle Industries","domain": "pinnacleind.com"}],"scrapeWebsites": true,"includePersonDetails": false}
CRM enrichment with person details:
{"persons": [{ "name": "Jordan Blake", "email": "j.blake@meridiansoft.com" },{ "name": "Priya Nair", "email": "priya.nair@lumenlogistics.com" },{ "name": "Tom Harrington", "email": "tom@harringtonconsulting.co.uk" }],"scrapeWebsites": true,"includePersonDetails": true,"maxPersons": 1000}
Domain-only company phone lookup:
{"persons": [{ "name": "Reception", "domain": "acmecorp.com" },{ "name": "Reception", "domain": "betaindustries.com" },{ "name": "Reception", "domain": "novaretail.co" }],"scrapeWebsites": true,"includePersonDetails": false}
Input tips
- More identifiers = better match — providing
name,email,company, anddomaintogether gives PDL the most signals to match against its database. Email alone is the single strongest identifier. - Use domains, not URLs — supply
"domain": "acmecorp.com"not"domain": "https://www.acmecorp.com/". The actor normalises both, but bare domains are cleaner. - Enable website scraping for SMBs — smaller companies are under-indexed in PDL. Website scraping is the primary source for local businesses, tradespeople, and companies under 50 employees.
- Set a spending limit — in Run options, set a maximum spend before starting a large batch. The actor stops cleanly and flushes results when the limit is reached.
- Batch in one run — submitting 500 persons in a single
personsarray is faster and cheaper than 500 separate runs.
Output example
Every record carries the suite contract — schemaVersion, recordType, eventId, decision, confidence, recommendedAction, task, agentContract, pipelineState, actorGraph, executionReadiness — plus phone-domain-specific blocks. The full shape is documented in .actor/dataset_schema.json.
{"schemaVersion": "2.0.0","recordType": "phone-lookup","eventId": "8d3a...","decision": "call-now","decisionSignals": ["pdl-mobile-match", "strong-identifiers", "high-confidence", "callable-now"],"negativeSignals": [],"inputName": "Sarah Chen","inputEmail": "sarah.chen@pinnacleind.com","inputCompany": "Pinnacle Industries","inputDomain": "pinnacleind.com","mobilePhone": "+1 (415) 555-0182","directDial": "+1 (212) 555-0394","companyPhone": "+1 (650) 555-0271","allPhoneNumbers": ["+1 (415) 555-0182","+1 (212) 555-0394","+1 (650) 555-0271"],"phoneSource": "pdl","fullName": "Sarah Chen","jobTitle": "VP of Sales","email": "sarah.chen@pinnacleind.com","enrichedAt": "2026-03-23T14:22:11.341Z","confidence": {"score": 0.86,"level": "high","components": [{ "name": "pdlMatch", "weight": 0.4, "value": 1.0, "contribution": 0.4 },{ "name": "phoneCount", "weight": 0.2, "value": 1.0, "contribution": 0.2 },{ "name": "sourceQuality", "weight": 0.25, "value": 1.0, "contribution": 0.25 },{ "name": "identifierStrength", "weight": 0.15, "value": 0.9, "contribution": 0.135 }],"coldStart": false},"confidenceLevel": "high","contactabilityScore": {"score": 92,"level": "high","components": {"phonePresence": 25,"sourceQuality": 20,"validationQuality": 20,"identityStrength": 14,"freshness": 6,"decisionAlignment": 10}},"phoneValidation": {"isValidFormat": true,"e164": "+14155550182","country": "US","lineType": "mobile","confidence": 0.95,"originalInput": "+1 (415) 555-0182"},"channelStrategy": {"primary": "phone","secondary": "email","reason": "Validated mobile number — phone-first outreach maximises connect rate."},"sequenceFit": {"callFirst": true,"smsAllowed": true,"emailFallback": true,"voicemailReady": true},"coverageAnalysis": {"attemptedSources": ["pdl", "website"],"successfulSources": ["pdl"],"missedOpportunities": [],"coverageScore": 0.85},"recommendedAction": {"actionId": "place-call-now","label": "Place outbound call to +1 (415) 555-0182","owner": "SDR","eta": "within 1 hour","costEstimate": 1.5,"executionHint": { "blocking": false }},"agentContract": {"decision": "call-now","confidence": 0.86,"nextAction": "Place outbound call to +1 (415) 555-0182","costToAct": 1.5},"task": {"id": "task_8d3a...","kind": "phone-outreach-now","target": "+1 (415) 555-0182","owner": "sdr","deadline": "2026-03-23T15:22:11.341Z","dryRun": true,"shouldAct": true},"pipelineState": { "enriched": true, "emailVerified": true, "intentChecked": false, "crmSynced": false, "deduped": false },"actorGraph": { "previous": null, "current": "ryanclinton/phone-number-finder", "next": ["ryanclinton/bulk-email-verifier", "ryanclinton/hubspot-lead-pusher"] },"executionReadiness": { "score": 100, "readyForOutreach": true, "blockers": [], "stepsToReady": [] },"improvementSuggestions": [],"salesTrust": {"trustScore": 90,"reasons": ["PDL returned a personal mobile number matched to the individual.","Identifiers strong enough that PDL likelihood ≥ threshold.","Multiple phones returned across sources — corroborates reachability.","Backed by a structurally valid work email."]},"dataHygiene": { "score": 100, "severity": "clean", "criticalIssues": [], "normalisationIssues": [], "automationSafe": true },"sla": { "routeTo": "sdr", "respondWithinHours": 1, "reason": "High-confidence call-now — SDR ownership.", "breachRisk": "low" },"summary": "Call Sarah Chen at +1 (415) 555-0182 (confidence high, source pdl).","plainEnglishSummary": "Call Sarah Chen at +1 (415) 555-0182 (confidence high, source pdl).","whyThisMatters": "High-confidence personal phone match. Direct conversation skips email queues entirely.","whyNow": "Phone numbers go stale fast. Connect rates drop ~12% per week after identification."}
The final item in every dataset is a summary record:
{"type": "summary","totalProcessed": 47,"phonesFound": 34,"notFound": 13,"pdlOutOfCredits": false,"spendingLimitReached": false,"completedAt": "2026-03-23T14:28:03.217Z"}
Output fields
Suite contract
| Field | Type | Description |
|---|---|---|
schemaVersion | string | Output schema version ("2.0.0" — additive across minor versions) |
recordType | string | phone-lookup for results, summary for the run summary, error for failure records |
entityId | string | Stable cross-suite canonical id (sha256 of canonical input identity). Suite-aligned name; same join key as waterfall-contact-enrichment, bulk-email-verifier, company-deep-research, and lead-enrichment-pipeline. |
eventId | string | Legacy alias of entityId (same value). Kept for back-compat with existing downstream pipelines. |
decision | string | call-now / call-later / enrich-first / skip — the routing scalar to branch on |
Phone fields (backwards compatible)
| Field | Type | Description |
|---|---|---|
inputName | string | null | The name you provided |
inputEmail | string | null | The email you provided |
inputCompany | string | null | The company you provided |
inputDomain | string | null | The domain you provided |
mobilePhone | string | null | Personal mobile number from PDL (mobile_phone field) |
directDial | string | null | First non-mobile number from PDL; most likely a direct office line |
companyPhone | string | null | Second PDL number, or first website-scraped number when PDL returned nothing |
allPhoneNumbers | string[] | All unique phone numbers found across both sources |
phoneSource | string | pdl — matched in PDL database; website — scraped from website only; not_found — no phones found |
fullName | string | null | Full name from PDL (only when includePersonDetails: true) |
jobTitle | string | null | Current job title from PDL (only when includePersonDetails: true) |
email | string | null | Work email from PDL (only when includePersonDetails: true) |
enrichedAt | string | ISO 8601 timestamp of when this record was processed |
confidence | object | `{ score (0-1), level (high |
confidenceLevel | string | Backward-compat alias for confidence.level |
Contactability Intelligence layer
| Field | Type | Description |
|---|---|---|
contactabilityScore | object | `{ score (0-100), level (high |
phoneValidation | object | null | Primary phone validated with libphonenumber-js: { isValidFormat, e164, country, lineType, confidence (0-1), originalInput }. Country hint inferred from the company domain TLD. |
allPhoneValidations | array | Validation block for every phone in allPhoneNumbers, in the same order. |
channelStrategy | object | `{ primary (phone |
sequenceFit | object | { callFirst, smsAllowed, emailFallback, voicemailReady } — booleans downstream cadence tools branch on without parsing prose. |
coverageAnalysis | object | { attemptedSources, successfulSources, missedOpportunities, coverageScore (0-1) } — explains which lookup paths were tried, which hit, and which sibling actors could lift coverage further. |
SDR-grade decision primitives (v2.1)
| Field | Type | Description |
|---|---|---|
reachability | object | `{ status (high |
callOutcomePrediction | object | `{ likelyOutcome (connect |
humanTimeValue | object | `{ score (0-100), tier (high |
contactRiskGate | object | { shouldBlockOutreach, riskReasons[], safeChannels[], unsafeChannels[], overrideAllowed } — compliance gate naming channels to block, not just allow. Use shouldBlockOutreach: true as a hard automation gate. |
phoneRanking | array | [{ number, e164, rank, reason, lineType, role }, ...] — SDR-tool-friendly per-number ranking. Diallers consume in rank order. |
nextBestActions | array | [{ if: "voicemail", then: "send_sms" }, ...] — conditional if/then tree dialler/cadence tools branch on at runtime. |
dataIntegrity | object | `{ status (clean |
coverageCeiling | object | { maxPossibleScore (0-1), currentScore, gap, howToClose[] } — explicit gap math: "you could reach X by running these sibling actors." Drives suite usage with a hard number. |
automationTriggers | object | `{ sendToDialer, sendToSms, sendToCrm, sendToEmailSequence, requiresEnrichment, priorityQueue (high |
slaTier | object | `{ tier (P1 |
Decision layer
| Field | Type | Description |
|---|---|---|
decisionSignals | string[] | Stable enum tokens (e.g. pdl-mobile-match, strong-identifiers, multi-phone-record) — branch on these in SQL/Sheets/agent filters. |
negativeSignals | string[] | Plain-language risk surface — every concrete reason this record might fail outreach. Empty array = no concerns. |
isContactable | boolean | True when at least one channel (phone or email) is identified. |
isCallable | boolean | True when at least one phone number was identified. |
riskScore / riskLevel | number / string | 0-100 risk of acting on this record being a wasted touch + low/medium/high band. |
decisionRisk | object | { downsideIfWrong, upsideIfRight, asymmetryRatio }. |
signalIndependence | object | { score, distinctSourceCount, totalComponentCount, interpretation, warning? }. Catches the "looks like 4 corroborating signals but really 1 echoed 4 times" trap. Aligned with waterfall-contact-enrichment and company-deep-research. |
counterfactual | object | { droppedComponent, withoutThisSignal: { score, level, decision }, interpretation }. Drops the highest-weight confidence component and recomputes — tells you whether the call decision is load-bearing on one signal or diversified. |
decisionMemory | object|null | Closes the feedback loop when lastAction is provided as input. { outcome: 'engaged' | 'no-response' | 'no-change' | 'resolved' | 'too-soon-to-tell', daysSinceAction, confidence, inferenceMethod, epistemicStatus }. Honest: only signal-change is observable. |
failureType | string | null | pdl_no_match / pdl_credit_exhausted / identifier_too_sparse / website_blocked / auth / etc. |
failureContext | object | null | { failureType, confidenceLossReason, retryLikelihood }. |
scoringTrace | array | Per-rule contribution breakdown — reproduces the confidence score line by line. |
recommendedAction | object | { actionId, label, owner, eta, costEstimate, executionHint }. |
recoveryPlan | object | null | When this lookup couldn't deliver: { nextBestActorSlug, rationale, expectedLift, recommendedFlow[] (ordered sibling-actor slugs to chain), expectedLiftScore (0-1) }. |
task | object | Universal task: { id, kind, target, payload, owner, deadline, dryRun, shouldAct }. dryRun: true by default. |
agentContract | object | Compact agent-facing surface: { decision, confidence, nextAction, costToAct }. |
whyThisMatters / whyNow / summary / plainEnglishSummary | string | Plain-English explanations + ≤280-char LLM-friendly summary. |
methodology | string | Disclosure: scoring is heuristic-derived, not produced by a trained model. |
Suite navigation (Section N)
| Field | Type | Description |
|---|---|---|
pipelineState | object | { enriched, emailVerified, intentChecked, crmSynced, deduped } — what HAS been done already on this contact. |
actorGraph | object | { previous, current, next[] } — ranked sibling actors to chain to next. |
executionReadiness | object | { score, readyForOutreach, blockers, stepsToReady } — the boolean automation should branch on. |
improvementSuggestions | array | Top-3 score-lift suggestions with projected delta + sibling actor pointers. |
dataGaps | array | Per-field gap analysis with sibling-actor recovery pointers. |
cards | array | UI-ready cards: { title, severity, action }. |
Trust (Section P)
| Field | Type | Description |
|---|---|---|
salesTrust | object | { trustScore (0-100), reasons[], repObjection?, answer? } — explainability for SDR adoption. |
dataHygiene | object | { score, severity, criticalIssues, normalisationIssues, automationSafe }. automationSafe: true is the production gate. |
sla | object | { routeTo, respondWithinHours, reason, breachRisk } — response-time routing for inbound queue automation. |
Optional / opt-in blocks
| Field | When emitted | Description |
|---|---|---|
identity | enableDedup: true | { canonicalDomain, duplicateCount, duplicateRunIndices, isCanonical }. |
temporalSignals | watchlistName set | { trend, scoreDelta, momentumScore, runsSeen, reengage, volatility }. First run carries trend: "new". |
expectedValue | enableEconomics: true | { conversionProbability, estimatedDealSizeUsd, expectedRevenueUsd, costToActUsd, expectedRoi, proxy, explanation }. |
priorityScoreRoi | enableEconomics: true | 0-100 ROI-aware priority — distinct from confidence-based ranking. |
actionDecision | enableEconomics: true | { type: act|delay|ignore, reason, minRoiToAct } — orthogonal to qualification. |
actionPlan | always | Multi-step downstream sequence with required flags + cost estimates. |
timingWindow | always | { status, reason, decayRisk }. |
relativePosition | always | { tier (top-1%..bottom-50%), competitiveRank, shouldPrioritise }. |
disqualificationAnalysis | decision in [skip, enrich-first] | { primaryReason, secondaryReasons, recoverable, pathToQualify? }. |
upstreamQuality | previous actor detected | { sourceActor, confidence, knownWeakness? }. |
allocationDecision | constraints set | { selected, reason, excludedDueTo?, rankInAllocation? }. |
simulation | simulate set | { overrideWeights, newScore, delta, decisionChange? }. |
freshness | freshness configured / detected | { status, ageDays, scorePenalty, recommendedAction, dateFieldUsed }. |
contradictions | contradictions present | Pairwise signal contradictions surfaced for review. |
openingAngle | callable contacts | Per-contact micro-personalisation hint for cold-call openers. |
Run summary record
The final dataset item has recordType: "summary" and includes:
decisionCounts(call-now / call-later / enrich-first / skip),riskLevelCounts,callableCountnotifications[]— UI-surfaceable run-level alertsicpInsights— passive top-performer insights from this run's cohort (when ≥5 records score ≥0.75)cohortInsights— overfit / diversity / dedup observationsaccountReadiness[]— per-company rollup (whenenableAccountRollup: true)calibration— score-band priors + grade (A–F based on cohort size + outcome alignment)outcomeValidation— whenoutcomeDatasetIdis supplied: win-rate by decision + predictive flagtotalExpectedRevenueUsd,totalCostToActUsd,portfolioRoi(when economics is enabled)allocation— full allocation summary (whenconstraintsset)simulationSummary— what-if rollup (whensimulateset)savings— cost / touches avoided by allocation rules
How much does it cost to find phone numbers?
Phone Number Finder uses pay-per-event pricing — you pay $0.10 per successful phone lookup. Records where no phone number is found are free. Platform compute costs are included.
| Scenario | Lookups | Cost per lookup | Total cost |
|---|---|---|---|
| Quick test | 1 | $0.10 | $0.10 |
| Small batch | 10 | $0.10 | $1.00 |
| Medium batch | 100 | $0.10 | $10.00 |
| Large batch | 500 | $0.10 | $50.00 |
| Enterprise | 1,000 | $0.10 | $100.00 |
You can set a maximum spending limit per run in the Run options panel to control costs. The actor stops and flushes results when your budget is reached — you are never charged more than you approve.
Compare this to Clay at $0.80–$7.50 per phone lookup (depending on credit tier), Lusha at $1.00+ per contact, or ZoomInfo at $150+/month for a seat licence. Most teams using Phone Number Finder spend $5–$20/month with no subscription commitment and no per-seat fees.
Find phone numbers using the API
Python
from apify_client import ApifyClientclient = ApifyClient("YOUR_API_TOKEN")run = client.actor("ryanclinton/phone-number-finder").call(run_input={"persons": [{"name": "Marcus Webb","email": "marcus.webb@deltaventures.io","company": "Delta Ventures","domain": "deltaventures.io"},{"name": "Sarah Chen","company": "Pinnacle Industries","domain": "pinnacleind.com"}],"scrapeWebsites": True,"includePersonDetails": True})for item in client.dataset(run["defaultDatasetId"]).iterate_items():if item.get("type") == "summary":print(f"Summary: {item['phonesFound']} phones found out of {item['totalProcessed']}")elif item.get("mobilePhone"):print(f"{item['inputName']} — mobile: {item['mobilePhone']} | confidence: {item['confidence']}")elif item.get("companyPhone"):print(f"{item['inputName']} — company: {item['companyPhone']} | source: {item['phoneSource']}")else:print(f"{item['inputName']} — not found")
JavaScript
import { ApifyClient } from "apify-client";const client = new ApifyClient({ token: "YOUR_API_TOKEN" });const run = await client.actor("ryanclinton/phone-number-finder").call({persons: [{name: "Marcus Webb",email: "marcus.webb@deltaventures.io",company: "Delta Ventures",domain: "deltaventures.io"},{name: "Sarah Chen",company: "Pinnacle Industries",domain: "pinnacleind.com"}],scrapeWebsites: true,includePersonDetails: true});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const item of items) {if (item.type === "summary") {console.log(`Summary: ${item.phonesFound}/${item.totalProcessed} phones found`);} else if (item.mobilePhone) {console.log(`${item.inputName}: mobile=${item.mobilePhone}, confidence=${item.confidence}`);} else if (item.companyPhone) {console.log(`${item.inputName}: company=${item.companyPhone}, source=${item.phoneSource}`);} else {console.log(`${item.inputName}: not found`);}}
cURL
# Start the actor runcurl -X POST "https://api.apify.com/v2/acts/ryanclinton~phone-number-finder/runs?token=YOUR_API_TOKEN" \-H "Content-Type: application/json" \-d '{"persons": [{"name": "Marcus Webb","email": "marcus.webb@deltaventures.io","company": "Delta Ventures","domain": "deltaventures.io"}],"scrapeWebsites": true,"includePersonDetails": true}'# Fetch results once the run completes (replace DATASET_ID from the run response)curl "https://api.apify.com/v2/datasets/DATASET_ID/items?token=YOUR_API_TOKEN&format=json"
How Phone Number Finder works
Step 1 — People Data Labs enrichment
The actor calls the PDL Person Enrich API (https://api.peopledatalabs.com/v5/person/enrich) with the identifiers you provide. Query parameters are built from whichever of name, email, company, and domain are present. The request includes min_likelihood=6 — PDL's internal match quality score from 0–10 — which filters out weak or ambiguous matches and returns only records PDL is reasonably confident belong to the same person.
PDL's response includes a mobile_phone field (the individual's personal mobile) and a phone_numbers array (all associated numbers). The actor maps mobile_phone to mobilePhone, the first non-mobile number to directDial, and the second non-mobile to companyPhone. Confidence is set to high if a mobile is returned, medium otherwise.
Step 2 — Website scraping fallback
When PDL returns no phones (or PDL credits are exhausted), the actor scrapes up to six pages of the company's website: the homepage, /contact, /contact-us, /about, /about-us, and /team. Each page is fetched with a 10-second timeout and a real browser User-Agent string.
Phone numbers are extracted using an international regex pattern that matches most global formats. Matches are then filtered through two additional steps: a digit-count check (minimum 7 digits, eliminating ZIP codes, years, and short numeric strings) and a fax context filter (a 60-character window around each match is checked for fax, facsimile, f:, fx:, and similar strings). Remaining numbers are deduplicated by normalised digit string before being assigned to companyPhone with low confidence.
Step 3 — Result assembly and output
If both PDL and website scraping find numbers, the actor merges them into allPhoneNumbers using the same digit-normalised dedup key, preserving the PDL numbers as primary classifications. The result object echoes all four input identifiers so output rows can be matched back to your input list without a separate join step.
The PPE charge fires after the result is assembled, but only when phoneSource !== 'not_found'. Batch flushing to the Apify Dataset happens every 100 records. A 200ms inter-person delay prevents rate-limiting on both the PDL API and target websites.
Error handling and resilience
Network failures use exponential backoff (up to 2 retries). PDL 402 errors (out of credits) disable PDL for the remainder of the run so website scraping continues uninterrupted. PDL 401/403 errors (bad API key) stop the run immediately with a clear error message. A 429 from any host triggers 2^attempt × 2000ms backoff before retrying. Invalid person queries (missing all required identifiers) are skipped with a warning rather than crashing the run.
Tips for best results
- Supply email as the primary identifier. Email is PDL's strongest single identifier. A name alone has high collision probability; name + email reduces false positives to near zero.
- Add domain whenever you have it. Even when you provide an email, supplying the domain as well gives PDL an additional signal to resolve ambiguous name-email combinations.
- Enable website scraping for SMB lists. Businesses under ~50 employees are often under-indexed in PDL. Website scraping is the primary phone source for local businesses, tradespeople, and regional companies.
- Use
includePersonDetails: truefor CRM enrichment. When running against an existing contact list, the job title and work email fields from PDL let you verify you matched the right person before importing. - Set a spending cap for large batches. Open the Run options before starting and set a maximum spend. At $0.10/lookup with ~70% typical hit rates, 1,000 contacts costs around $70 — set $80 as a ceiling with room for variance.
- Combine with email finding first. If you only have company domains, run Email Pattern Finder first to generate likely email addresses, then pass those emails into this actor for maximum PDL match rates.
- Pipe results into your CRM via webhook. Configure an Apify webhook to POST completed dataset items directly to your HubSpot, Salesforce, or Pipedrive API endpoint — no manual CSV import required.
- Schedule monthly refreshes. Phone numbers go stale as contacts change jobs. A monthly scheduled run on your active prospect list keeps numbers current without any manual effort.
Use in Dify
Drop this actor into Dify workflows via the Apify plugin's Run Actor node. Each lookup returns scored, classified, and routed as structured JSON — call-now / call-later / enrich-first / skip plus the recommendedAction.actionId enum your downstream node branches on. Competitors pointed at the same data return raw phone strings; this returns decisions.
- Actor ID:
ryanclinton/phone-number-finder - Sample input (call-decision routing for a sales prospect list):
{"persons": [{ "name": "Marcus Webb", "email": "marcus.webb@deltaventures.io", "domain": "deltaventures.io", "industry": "software" },{ "name": "Sarah Chen", "company": "Pinnacle Industries", "domain": "pinnacleind.com", "industry": "manufacturing" }],"mode": "balanced","persona": "outbound-sdr","goal": "quick-wins","scorecardTemplate": "b2b-saas-cold-call","outputProfile": "standard","enableEconomics": true}
- Branching example — wire a Dify if/else node on the v2.1 SDR primitives:
recordType = "phone-lookup" AND contactRiskGate.shouldBlockOutreach = true→ halt automation; route to human review queue (compliance gate)recordType = "phone-lookup" AND slaTier.tier = "P1"→ top-priority dial queue: AE-owned, 1-hour SLArecordType = "phone-lookup" AND slaTier.tier = "P2" AND automationTriggers.sendToDialer = true→ standard SDR dial queue, prefilled withphoneRanking[0].e164recordType = "phone-lookup" AND humanTimeValue.tier = "high" AND callOutcomePrediction.likelyOutcome = "connect"→ high-value mobile dial — skip the queue, dial nowrecordType = "phone-lookup" AND callOutcomePrediction.likelyOutcome = "voicemail" AND automationTriggers.sendToSms = true→ bypass voicemail; SMS insteadrecordType = "phone-lookup" AND humanTimeValue.tier = "avoid"→ archive; not worth a human touchpointrecordType = "phone-lookup" AND decision = "enrich-first"→ fan out acrossrecoveryPlan.recommendedFlow[]in order (e.g.email-pattern-finder→bulk-email-verifier→ re-runphone-number-finder), then re-evaluaterecordType = "phone-lookup" AND dataIntegrity.status = "conflicted"→ halt automation; surface to human reviewer withdataIntegrity.issues[]recordType = "phone-lookup" AND channelStrategy.primary = "email"→ skip phone, route to your email cadence using the verified emailrecordType = "phone-lookup" AND decision = "skip"→ archive; identifiers are too sparse to act onrecordType = "summary"→ readheadline+oneLine+decisionCountsfor the run-level Slack/email digest
- Opt-in modes Dify workflows can leverage:
watchlistName(cross-run state) — every record from run 2 onwards carriestemporalSignalswithtrend(rising/falling/stable/unchanged/new) andreengage: trueflag for contacts whose decision flipped fromskip/enrich-firsttocall-now/call-latersince the last run. Branch onreengage = trueto surface re-engaged leads in scheduled monitoring loops.enableEconomics: true— addsexpectedValue.expectedRoiandactionDecision.type(act/delay/ignore) per record. ROI-aware automation: branch onactionDecision.type = "act"for production gating.constraints: { budgetUsd, maxOutreachPerRun }— greedy ROI-first allocator selects records under all caps; each record getsallocationDecision.selected: true/falseso Dify can route only the selected leads downstream.outputProfile: "llm"— strips heavy diagnostic blocks; ideal when the next Dify node is an LLM that needsdecision,summary,recommendedActiononly.
- Action arrays usable verbatim — no LLM rewriting required:
actionPlan[](multi-step downstream sequence withtargetActorSlug+estimatedCostUsdper step),improvementSuggestions[](top-3 score-lift suggestions with sibling-actor pointers),dataGaps[].suggestedFix(sibling actor slug to chain when a field is missing),cards[](UI-ready{title, severity, action}for Slack/dashboard rendering), and the universaltask[]schema ({id, kind, target, payload, owner, deadline, dryRun: true, shouldAct}) — wire-compatible with Jira / Linear / GitHub Issues without parsing prose.
Combine with other Apify actors
| Actor | How to combine |
|---|---|
| Email Pattern Finder | Generate likely email addresses from company domains first, then pass those emails into Phone Number Finder for higher PDL match rates |
| Website Contact Scraper | Scrape all contact info (emails, phones, socials) from a business website in one pass — use instead of Phone Number Finder when you need emails too |
| Google Maps Email Extractor | Extract local business leads from Google Maps, then enrich with phones using this actor for a complete call-ready prospect list |
| Waterfall Contact Enrichment | 10-step enrichment cascade for contacts where you need emails, phones, and LinkedIn — use when you need full profiles rather than phones alone |
| B2B Lead Gen Suite | Full pipeline from URL list to scored leads — Phone Number Finder slots in as the phone enrichment step post-qualification |
| Bulk Email Verifier | Verify the work emails returned by includePersonDetails: true before sending outreach |
| HubSpot Lead Pusher | Push enriched phone records directly into HubSpot contacts after a Phone Number Finder run completes |
| Lead Enrichment Pipeline | All-in-one Clay alternative: email discovery, verification, company research, and scoring in one run ($0.12/lead) |
| AI Outreach Personalizer | Generate personalized cold emails using your own OpenAI/Anthropic key — zero AI markup ($0.01/lead) |
| Intent Signal Tracker | Track buying signals: hiring, tech changes, funding, content updates. Prioritize outreach by intent score ($0.05/company) |
| Lead Data Quality Auditor | Audit lead data quality before outreach — email verification, phone validation, domain freshness ($0.005/record) |
Limitations
- PDL coverage is strongest for US and English-speaking markets. Phone number coverage for contacts in continental Europe, Asia, and Latin America is lower. Website scraping partially compensates but company sites in non-English languages may structure contact pages differently.
- PDL mobile numbers are not available for all profiles. PDL's
mobile_phonefield is populated for a subset of their database. Many matches will return a direct dial or company number but no mobile. Confidence will bemediumrather thanhighfor these records. - Website scraping returns company phones, not personal numbers. Numbers found via the website fallback are general company lines, not the individual's direct dial. Use these for initial company contact, not personal outreach.
- PDL match requires at least one strong identifier. Name alone (without domain, company, or email) will be rejected before the PDL query is sent. Incomplete person objects are skipped with a warning in the logs.
- No JavaScript rendering on website scraping. The website fallback uses plain HTTP fetch. Sites that load phone numbers via JavaScript after page load will not have those numbers captured. The Website Contact Scraper Pro handles JS-rendered sites.
- 10-second timeout per web page. Very slow websites may time out without returning phone data. The actor continues to the next person rather than hanging the run.
- Built-in PDL key has shared rate limits. For batches over 200 persons, bring your own PDL API key to avoid hitting shared rate limits mid-run.
- Phone numbers are not verified for active status. Numbers are returned as-found in PDL or on the website. Use a separate verification step before SMS campaigns where deliverability is critical.
Integrations
- Zapier — trigger a Phone Number Finder run when a new lead is added to a Google Sheet or CRM, then write the phone number back automatically
- Make — build multi-step scenarios: enrich a lead list, filter by
confidence: "high", then push mobile numbers to an SMS platform - Google Sheets — export completed phone datasets directly to Sheets for team review or further processing
- Apify API — call the actor programmatically from any language; pass dynamic person lists from your CRM or data pipeline
- Webhooks — fire a POST to your HubSpot or Salesforce endpoint the moment a run completes, with the full dataset payload
- LangChain / LlamaIndex — use the Apify Loader to feed phone-enriched contact records into an AI sales assistant or lead qualification agent
Troubleshooting
- Records coming back as
not_foundfor known contacts — This usually means PDL does not have a record for that individual, or the identifiers provided are too sparse for a confident match. Add more identifiers (especially email) and check that company names are spelled correctly. EnablescrapeWebsites: trueas a fallback. - PDL auth error in the logs — If you supplied a custom
pdlApiKeyand see a 401 or 403 error, the key is invalid or expired. RemovepdlApiKeyfrom the input to use the built-in key, or regenerate your key at the PDL dashboard. - Website scraping finding no numbers — Some company sites load phone numbers via client-side JavaScript rather than static HTML. The HTTP-based scraper cannot execute JavaScript. For these sites, consider the Website Contact Scraper Pro which uses a full browser.
- Run stopped early with
spendingLimitReached: truein summary — Your per-run spending cap was reached. Increase the limit in Run options, or split your list into smaller batches across multiple runs. - Slow run times for large batches — The 200ms inter-person delay and website scraping (up to six pages per domain when PDL misses) add up for large lists. Disable
scrapeWebsitesfor the fastest possible run when PDL coverage is sufficient for your use case.
Responsible use
- This actor only accesses publicly available contact information and the People Data Labs database, which aggregates data from public sources.
- Respect website terms of service and
robots.txtdirectives when website scraping is enabled. - Comply with GDPR, CCPA, CAN-SPAM, TCPA, and other applicable data protection laws when using phone numbers for outreach, especially for SMS and cold calling.
- Do not use extracted phone numbers for spam, robocalling, harassment, or unauthorized commercial contact.
- For guidance on web scraping legality, see Apify's guide.
FAQ
How many phone numbers can I look up in one run?
The default cap is 100 persons per run, configurable up to 1,000 via the maxPersons parameter. For larger batches, either increase maxPersons or split across multiple runs. There is no hard platform limit beyond what your spending cap allows.
How accurate are the phone numbers returned by Phone Number Finder?
PDL-sourced numbers (confidence high or medium) are drawn from their aggregated public database and are generally accurate for currently employed professionals. Mobile numbers from PDL have the highest individual match accuracy. Website-scraped numbers (confidence low) are correct as of the scrape date but are company lines rather than personal numbers.
Does Phone Number Finder return personal mobile numbers or just company switchboards?
Both. PDL's mobile_phone field is a personal mobile number matched to the individual. When PDL returns only a direct dial or the website fallback is used, you will get a company or direct office line rather than a personal mobile.
What identifiers does each person entry need?
Each person needs at least one of: email alone, name + domain, or name + company. Email is the strongest single identifier. Name alone is not accepted because it is too ambiguous for PDL to return a confident match.
How is Phone Number Finder different from Clay or Lusha? Clay charges $0.80–$7.50 per enrichment credit depending on your plan tier. Lusha charges $1.00+ per contact export. Both require monthly subscriptions. Phone Number Finder charges $0.10 per successful lookup with no subscription — you pay only when a number is found. The same PDL database underpins the lookup in both cases.
Can I bring my own People Data Labs API key?
Yes. Supply your PDL key in the pdlApiKey field. This is useful for teams with an existing PDL subscription who want higher rate limits or to use their own PDL credit allocation. If omitted, the built-in key is used at no extra charge.
What happens when PDL runs out of credits mid-run?
The actor detects the 402 response from PDL and disables PDL lookups for the rest of the run. Website scraping continues for remaining records, so you still get company phone numbers for contacts whose domains are present. The summary record includes a pdlOutOfCredits: true flag.
Is it legal to scrape phone numbers from websites? Scraping publicly available phone numbers from business websites is generally lawful in most jurisdictions. However, using those numbers for unsolicited commercial contact may be regulated by TCPA (US), PECR (UK), or GDPR (EU). Always ensure your outreach complies with applicable telemarketing and data protection laws. See Apify's scraping legality guide for more detail.
Can I schedule Phone Number Finder to run automatically? Yes. Use the Apify Scheduler to trigger runs on a daily, weekly, or custom cron schedule. This is useful for refreshing a prospect database monthly or monitoring new contacts added to a CRM.
How long does a typical run take?
A batch of 100 persons takes approximately 3–5 minutes with website scraping enabled. With scrapeWebsites: false, the same batch runs in under 2 minutes. Time scales roughly linearly with batch size and the proportion of contacts that require website fallback.
What does the confidence field mean, and how should I use it?
high means PDL returned a personal mobile number — the strongest match. medium means PDL returned a non-mobile number (direct dial or office line). low means the number came from website scraping only, with no individual-to-number match. For outreach prioritisation, route high confidence records to personalised SMS or direct calling sequences, and low confidence records to general company contact flows.
Can I use this actor with Make or Zapier? Yes. Both integrations are supported natively through the Apify platform. In Make, use the Apify "Run Actor" module to trigger a run with your person list and the "Watch Dataset Items" module to process results. In Zapier, the Apify integration lets you trigger runs and retrieve dataset items as part of multi-step zaps.
Help us improve
If you encounter issues, you can help us debug faster by enabling run sharing in your Apify account:
- Go to Account Settings > Privacy
- Enable Share runs with public Actor creators
This lets us see your run details when something goes wrong, so we can fix issues faster. Your data is only visible to the actor developer, not publicly.
Support
Found a bug or have a feature request? Open an issue in the Issues tab on this actor's page. For custom solutions or enterprise integrations, reach out through the Apify platform.