# Phone Number Finder — Direct Dials from Websites (`ryanclinton/phone-number-finder`) Actor

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.

- **URL**: https://apify.com/ryanclinton/phone-number-finder.md
- **Developed by:** [Ryan Clinton](https://apify.com/ryanclinton) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 50 total users, 25 monthly users, 97.8% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $100.00 / 1,000 phone founds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## Phone Number Finder

**Decides if a sales rep should call a person right now — and finds the number to do it for $0.10.**

> **This actor converts raw signals into deterministic, automation-ready call decisions.**

**Apify GTM Pipeline:** Scrape → Enrich → Verify → Score → Research → Push to CRM
**Role of this actor:** Phone discovery + call-decision layer.

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 — typically a meaningful discount versus subscription / per-seat phone-data platforms.** 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 plus a deterministic, reproducible reason path.

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.

### 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 (`decision` enum)
- 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`)

No LLM, no learned weights, no surprise scoring — every output field is a pure function of the inputs and reproducible across runs.

> **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 | ✅ | ❌ | ❌ | ❌ | per-credit, subscription (verify current plans) |
| ZoomInfo | ✅ | ❌ | ❌ | ❌ | per-seat subscription (verify current plans) |
| Lusha | ✅ | ❌ | ❌ | ❌ | per-contact, subscription (verify current plans) |
| Apollo | ✅ | partial | ❌ | partial | per-seat subscription (verify current plans) |
| **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) typically priced at >$1/lookup or per-seat subscription (verify each vendor's current plans)
- 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 with no subscription, typically a meaningful discount versus the per-credit / per-seat pricing of the legacy alternatives (verify each vendor's current published plans).

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](https://apify.com/ryanclinton/waterfall-contact-enrichment)
- email-only workflows — use [Email Pattern Finder](https://apify.com/ryanclinton/email-pattern-finder)
- JS-rendered website contact scraping — use [Website Contact Scraper Pro](https://apify.com/ryanclinton/website-contact-scraper-pro)
- CRM push from existing enriched data — use [HubSpot Lead Pusher](https://apify.com/ryanclinton/hubspot-lead-pusher) or [Salesforce Lead Pusher](https://apify.com/ryanclinton/salesforce-lead-pusher)

### Core outputs

Every record returns:

- **`decision`** — `call-now` / `call-later` / `enrich-first` / `skip`
- **`reachability`** — 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 number
- **`nextBestActions`** — conditional `if/then` tree for runtime cadence branching
- **`contactabilityScore`** — composite 0-100 flagship metric
- **`recoveryPlan.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:

1. `slaTier.tier` ascending (P1 first)
2. `humanTimeValue.tier` (high → medium → low → avoid)
3. `reachability.score` descending

For a cost-aware allocation pass (with `enableEconomics: true`), sort by:

1. `actionDecision.type = "act"` first
2. `priorityScoreRoi` descending
3. `contactabilityScore.score` descending

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=6` to filter weak matches; returns `mobile_phone` and `phone_numbers` array 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 (`\D` stripped) 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`, and `companyPhone` for downstream routing (e.g., send SMS only to mobile numbers)
- **Pay-per-event pricing** — the `Actor.charge()` call fires only when `phoneSource !== '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 × 1000ms` delay; 429 rate limits trigger `2^attempt × 2000ms` backoff
- **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
- **Your own PDL key** — supply your own People Data Labs API key (required). A free PDL trial gives 500 lookups with real phone numbers for 30 days; your credits, your control
- **Optional person detail enrichment** — set `includePersonDetails: true` to also return `fullName`, `jobTitle`, and `workEmail` from 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

1. **Get a free PDL key** — phone data comes from People Data Labs, so the actor runs on your own PDL key. Sign up at [peopledatalabs.com/signup](https://peopledatalabs.com/signup) and click **Start Trial** in the API Dashboard: the trial gives 500 lookups with real phone numbers for 30 days, no card required. Paste the key into the **People Data Labs API Key** input.
2. **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 work `email`, or `name` + `company`. More identifiers improve match accuracy.
3. **Configure options** — leave `scrapeWebsites` enabled (it is on by default) for maximum coverage. Enable `includePersonDetails` if you also want job title and work email returned from PDL.
4. **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.
5. **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 | **Yes** | — | Your People Data Labs API key (phone data source). Get a free key at [peopledatalabs.com/signup](https://peopledatalabs.com/signup) and start a trial: 500 lookups with real numbers for 30 days, no card. After the trial, PDL's free plan hides contact fields, so continued phone data needs a PDL Pro plan. |
| `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|equals|matches, penalty, reason?}]`. Total penalty per record capped at 50 points (out of 100). Invalid regex is skipped silently. |
| `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:**
```json
{
  "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:**

```json
{
  "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:**

```json
{
  "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`, and `domain` together 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 `persons` array 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`.

```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:

```json
{
  "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|medium|low|very-low), components[], coldStart }` |
| `confidenceLevel` | string | Backward-compat alias for `confidence.level` |

#### Contactability Intelligence layer

| Field | Type | Description |
|---|---|---|
| `contactabilityScore` | object | `{ score (0-100), level (high|medium|low|unreachable), components{phonePresence,sourceQuality,validationQuality,identityStrength,freshness,decisionAlignment} }` — the flagship metric. Sort, filter, SLA-gate on it. |
| `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|email|enrichment|archive), secondary, reason }` — derived routing decision: which channel to attempt first and what to fall back to. |
| `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|medium|low|unknown), score (0-1), factors[], riskFlags[] }` — answers "will this number actually CONNECT to a real human?". Distinct from `contactabilityScore`, which bundles identity + freshness + decision alignment. |
| `callOutcomePrediction` | object | `{ likelyOutcome (connect|gatekeeper|voicemail|invalid|unknown), confidence (0-1), drivers[] }` — deterministic heuristic. No LLM. |
| `humanTimeValue` | object | `{ score (0-100), tier (high|medium|low|avoid), expectedOutcome, reason }` — the SDR-friendly view: "is this worth my next call?" Distinct from `priorityScoreRoi` (the ops view). Reps filter on `tier`; managers filter on `expectedOutcome`. |
| `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|minor-issue|conflicted), issues[], severity (none|low|medium|high) }` — first-class status surfacing the contradictions block. Halt automation on `conflicted`. |
| `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|medium|low|none) }` — Zapier / Make / n8n boolean set. No prose parsing required. |
| `slaTier` | object | `{ tier (P1|P2|P3|P4), description, reason }` — sales-manager-friendly priority enum. **P1** = call within 1 hour, top-priority. **P4** = no SLA, archive. |

#### 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`, `callableCount`
- `notifications[]` — UI-surfaceable run-level alerts
- `icpInsights` — passive top-performer insights from this run's cohort (when ≥5 records score ≥0.75)
- `cohortInsights` — overfit / diversity / dedup observations
- `accountReadiness[]` — per-company rollup (when `enableAccountRollup: true`)
- `calibration` — score-band priors + grade (A–F based on cohort size + outcome alignment)
- `outcomeValidation` — when `outcomeDatasetId` is supplied: win-rate by decision + predictive flag
- `totalExpectedRevenueUsd`, `totalCostToActUsd`, `portfolioRoi` (when economics is enabled)
- `allocation` — full allocation summary (when `constraints` set)
- `simulationSummary` — what-if rollup (when `simulate` set)
- `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 subscription / per-credit phone-data platforms (Clay, Lusha, ZoomInfo) which are typically priced significantly higher per lookup or per seat — verify each vendor's current published plans for specifics. 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

```python
from apify_client import ApifyClient

client = 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

```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

```bash
## Start the actor run
curl -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

1. **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.
2. **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.
3. **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.
4. **Use `includePersonDetails: true` for 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.
5. **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.
6. **Combine with email finding first.** If you only have company domains, run [Email Pattern Finder](https://apify.com/ryanclinton/email-pattern-finder) first to generate likely email addresses, then pass those emails into this actor for maximum PDL match rates.
7. **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.
8. **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](https://docs.apify.com/platform/integrations/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):

```json
{
  "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 SLA
  - `recordType = "phone-lookup" AND slaTier.tier = "P2" AND automationTriggers.sendToDialer = true` → standard SDR dial queue, prefilled with `phoneRanking[0].e164`
  - `recordType = "phone-lookup" AND humanTimeValue.tier = "high" AND callOutcomePrediction.likelyOutcome = "connect"` → high-value mobile dial — skip the queue, dial now
  - `recordType = "phone-lookup" AND callOutcomePrediction.likelyOutcome = "voicemail" AND automationTriggers.sendToSms = true` → bypass voicemail; SMS instead
  - `recordType = "phone-lookup" AND humanTimeValue.tier = "avoid"` → archive; not worth a human touchpoint
  - `recordType = "phone-lookup" AND decision = "enrich-first"` → fan out across `recoveryPlan.recommendedFlow[]` in order (e.g. `email-pattern-finder` → `bulk-email-verifier` → re-run `phone-number-finder`), then re-evaluate
  - `recordType = "phone-lookup" AND dataIntegrity.status = "conflicted"` → halt automation; surface to human reviewer with `dataIntegrity.issues[]`
  - `recordType = "phone-lookup" AND channelStrategy.primary = "email"` → skip phone, route to your email cadence using the verified email
  - `recordType = "phone-lookup" AND decision = "skip"` → archive; identifiers are too sparse to act on
  - `recordType = "summary"` → read `headline` + `oneLine` + `decisionCounts` for the run-level Slack/email digest
- **Opt-in modes Dify workflows can leverage:**
  - `watchlistName` (cross-run state) — every record from run 2 onwards carries `temporalSignals` with `trend` (`rising` / `falling` / `stable` / `unchanged` / `new`) and `reengage: true` flag for contacts whose decision flipped from `skip` / `enrich-first` to `call-now` / `call-later` since the last run. Branch on `reengage = true` to surface re-engaged leads in scheduled monitoring loops.
  - `enableEconomics: true` — adds `expectedValue.expectedRoi` and `actionDecision.type` (`act` / `delay` / `ignore`) per record. ROI-aware automation: branch on `actionDecision.type = "act"` for production gating.
  - `constraints: { budgetUsd, maxOutreachPerRun }` — greedy ROI-first allocator selects records under all caps; each record gets `allocationDecision.selected: true/false` so Dify can route only the selected leads downstream.
  - `outputProfile: "llm"` — strips heavy diagnostic blocks; ideal when the next Dify node is an LLM that needs `decision`, `summary`, `recommendedAction` only.
- **Action arrays usable verbatim — no LLM rewriting required:** `actionPlan[]` (multi-step downstream sequence with `targetActorSlug` + `estimatedCostUsd` per 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 universal `task[]` 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](https://apify.com/ryanclinton/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](https://apify.com/ryanclinton/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](https://apify.com/ryanclinton/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](https://apify.com/ryanclinton/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](https://apify.com/ryanclinton/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](https://apify.com/ryanclinton/bulk-email-verifier) | Verify the work emails returned by `includePersonDetails: true` before sending outreach |
| [HubSpot Lead Pusher](https://apify.com/ryanclinton/hubspot-lead-pusher) | Push enriched phone records directly into HubSpot contacts after a Phone Number Finder run completes |
| [Lead Enrichment Pipeline](https://apify.com/ryanclinton/lead-enrichment-pipeline) | All-in-one Clay alternative: email discovery, verification, company research, and scoring in one run ($0.12/lead) |
| [AI Outreach Personalizer](https://apify.com/ryanclinton/ai-outreach-personalizer) | Generate personalized cold emails using your own OpenAI/Anthropic key — zero AI markup ($0.01/lead) |
| [Intent Signal Tracker](https://apify.com/ryanclinton/intent-signal-tracker) | Track buying signals: hiring, tech changes, funding, content updates. Prioritize outreach by intent score ($0.05/company) |
| [Lead Data Quality Auditor](https://apify.com/ryanclinton/enrichment-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_phone` field is populated for a subset of their database. Many matches will return a direct dial or company number but no mobile. Confidence will be `medium` rather than `high` for 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](https://apify.com/ryanclinton/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.
- **PDL free plan hides phone numbers after the trial.** A new PDL trial returns real numbers (500 lookups / 30 days). Once the trial lapses, PDL's ongoing free plan obfuscates contact fields — the API returns `true`/`false` instead of the number — so the actor flags this and you need a PDL Pro plan for continued phone data.
- **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](https://apify.com/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](https://apify.com/integrations/make) — build multi-step scenarios: enrich a lead list, filter by `confidence: "high"`, then push mobile numbers to an SMS platform
- [Google Sheets](https://apify.com/integrations/google-sheets) — export completed phone datasets directly to Sheets for team review or further processing
- [Apify API](https://docs.apify.com/api/v2) — call the actor programmatically from any language; pass dynamic person lists from your CRM or data pipeline
- [Webhooks](https://docs.apify.com/platform/integrations/webhooks) — fire a POST to your HubSpot or Salesforce endpoint the moment a run completes, with the full dataset payload
- [LangChain / LlamaIndex](https://docs.apify.com/platform/integrations) — 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_found` for 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. Enable `scrapeWebsites: true` as a fallback.
- **`missing_pdl_key` error / "add your People Data Labs API key to run"** — No `pdlApiKey` was provided. The actor needs your own PDL key. Create a free one at [peopledatalabs.com/signup](https://peopledatalabs.com/signup), start a trial (500 lookups, real numbers, 30 days, no card), and paste it into the **People Data Labs API Key** input.
- **PDL auth error in the logs (401/403)** — Your `pdlApiKey` is invalid or expired. Regenerate the key in your PDL dashboard and paste the new value into the input.
- **Phones coming back as `true`/`false` warnings / "your PDL key is on the free plan"** — Your PDL trial has lapsed and the key dropped to the free plan, which hides contact fields. Start a new trial or upgrade to PDL Pro to get real numbers again.
- **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](https://apify.com/ryanclinton/website-contact-scraper-pro) which uses a full browser.
- **Run stopped early with `spendingLimitReached: true` in 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 `scrapeWebsites` for 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.txt` directives 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](https://blog.apify.com/is-web-scraping-legal/).

### 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 and Lusha are typically subscription-priced with per-credit / per-contact charges on top — verify each vendor's current published plans for specifics. 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.

**Do I need my own People Data Labs API key?**
Yes. Phone numbers come from People Data Labs, so the actor runs on your own PDL key — supply it in the `pdlApiKey` field. Getting one is free: sign up at [peopledatalabs.com/signup](https://peopledatalabs.com/signup) and click Start Trial in the API Dashboard. The trial gives 500 lookups with real phone numbers for 30 days, no card required. After the trial, PDL's ongoing free plan hides contact fields (it returns `true`/`false` instead of the number), so for continued phone data you would move to a PDL Pro plan. Using your own key also means your PDL credits, your rate limits, and full control over your spend.

**What happens when my PDL key runs out of credits mid-run?**
The actor detects the 402 response from PDL and disables PDL lookups for the rest of the run rather than erroring. 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 and a notification pointing you to top up or renew your PDL key.

**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](https://blog.apify.com/is-web-scraping-legal/) 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:

1. Go to [Account Settings > Privacy](https://console.apify.com/account/privacy)
2. 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](https://console.apify.com/actors/ryanclinton~phone-number-finder/issues) on this actor's page. For custom solutions or enterprise integrations, reach out through the Apify platform.

# Actor input Schema

## `persons` (type: `array`):

List of people to find phone numbers for. Each entry must include at least: name + domain, email, or name + company. Optional fields (industry, companySize, title, lastVerifiedAt, hubspotId, etc.) flow into the decision/economics layer.

## `pdlApiKey` (type: `string`):

Required. Phone numbers come from People Data Labs, so you supply your own PDL key. Create a free key at https://peopledatalabs.com/signup and click Start Trial in the API Dashboard: the trial gives 500 lookups with real phone numbers for 30 days, no card required. Paste the key here. Note: after the trial, PDL's ongoing free plan hides contact fields (returns true/false instead of the actual number), so for continued phone data you need a PDL Pro plan.

## `maxPersons` (type: `integer`):

Maximum number of persons to process. Default: 100. Max: 1000.

## `scrapeWebsites` (type: `boolean`):

When enabled, scrapes the company website (homepage, /contact, /about, /team) for phone numbers as a fallback when PDL finds nothing.

## `includePersonDetails` (type: `boolean`):

When enabled, also returns the person's full name, job title, and work email from PDL alongside the phone numbers.

## `mode` (type: `string`):

fast = PDL only, no website fallback. balanced (default) = PDL + website fallback. thorough = PDL + website + extended retries. auto = picks based on batch size.

## `persona` (type: `string`):

Adjusts scoring weights to favour what each role cares about. outbound-sdr prioritises mobile dials; account-exec prioritises high-confidence direct dials; growth-marketer prioritises volume.

## `goal` (type: `string`):

What outcome are you optimising for? pipeline-growth = volume; quick-wins = highest-confidence calls only; cost-efficiency = penalise low-source-quality numbers; high-ltv = bias toward enterprise dealSize.

## `scoringWeights` (type: `object`):

Per-dimension weight overrides. Sum is renormalised. Keys: pdlMatch, phoneCount, sourceQuality, identifierStrength.

## `outputProfile` (type: `string`):

Strip optional fields from the dataset records. minimal = phones + decision only; standard = full minus deep diagnostics; full (default) = all blocks; llm = compact summary for AI consumers.

## `scorecardTemplate` (type: `string`):

Pre-built bundles: b2b-saas-cold-call (mobile-first, personal-email penalties), enterprise-account-research (AE ownership, longer SLA), local-business-leads (website-friendly, lower cost-per-touch), recruiter-sourcing (mobile-only, HR-alias penalty).

## `negativeRules` (type: `array`):

User-configurable penalties. Each rule: {field, contains|equals|matches, penalty, reason?}. Total penalty per record capped at 50 points (out of 100).

## `freshnessConfig` (type: `object`):

Penalise stale records: {dateField?, decayAfterDays? (default 90), maxPenalty? (default 25)}. Auto-detects lastVerifiedAt / verifiedAt / updatedAt / scrapedAt / fetchedAt / lastSeenAt / lastUpdated.

## `circuitBreakerThreshold` (type: `integer`):

Stop the run cleanly after N consecutive no-phone records. Default: 5. Minimum 1, maximum 100.

## `watchlistName` (type: `string`):

Optional name for cross-run state. Stored at phone-number-finder-history-{watchlistName}. Leave blank to run stateless.

## `monitorStateKey` (type: `string`):

Suite-aligned alias for watchlistName. Either input works; if both are set, watchlistName wins. Lets the same upstream orchestrator pass one consistent field name across phone-number-finder, waterfall-contact-enrichment, bulk-email-verifier, company-deep-research, and lead-enrichment-pipeline.

## `lastAction` (type: `object`):

Optional. Tells 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 — direct call-connect / voicemail / conversation outcomes are not. Shape: { type: 'called' | 'left-voicemail' | 'sent-text' | string, takenAt: ISO date, note?: string }. Requires watchlistName / monitorStateKey to load history.

## `referenceRunId` (type: `string`):

When set, switches the actor into diff-me-against-this-prior-run mode and emits changeFlags per record.

## `enableDedup` (type: `boolean`):

Annotate records when multiple persons share a canonical domain. Records are FLAGGED, not dropped.

## `enableAccountRollup` (type: `boolean`):

Group records by company domain and emit accountReadiness\[] in the summary record (sales-ready / developing / cold / unknown plus coverage classification).

## `enableEconomics` (type: `boolean`):

Compute expectedRevenue / costToAct / ROI per record and a portfolioRoi rollup in the summary. Off by default — dealSize proxies are domain-specific.

## `industryDealSizeOverrides` (type: `object`):

Override the proxy table. Keys are lowercase industry tokens (e.g. "software"), values are USD numbers. User overrides win over the proxy.

## `sdrCostPerTouch` (type: `number`):

Used in costToAct computation when economics is enabled. Default $1.50 per touchpoint.

## `enrichmentCostPerLead` (type: `number`):

Used in costToAct when a record decision requires enrichment first. Default $0.50.

## `constraints` (type: `object`):

Run-level caps: {maxOutreachPerRun?, maxEnrichmentPerRun?, budgetUsd?}. Greedy ROI-first allocator selects records under all caps; selection result attaches to each record's allocationDecision block.

## `simulate` (type: `object`):

Re-score every record under override weights and emit a simulation block per record + simulationSummary in the run summary. Pure compute — does not double-charge.

## `enableSavingsReport` (type: `boolean`):

Emit a savings block in the summary tallying enrichment cost / SDR touches / spend avoided by allocation rules. Auto-emits when constraints are set.

## `outcomeDatasetId` (type: `string`):

Apify dataset ID with prior outcome records. Must include the joinKey (default 'domain') plus the won field (default 'won').

## `outcomeJoinKey` (type: `string`):

Field name to join outcomes against — defaults to 'domain'.

## `outcomeFields` (type: `object`):

Map your outcome dataset's column names: {won?: 'isWon', revenue?: 'dealValue'}. Defaults: {won: 'won'}.

## `webhookUrl` (type: `string`):

Slack / Discord / generic webhook. Posts a rich embed with run totals + calibration grade when the run completes.

## Actor input object example

```json
{
  "persons": [
    {
      "name": "John Smith",
      "email": "john.smith@acme.com",
      "company": "Acme Corp",
      "domain": "acme.com"
    }
  ],
  "maxPersons": 100,
  "scrapeWebsites": true,
  "includePersonDetails": false,
  "mode": "balanced",
  "persona": "generic",
  "goal": "generic",
  "outputProfile": "full",
  "scorecardTemplate": "custom",
  "negativeRules": [],
  "circuitBreakerThreshold": 5,
  "enableDedup": false,
  "enableAccountRollup": false,
  "enableEconomics": false,
  "sdrCostPerTouch": 1.5,
  "enrichmentCostPerLead": 0.5,
  "enableSavingsReport": false,
  "outcomeJoinKey": "domain"
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "persons": [
        {
            "name": "John Smith",
            "email": "john.smith@acme.com",
            "company": "Acme Corp",
            "domain": "acme.com"
        }
    ],
    "pdlApiKey": "",
    "maxPersons": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("ryanclinton/phone-number-finder").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "persons": [{
            "name": "John Smith",
            "email": "john.smith@acme.com",
            "company": "Acme Corp",
            "domain": "acme.com",
        }],
    "pdlApiKey": "",
    "maxPersons": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("ryanclinton/phone-number-finder").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "persons": [
    {
      "name": "John Smith",
      "email": "john.smith@acme.com",
      "company": "Acme Corp",
      "domain": "acme.com"
    }
  ],
  "pdlApiKey": "",
  "maxPersons": 100
}' |
apify call ryanclinton/phone-number-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=ryanclinton/phone-number-finder",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Phone Number Finder — Direct Dials from Websites",
        "description": "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.",
        "version": "2.1",
        "x-build-id": "45zqrcH2jYUwFVc8C"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/ryanclinton~phone-number-finder/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-ryanclinton-phone-number-finder",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/ryanclinton~phone-number-finder/runs": {
            "post": {
                "operationId": "runs-sync-ryanclinton-phone-number-finder",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/ryanclinton~phone-number-finder/run-sync": {
            "post": {
                "operationId": "run-sync-ryanclinton-phone-number-finder",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "persons"
                ],
                "properties": {
                    "persons": {
                        "title": "Persons to look up",
                        "type": "array",
                        "description": "List of people to find phone numbers for. Each entry must include at least: name + domain, email, or name + company. Optional fields (industry, companySize, title, lastVerifiedAt, hubspotId, etc.) flow into the decision/economics layer.",
                        "default": [
                            {
                                "name": "John Smith",
                                "email": "john.smith@acme.com",
                                "company": "Acme Corp",
                                "domain": "acme.com"
                            }
                        ]
                    },
                    "pdlApiKey": {
                        "title": "People Data Labs API Key (required)",
                        "type": "string",
                        "description": "Required. Phone numbers come from People Data Labs, so you supply your own PDL key. Create a free key at https://peopledatalabs.com/signup and click Start Trial in the API Dashboard: the trial gives 500 lookups with real phone numbers for 30 days, no card required. Paste the key here. Note: after the trial, PDL's ongoing free plan hides contact fields (returns true/false instead of the actual number), so for continued phone data you need a PDL Pro plan."
                    },
                    "maxPersons": {
                        "title": "Max persons",
                        "minimum": 1,
                        "maximum": 1000,
                        "type": "integer",
                        "description": "Maximum number of persons to process. Default: 100. Max: 1000.",
                        "default": 100
                    },
                    "scrapeWebsites": {
                        "title": "Scrape company websites",
                        "type": "boolean",
                        "description": "When enabled, scrapes the company website (homepage, /contact, /about, /team) for phone numbers as a fallback when PDL finds nothing.",
                        "default": true
                    },
                    "includePersonDetails": {
                        "title": "Include person details",
                        "type": "boolean",
                        "description": "When enabled, also returns the person's full name, job title, and work email from PDL alongside the phone numbers.",
                        "default": false
                    },
                    "mode": {
                        "title": "Mode",
                        "enum": [
                            "fast",
                            "balanced",
                            "thorough",
                            "auto"
                        ],
                        "type": "string",
                        "description": "fast = PDL only, no website fallback. balanced (default) = PDL + website fallback. thorough = PDL + website + extended retries. auto = picks based on batch size.",
                        "default": "balanced"
                    },
                    "persona": {
                        "title": "Persona",
                        "enum": [
                            "outbound-sdr",
                            "account-exec",
                            "growth-marketer",
                            "generic"
                        ],
                        "type": "string",
                        "description": "Adjusts scoring weights to favour what each role cares about. outbound-sdr prioritises mobile dials; account-exec prioritises high-confidence direct dials; growth-marketer prioritises volume.",
                        "default": "generic"
                    },
                    "goal": {
                        "title": "Goal",
                        "enum": [
                            "pipeline-growth",
                            "quick-wins",
                            "cost-efficiency",
                            "high-ltv",
                            "generic"
                        ],
                        "type": "string",
                        "description": "What outcome are you optimising for? pipeline-growth = volume; quick-wins = highest-confidence calls only; cost-efficiency = penalise low-source-quality numbers; high-ltv = bias toward enterprise dealSize.",
                        "default": "generic"
                    },
                    "scoringWeights": {
                        "title": "Scoring weights (advanced)",
                        "type": "object",
                        "description": "Per-dimension weight overrides. Sum is renormalised. Keys: pdlMatch, phoneCount, sourceQuality, identifierStrength."
                    },
                    "outputProfile": {
                        "title": "Output profile",
                        "enum": [
                            "minimal",
                            "standard",
                            "full",
                            "llm"
                        ],
                        "type": "string",
                        "description": "Strip optional fields from the dataset records. minimal = phones + decision only; standard = full minus deep diagnostics; full (default) = all blocks; llm = compact summary for AI consumers.",
                        "default": "full"
                    },
                    "scorecardTemplate": {
                        "title": "Scorecard template",
                        "enum": [
                            "b2b-saas-cold-call",
                            "enterprise-account-research",
                            "local-business-leads",
                            "recruiter-sourcing",
                            "custom"
                        ],
                        "type": "string",
                        "description": "Pre-built bundles: b2b-saas-cold-call (mobile-first, personal-email penalties), enterprise-account-research (AE ownership, longer SLA), local-business-leads (website-friendly, lower cost-per-touch), recruiter-sourcing (mobile-only, HR-alias penalty).",
                        "default": "custom"
                    },
                    "negativeRules": {
                        "title": "Negative scoring rules",
                        "type": "array",
                        "description": "User-configurable penalties. Each rule: {field, contains|equals|matches, penalty, reason?}. Total penalty per record capped at 50 points (out of 100).",
                        "default": []
                    },
                    "freshnessConfig": {
                        "title": "Freshness decay",
                        "type": "object",
                        "description": "Penalise stale records: {dateField?, decayAfterDays? (default 90), maxPenalty? (default 25)}. Auto-detects lastVerifiedAt / verifiedAt / updatedAt / scrapedAt / fetchedAt / lastSeenAt / lastUpdated."
                    },
                    "circuitBreakerThreshold": {
                        "title": "Circuit breaker threshold",
                        "minimum": 1,
                        "maximum": 100,
                        "type": "integer",
                        "description": "Stop the run cleanly after N consecutive no-phone records. Default: 5. Minimum 1, maximum 100.",
                        "default": 5
                    },
                    "watchlistName": {
                        "title": "Watchlist name",
                        "type": "string",
                        "description": "Optional name for cross-run state. Stored at phone-number-finder-history-{watchlistName}. Leave blank to run stateless."
                    },
                    "monitorStateKey": {
                        "title": "Monitor state key (alias for watchlistName)",
                        "type": "string",
                        "description": "Suite-aligned alias for watchlistName. Either input works; if both are set, watchlistName wins. Lets the same upstream orchestrator pass one consistent field name across phone-number-finder, waterfall-contact-enrichment, bulk-email-verifier, company-deep-research, and lead-enrichment-pipeline."
                    },
                    "lastAction": {
                        "title": "Last action (closes the feedback loop)",
                        "type": "object",
                        "description": "Optional. Tells 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 — direct call-connect / voicemail / conversation outcomes are not. Shape: { type: 'called' | 'left-voicemail' | 'sent-text' | string, takenAt: ISO date, note?: string }. Requires watchlistName / monitorStateKey to load history."
                    },
                    "referenceRunId": {
                        "title": "Reference run ID",
                        "type": "string",
                        "description": "When set, switches the actor into diff-me-against-this-prior-run mode and emits changeFlags per record."
                    },
                    "enableDedup": {
                        "title": "Flag duplicate domains",
                        "type": "boolean",
                        "description": "Annotate records when multiple persons share a canonical domain. Records are FLAGGED, not dropped.",
                        "default": false
                    },
                    "enableAccountRollup": {
                        "title": "Account-level rollup",
                        "type": "boolean",
                        "description": "Group records by company domain and emit accountReadiness[] in the summary record (sales-ready / developing / cold / unknown plus coverage classification).",
                        "default": false
                    },
                    "enableEconomics": {
                        "title": "Enable ROI / economics",
                        "type": "boolean",
                        "description": "Compute expectedRevenue / costToAct / ROI per record and a portfolioRoi rollup in the summary. Off by default — dealSize proxies are domain-specific.",
                        "default": false
                    },
                    "industryDealSizeOverrides": {
                        "title": "Industry deal-size overrides",
                        "type": "object",
                        "description": "Override the proxy table. Keys are lowercase industry tokens (e.g. \"software\"), values are USD numbers. User overrides win over the proxy."
                    },
                    "sdrCostPerTouch": {
                        "title": "SDR labour cost per touch (USD)",
                        "minimum": 0,
                        "type": "number",
                        "description": "Used in costToAct computation when economics is enabled. Default $1.50 per touchpoint.",
                        "default": 1.5
                    },
                    "enrichmentCostPerLead": {
                        "title": "Enrichment cost per lead (USD)",
                        "minimum": 0,
                        "type": "number",
                        "description": "Used in costToAct when a record decision requires enrichment first. Default $0.50.",
                        "default": 0.5
                    },
                    "constraints": {
                        "title": "Allocation constraints",
                        "type": "object",
                        "description": "Run-level caps: {maxOutreachPerRun?, maxEnrichmentPerRun?, budgetUsd?}. Greedy ROI-first allocator selects records under all caps; selection result attaches to each record's allocationDecision block."
                    },
                    "simulate": {
                        "title": "Simulation override weights",
                        "type": "object",
                        "description": "Re-score every record under override weights and emit a simulation block per record + simulationSummary in the run summary. Pure compute — does not double-charge."
                    },
                    "enableSavingsReport": {
                        "title": "Savings report",
                        "type": "boolean",
                        "description": "Emit a savings block in the summary tallying enrichment cost / SDR touches / spend avoided by allocation rules. Auto-emits when constraints are set.",
                        "default": false
                    },
                    "outcomeDatasetId": {
                        "title": "Outcome dataset ID",
                        "type": "string",
                        "description": "Apify dataset ID with prior outcome records. Must include the joinKey (default 'domain') plus the won field (default 'won')."
                    },
                    "outcomeJoinKey": {
                        "title": "Outcome join key",
                        "type": "string",
                        "description": "Field name to join outcomes against — defaults to 'domain'.",
                        "default": "domain"
                    },
                    "outcomeFields": {
                        "title": "Outcome field mapping",
                        "type": "object",
                        "description": "Map your outcome dataset's column names: {won?: 'isWon', revenue?: 'dealValue'}. Defaults: {won: 'won'}."
                    },
                    "webhookUrl": {
                        "title": "Notification webhook URL",
                        "type": "string",
                        "description": "Slack / Discord / generic webhook. Posts a rich embed with run totals + calibration grade when the run completes."
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
