# Pre-Dial Phone Scrub — Send, Skip & Review Verdicts (`pequod-labs/outbound-phone-scrub`) Actor

Bulk pre-dial phone scrub for outbound call/SMS: E.164 normalize, Twilio line type, UK TPS/CTPS, optional HLR. Each number gets send/skip/review with reason codes, caveats, and audit — not a format-only validator.

- **URL**: https://apify.com/pequod-labs/outbound-phone-scrub.md
- **Developed by:** [Pequod](https://apify.com/pequod-labs) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 4 total users, 2 monthly users, 94.7% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $24.00 / 1,000 phone scrubbeds

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## Pre-Dial Phone Scrub — Send, Skip & Review Verdicts

**Apify slug:** `pequod-labs/outbound-phone-scrub`

Bulk **pre-dial phone scrub** for outbound call and SMS workflows. Paste a list of phone numbers and get a **send**, **skip**, or **review** verdict per row with **reason codes**, **line type**, optional **UK TPS/CTPS** register hits, optional **HLR reachability**, and a step-by-step **audit trail**. This is operational guidance for dialers and sequencers — not a format-only validator and not a consumer reverse-phone lookup.

| | |
|---|---|
| **Input** | Array of phone strings (`phones`), optional `channel`, enrichment `packs`, `defaultRegion` |
| **Output** | E.164, line type, register hits, reachability, `verdict`, `reason_codes`, `caveats`, `audit` |
| **Best for** | Pre-dial scrub before cold call/SMS; UK TPS/CTPS list checks; HLR dead-number filtering |
| **Not for** | Reverse phone lookup; GDPR/TCPA compliance guarantees; invented EU do-not-call certainty |

### Use this actor when

- You have a **bulk phone list** from CRM export, decision-maker enrichment, or lead gen and need a **pre-dial gate** before your dialer or SMS tool fires
- You want **send / skip / review verdicts** with machine-readable **reason codes** and human-readable **caveats**, not just valid/invalid formatting
- You need **Twilio line type intelligence** (mobile, landline, VoIP, premium) to route SMS vs voice or deprioritize risky types
- You call or text **UK numbers** and must check **TPS/CTPS** before outbound contact
- You want optional **HLR reachability** to flag dead or absent numbers before spend on connect attempts
- You need an **audit trail** (`audit`, `providers_used`, `scrubbed_at`) for ops review and downstream automation (n8n, webhooks, MCP)
- You are building a **phone number validation API** workflow on Apify without wiring Twilio, tpsapi, and HLR vendors yourself

### Do not use this actor when

- You need **who owns this number** or carrier billing identity — this is not reverse-phone lookup
- You expect **GDPR, TCPA, or ePrivacy compliance guarantees** — output is operational guidance with explicit coverage limits
- You want a **cheap format-only validator** that never calls upstream APIs — libphonenumber normalization alone is not the product
- You need **invented EU national DNC register hits** for consent-regime markets (FR, DE, NL, etc.) — those rows get `consent_or_local_rules` caveats instead
- Your workflow is **inbound-only** with no outbound dial/SMS intent
- You want WhatsApp, social profile, or email enrichment from a phone number

### How it works

1. **Normalize** — Each input string is parsed with Google's **libphonenumber** (`phonenumbers`). Invalid numbers get `skip` with format reasons; valid numbers become **E.164**.
2. **Line type (default on)** — When Twilio credentials are configured, **Twilio Lookup v2** `line_type_intelligence` enriches mobile/landline/VoIP/premium and carrier metadata.
3. **UK registers (default on)** — For GB/GG/JE/IM numbers, **TPS/CTPS** is checked via [tpsapi.com](https://tpsapi.com/) when `TPSAPI_API_KEY` is set. Listed numbers typically get `skip`.
4. **Optional packs** — Turn on `packs.reachability` for **HLR Lookup** pre-dial reachability; `packs.usReassigned` for US RND via Twilio; `packs.usLitigator` / `packs.usDnc` for US CheckThatPhone flags; `packs.euRegisters` for licensed EU registers via your **BYO webhook** (IT RPO, ES Robinson, BE DNCM, SE NIX only).
5. **Quiet hours (optional)** — `quietHours` surfaces `review` outside your local contact window for call/SMS — enforcement stays in your dialer.
6. **Verdict** — Rules combine signals into **`send`**, **`skip`**, or **`review`** with parallel **`reasons[]`**, buyer-facing **`reason_codes[]`**, and **`caveats[]`** where coverage is incomplete.
7. **Audit** — Every enrichment step records ok/error/latency in **`audit`**; **`providers_used`** lists what ran; **`scrubbed_at`** timestamps the row.

Missing upstream credentials **skip the pack** and add caveats — the run does not fail silently as "all clear".

### Pre-dial phone scrub vs format validators

| | Format-only validators | This actor |
|---|---|---|
| Primary output | valid / invalid, E.164 | **send / skip / review** + reason codes |
| Line type | Usually absent | Twilio **line\_type\_intelligence** (default) |
| UK TPS/CTPS | Rare on Apify | **Built-in** via tpsapi when keyed |
| HLR / live reachability | Separate product | Opt-in **`packs.reachability`** |
| Audit | None | Per-step **`audit`** trail |
| Positioning | Syntax check | **Pre-dial scrub** before dial/SMS spend |

Competitors like standalone **Twilio Lookup** scripts or generic **phone number validation API** actors often stop at carrier or line type. This actor merges normalization, register checks, optional HLR, and a campaign-ready verdict so your dialer can branch on `verdict` instead of re-implementing rules.

### UK TPS/CTPS and HLR reachability

**UK TPS/CTPS** — When `packs.ukRegisters` is true (default) and `TPSAPI_API_KEY` is configured, GB/GG/JE/IM numbers are checked against Telephone Preference Service and Corporate TPS. Register hits appear in **`register_hits`**; listed numbers usually receive **`skip`** with register-specific reason codes. This is the primary **UK TPS check** path on Apify for outbound teams scrubbing British mobile and landline lists.

**HLR reachability** — When `packs.reachability` is true and `HLRLOOKUP_API_KEY` + `HLRLOOKUP_API_SECRET` are set, the actor calls **HLR Lookup** before dial. Dead, absent, or unreachable states surface in **`reachability`** and typically drive **`skip`** or **`review`**. HLR is opt-in because it adds upstream COGS (~$0.007 per lookup); default runs use line type + UK registers only.

**US optional packs** — `packs.usReassigned` (Twilio RND + `consentDate`), `packs.usLitigator`, and `packs.usDnc` (CheckThatPhone) are US-only opt-ins. They do not invent `listed=false` as certainty when the provider is silent.

**EU registers** — Licensed-register countries (IT, ES, BE, SE) require your **`EU_REGISTER_WEBHOOK_URL`** when `packs.euRegisters` is on. Consent-regime markets never receive invented register hits.

### Cost estimate

Pay-per-event on Apify Console — one charge per unique E.164 number scrubbed (duplicates that normalize to the same number are not charged again).

| Event | Price (list) | Charged when |
|---|---:|---|
| **`phone-scrubbed`** | **$0.024** | One unique E.164 number completes scrub in the dataset |
| **`apify-actor-start`** | $0.00005 | Run starts (platform synthetic; not charged in application code) |

**List price** is sized above Twilio line-type + tpsapi COGS (~$0.013) after Apify’s ~20% margin. Optional Store discount tiers may follow; until then every plan sees **$0.024 / phone-scrubbed**.

**Trial tip (Apify free plan):** set **Max total charge** to **$1–2** for evaluation (~40–80 numbers). Published example tasks use ≤5 phones and default packs only (no HLR) so trials stay cheap for both sides.

**COGS included in default scrub:** Twilio **line type** and **tpsapi** UK TPS/CTPS (when keys are present) are bundled into the single **`phone-scrubbed`** event — you do not pay separate Twilio line-items through Apify.

**HLR opt-in:** Enabling **`packs.reachability`** adds roughly **$0.007** upstream COGS per number; pricing keeps HLR inside the same **`phone-scrubbed`** event with margin rather than a second meter.

**Examples:**

- 1,000 UK mobiles with default packs (line type + TPS): **≈ $24** in `phone-scrubbed` events on FREE tier (~$16 on GOLD)
- Same run with HLR enabled on all rows: same event count; operator absorbs extra HLR COGS inside the configured PPE price

No monthly seat. Scale with batch size and `maxConcurrency` (default 5, max 25).

### Example output

```json
{
  "input_phone": "+44 7700 900123",
  "e164": "+447700900123",
  "country_iso": "GB",
  "valid": true,
  "verdict": "skip",
  "reason_codes": ["tps_listed"],
  "reasons": ["tps_listed"],
  "caveats": [],
  "register_hits": [
    {"register": "tps", "listed": true}
  ],
  "line_type": {
    "line_type": "mobile",
    "carrier": "Example Mobile Ltd",
    "available": true,
    "error": null,
    "twilio_valid": true
  },
  "reachability": null,
  "providers_used": ["normalize", "twilio_line_type", "tpsapi"],
  "audit": {
    "steps": [
      {"name": "normalize", "ok": true, "latency_ms": 2},
      {"name": "twilio_line_type", "ok": true, "latency_ms": 340},
      {"name": "tpsapi", "ok": true, "latency_ms": 180}
    ]
  },
  "scrubbed_at": "2026-08-21T12:00:00.000Z"
}
```

Field names match `.actor/dataset_schema.json` (`snake_case`). Filter downstream on **`verdict`** and **`reason_codes`**.

### FAQ

**What is a pre-dial phone scrub and how is it different from phone number validation?**
A pre-dial scrub runs immediately before outbound call or SMS and returns a campaign action — **send**, **skip**, or **review** — with reason codes. Format validators only check syntax; this actor adds line type, UK TPS/CTPS, optional HLR, and an audit trail.

**Does this actor perform an HLR lookup?**
Yes, when you set **`packs.reachability`** to true and configure **`HLRLOOKUP_API_KEY`** + **`HLRLOOKUP_API_SECRET`**. HLR is opt-in because it adds upstream cost; default runs use normalization, line type, and UK registers.

**Can I check UK TPS and CTPS before cold calling?**
Yes. With **`packs.ukRegisters`** enabled (default) and **`TPSAPI_API_KEY`** set, GB/GG/JE/IM numbers are checked against TPS/CTPS. Hits appear in **`register_hits`** and listed numbers usually get **`skip`**.

**How does this compare to Twilio Lookup alone?**
Twilio Lookup excels at line type and US reassigned-number packages. This actor wraps Twilio line type with libphonenumber normalization, UK TPS/CTPS, optional HLR, optional US litigator/DNC packs, and a unified **send/skip/review** verdict — closer to a **dnc list scrubber** workflow than a raw carrier API.

**What verdict values can I expect?**
Each row gets **`send`** (safe to dial/text given configured packs), **`skip`** (do not contact — invalid, listed, dead, risky line type, etc.), or **`review`** (human or rules engine should decide — missing credentials, quiet hours, EU webhook errors, consent-regime caveats).

**What are reason\_codes and caveats?**
**`reason_codes`** (and parallel **`reasons`**) explain why the verdict was chosen — e.g. `uk_tps_listed`, `invalid_number`, `hlr_dead`, `voip_sms_risk`. **`caveats`** flag coverage limits — e.g. register pack skipped because a key is missing, or `consent_or_local_rules` for markets without licensed register APIs.

**Is this a reverse phone lookup tool?**
No. It does not identify the person or business behind a number. It scrubs outbound lists you already have before dial/SMS.

**Does output guarantee GDPR or TCPA compliance?**
No. The actor provides operational signals and explicit caveats. You remain responsible for consent, local marketing law, and list hygiene in every country you contact.

**Do you invent EU do-not-call register results?**
No. Consent-regime EU markets (FR, DE, NL, AT, PT, IE) receive **`consent_or_local_rules`** caveats, not fabricated **`register_hits`**. Licensed registers (IT, ES, BE, SE) require your **`EU_REGISTER_WEBHOOK_URL`** when **`packs.euRegisters`** is on.

**How am I billed on Apify?**
One **`phone-scrubbed`** pay-per-event per unique E.164 number (**$0.024** list / FREE). Apify also charges **`apify-actor-start`** automatically. Duplicate inputs normalizing to the same E.164 are not charged twice. Store volume discounts (Bronze–Diamond) are scheduled; until they go live every plan sees the list price.

**What enrichment runs by default?**
**`packs.lineType`** and **`packs.ukRegisters`** default to true. HLR, US RND, US litigator/DNC, and EU register webhook packs default to false.

**Can I use this after Decision Maker Finder in a pipeline?**
Yes. Run [Decision Maker Finder](https://apify.com/pequod-labs/company-decision-maker-finder) for domain-to-phone enrichment, then pass **`decision_maker.best_phone`** values into this actor for pre-dial scrub before your sequencer or dialer.

**How do I automate verdict routing?**
Export the dataset, use Apify webhooks on run finished, or import [`examples/n8n-outbound-phone-scrub.json`](./examples/n8n-outbound-phone-scrub.json). Branch on **`verdict=skip`** to drop numbers before campaign upload.

### Pequod ecommerce pipeline

Compose discovery → contact enrichment → **pre-dial scrub** → shipping intel:

| Step | Actor | When |
|------|-------|------|
| 1. Discover | [Instagram TikTok Shop Leads](https://apify.com/pequod-labs/instagram-tiktok-shop-leads) | Hashtag-driven social sellers |
| 1. Discover | [Lookalike Discovery](https://apify.com/pequod-labs/ecommerce-lookalike-discovery) | Domain/niche-based stores |
| 1. Discover | [Amazon Seller Intelligence](https://apify.com/pequod-labs/amazon-seller-intelligence) | Third-party Amazon sellers + own-shop domains |
| 2. Enrich contact | [Decision Maker Finder](https://apify.com/pequod-labs/company-decision-maker-finder) | Name, role, verified email & phone |
| 3. **Pre-dial scrub** | **This actor** | **`send` / `skip` / `review`** before call or SMS |
| 4. Enrich shipping | [Shipping Intelligence](https://apify.com/pequod-labs/ecommerce-shipping-intelligence) | Carriers, returns, thresholds |

**Related:** Local offline prospects via [Google Maps Local Leads](https://apify.com/pequod-labs/google-maps-local-leads).

### Environment variables

| Variable | Required | Purpose |
|----------|----------|---------|
| `TWILIO_ACCOUNT_SID` | For Twilio packs | Lookup v2 (line type and/or reassigned) |
| `TWILIO_AUTH_TOKEN` | For Twilio packs | Lookup v2 |
| `TPSAPI_API_KEY` | For UK register pack | TPS/CTPS via tpsapi.com |
| `HLRLOOKUP_API_KEY` | For reachability pack | HLR Lookup API v2 |
| `HLRLOOKUP_API_SECRET` | For reachability pack | HLR Lookup API v2 |
| `CHECKTHATPHONE_API_KEY` | For US litigator/DNC packs | CheckThatPhone-style validation API |
| `EU_REGISTER_WEBHOOK_URL` | For EU register pack | BYO bridge for licensed national registers |
| `EU_REGISTER_WEBHOOK_TOKEN` | Optional | Bearer token for the BYO webhook |

### Local development

See [DEVELOPMENT.md](./DEVELOPMENT.md).

### Deploy

From `origin/main` with a clean tree and aligned vendor:

```bash
APIFY_TOKEN=... python3 scripts/deploy_prod.py
```

Never use `apify push` — it overwrites platform secrets.

# Actor input Schema

## `phones` (type: `array`):

One phone per item. Accepts national or international formats; optional <code>defaultRegion</code> helps parse numbers without a country prefix.

## `channel` (type: `string`):

How you plan to contact the number. SMS is stricter on risky VoIP/premium line types.

## `purpose` (type: `string`):

Campaign purpose metadata. Does not change MVP verdicts; US RND uses consentDate + packs.usReassigned instead.

## `packs` (type: `object`):

Toggle optional enrichment steps. Missing upstream credentials skip the pack and surface explicit coverage caveats instead of failing the run.

## `maxConcurrency` (type: `integer`):

Parallel upstream lookups per run (Twilio and TPSCheck).

## `defaultRegion` (type: `string`):

Two-letter ISO country code used when a number has no international prefix (e.g. <code>GB</code>, <code>IT</code>).

## `consentDate` (type: `string`):

YYYY-MM-DD or YYYYMMDD. Required for meaningful US RND when packs.usReassigned is on (sent to Twilio as LastVerifiedDate). EU numbers still get consent\_or\_local\_rules caveats from country coverage, not from this field.

## `quietHours` (type: `object`):

Optional local-time window flag. When enabled and the current local hour is outside \[startHour, endHour), call/SMS rows get review + quiet\_hours (enforcement stays in your dialer).

## Actor input object example

```json
{
  "phones": [
    "+447700123456",
    "+393331234567",
    "+33123456789"
  ],
  "channel": "unknown",
  "purpose": "unknown",
  "packs": {
    "lineType": true,
    "ukRegisters": true,
    "usReassigned": false,
    "reachability": false,
    "usLitigator": false,
    "usDnc": false,
    "euRegisters": false
  },
  "maxConcurrency": 5,
  "defaultRegion": "",
  "consentDate": "",
  "quietHours": {
    "enabled": false,
    "timezone": "America/New_York",
    "startHour": 8,
    "endHour": 21
  }
}
```

# Actor output Schema

## `scrubbedPhones` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

# 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 = {
    "phones": [
        "+447700123456",
        "+393331234567",
        "+33123456789"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pequod-labs/outbound-phone-scrub").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 = { "phones": [
        "+447700123456",
        "+393331234567",
        "+33123456789",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("pequod-labs/outbound-phone-scrub").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "phones": [
    "+447700123456",
    "+393331234567",
    "+33123456789"
  ]
}' |
apify call pequod-labs/outbound-phone-scrub --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,pequod-labs/outbound-phone-scrub"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/xwPwdvHEwLSeJfGMI/builds/XZbQb0nP7cIhVUUxO/openapi.json
