# Website Contact Scraper — Email & Phone with a Billing Receipt (`drumlinlabs/contact-receipt`) Actor

Extract contact details — emails and phone numbers — from any website you supply. Priced per contact delivered, not per page crawled, with a full receipt reconciling pages fetched, contacts found, duplicates dropped, and units charged on every run.

- **URL**: https://apify.com/drumlinlabs/contact-receipt.md
- **Developed by:** [Drumlin Labs](https://apify.com/drumlinlabs) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 contact delivereds

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

## Website Contact Scraper — Email & Phone with a Billing Receipt

Extract **emails and phone numbers** from a list of websites. Hand it 5,000 domains; get back
every contact found, deduped, and a receipt showing exactly what was charged — so you can
verify the bill yourself rather than take it on faith.

The difference from every other scraper in this category: you are billed **per contact
delivered**, not per page crawled. A page with 0 contacts costs nothing. A page that takes
800 sub-requests to crawl still only bills for the contacts it yields, not the pages it touched
to get there.

***

### Pricing

**$0.004 per contact delivered** — emails and phone numbers only. Refused URLs, failed fetches,
and duplicates are never charged. Your maximum cost per run is a hard ceiling, not an
approximation: work stops before the next charge would exceed it.

***

### What it extracts

- **Emails** — from `mailto:` links (highest reliability) and page text. Common false positives
  (image filenames, file paths) are filtered.
- **Phone numbers** — from `tel:` links. Text-extracted phone numbers are not billed because
  phone patterns in text produce too many false positives; `tel:` links are authoritative.
- **Social profile URLs** — LinkedIn, X/Twitter, Facebook, Instagram, GitHub, YouTube. Reported
  for your information; social profiles are not billed.

***

### What it refuses

Some sources use anti-bot measures or prohibit automated access in their terms of service.
URLs on those domains are **refused before any fetch is attempted** and appear in the dataset
with `refused: true` and a `refusedReason` explaining why. Refused URLs are never billed.
You will see in the BILLING\_RECEIPT exactly how many URLs were refused and why.

***

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `urls` | array | — | Websites to crawl. Accepts `https://example.com`, `http://example.com/contact`, or bare `example.com`. Duplicates are removed before any fetch. |
| `concurrency` | integer | 5 | Parallel requests (1–20). Higher is faster; lower is gentler on target sites. |

```json
{
  "urls": ["https://example.com", "https://company.io/contact"],
  "concurrency": 5
}
```

***

### Output

One dataset row per input URL:

| Field | Description |
|---|---|
| `url` | The URL that was crawled (or refused) |
| `hostname` | Hostname extracted from the URL |
| `refused` | `true` if this URL was refused by the denylist before any fetch |
| `refusedReason` | Why the URL was refused, if refused |
| `fetchedOk` | `true` if the page was fetched and parsed without error |
| `statusCode` | HTTP status code from the server |
| `error` | Error message if the fetch failed |
| `emails` | Unique email addresses found on this page |
| `phones` | Unique phone numbers found on this page (from `tel:` links) |
| `socials` | Social profile objects: `{ platform, url }` |
| `contactCount` | `emails.length + phones.length` for this page |
| `newContacts` | Contacts from this page that were new to the run (not already found on another page) |

A `SUMMARY` key-value record gives aggregate counts for the run.

***

### Check your own bill: the billing receipt

Every run writes a **`BILLING_RECEIPT`** to the key-value store. Find it at
**Storage → Key-value store → BILLING\_RECEIPT** or via API:

```
GET https://api.apify.com/v2/key-value-stores/<runKeyValueStoreId>/records/BILLING_RECEIPT
```

The same numbers appear at the end of the run log under `--- billing receipt ---`.

| Field | What it means |
|---|---|
| `eventName` | The charged event — `contact-delivered`. |
| `delivered` | Unique contacts (emails + phones) extracted and returned to you. |
| `billedEvents` | Unique contacts **actually charged**. Compare against your invoice. |
| `duplicatesDropped` | Contacts already found earlier in the same run — delivered once, charged once. |
| `failuresNotCharged` | Extraction attempts that failed. Never charged. |
| `invalidNotCharged` | Inputs that produced no billable contact — duplicate URLs, refused hosts and failed fetches. Never charged. |
| `skipReasons` | Breakdown of skipped inputs by reason (e.g. how many URLs were refused and for which domain). |
| `budgetExhausted` | `true` if your maximum cost per run stopped the work. |
| `deliveredUnbilled` | Contacts already in flight when the cap was reached — delivered to you free. |
| `billingActive` | `false` when pay-per-event pricing is not in effect — the run was free. |
| `coverageRatio` | `null` — a URL list has no external denominator, so no coverage ratio is calculated. |
| `note` | One sentence summarising the billing status. |

**Reconciling the receipt against what you sent.** Every URL you submit ends in exactly one of four
places, and the two records together account for all of them:

```
URLs submitted = urlsCrawled + urlsRefused + urlsFailed + inputDuplicatesSkipped   (SUMMARY)
delivered      = unique contacts returned to you                                   (BILLING_RECEIPT)
billedEvents   = what you were charged for — never more than delivered
```

`skipReasons` in the receipt itemises every URL that produced nothing, by reason and by host, so a
refusal or a dead page is visible rather than merely absent. **A URL that vanishes without appearing
in one of those counts is a bug — report it with the run ID and it will be treated as one.**

If `billedEvents` exceeds the number of unique contacts you received, that is a bug — report
the run ID to **<support@drumlin.dev>**.

***

### Honest limitations

- **One page per URL.** The actor fetches the URL as submitted. It does not follow internal links
  or crawl a whole site. Submit a `/contact` page directly if that is where the contacts are.
- **JavaScript-rendered pages.** The actor uses standard HTTP fetches. Content loaded
  dynamically after page load is not captured.
- **`tel:` links only for phones.** Phone numbers in plain text are omitted because formatted
  phone patterns in prose produce too many false positives to bill on. Use `tel:` links for
  reliable phone extraction.
- **Results reflect the page at the moment of the run.** Pages change.

***

### About

Every result is checkable without trusting us: the receipt reconciles what was fetched, what
was found, and what was charged, so an unexpected bill can be traced to a specific URL rather
than disputed in the dark.

A Drumlin Labs tool — [labs.drumlin.dev](https://labs.drumlin.dev).

**Support: <support@drumlin.dev>** — the right address for a wrong
result, an unexpected charge, or a refused URL you think should be allowed. Charges, invoices
and refunds are handled by Apify as merchant of record, not through this address.

# Actor input Schema

## `urls` (type: `array`):

Website URLs or bare domains to extract contacts from. Accepts https://example.com, http://example.com/contact, or bare example.com — each is normalised to a URL and fetched once. Duplicates are removed before any fetch.

## `concurrency` (type: `integer`):

How many pages to fetch in parallel. Higher is faster; lower is gentler on target sites and avoids rate limiting.

## Actor input object example

```json
{
  "urls": [
    "https://example.com",
    "https://httpbin.org/html"
  ],
  "concurrency": 5
}
```

# Actor output Schema

## `contacts` (type: `string`):

One row per input URL: emails found, phones found, whether the URL was refused by the denylist, and whether the fetch succeeded.

## `receipt` (type: `string`):

Reconciles pages fetched, contacts found, duplicates dropped, and units charged so you can verify the bill yourself.

## `summary` (type: `string`):

Aggregate counts: URLs crawled, refused, failed, and unique contacts delivered.

# 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 = {
    "urls": [
        "https://example.com",
        "https://httpbin.org/html"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("drumlinlabs/contact-receipt").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 = { "urls": [
        "https://example.com",
        "https://httpbin.org/html",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("drumlinlabs/contact-receipt").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 '{
  "urls": [
    "https://example.com",
    "https://httpbin.org/html"
  ]
}' |
apify call drumlinlabs/contact-receipt --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,drumlinlabs/contact-receipt"
        }
    }
}

```

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/unZKSb2l3RyaaennG/builds/qtcqv6yw9RuvcxQTl/openapi.json
