# Website Contact Extractor – Emails, Phones, Socials & LINE (`raspberry_rondel/website-contact-extractor`) Actor

Get the business emails, phone numbers and social profiles (incl. LINE) that companies publish on their own websites, with source URLs. Taiwan phone formats, filters obvious junk (e.g. image filenames, placeholder emails), obeys robots.txt. $0.003/domain with contacts + $0.00005/GB start.

- **URL**: https://apify.com/raspberry_rondel/website-contact-extractor.md
- **Developed by:** [星賢 陳](https://apify.com/raspberry_rondel) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Website Contact Extractor: Emails, Phones, Social Links and LINE

Give it a list of company websites and get back one clean row per domain: the **business emails, phone numbers and social media profiles that the company publishes on its own site**, each with the page it came from. The crawl is small and polite. It reads the homepage plus a few contact or about pages, obeys `robots.txt` (including `Crawl-delay`), and does not log in or try to work around anti-bot protection.

Built with Taiwan and Asia in mind, and works worldwide:

- **LINE official accounts** (`line.me`, `lin.ee`, `page.line.me`) are detected and returned in their own `lineLinks` column.
- **Taiwan phone formats** are supported, for example `02-2758-3916`, `(02)2758-3916`, `+886-2-2758-3916`, `0928-617-354`, `+886 928 617 354`, `0800-261-735` and `(04)2327-5916 分機 123`. Phone numbers are also normalised to E.164 (`+886227583916`) using Google's libphonenumber.
- Chinese labels such as 電話, 傳真, 分機, 客服 and 統一編號 are understood, so fax numbers are labelled as fax and company tax IDs are filtered out rather than returned as phone numbers.

### What you get

| Field | Description |
|---|---|
| `domain`, `homepageUrl`, `status` | The site and how the crawl went (`ok`, `no-contacts-found`, `blocked-by-robots-txt`, `blocked-by-anti-bot`, `unreachable`, `http-error`, `invalid-url`, `skipped-spending-limit`, …) |
| `emailList` | Unique emails, lower-cased |
| `phoneList` | Unique phone numbers in E.164 format |
| `socialLinks` | Profiles grouped by platform: Facebook, Instagram, X/Twitter, LinkedIn, YouTube, TikTok, LINE, Threads, Pinterest, Telegram, WhatsApp, Discord, Weibo, Xiaohongshu, Plurk and Dcard |
| `lineLinks` | LINE official account and add-friend links |
| `emails[]` | Details for each email: `isRoleBased` (info@, sales@ …), `isFreeMailProvider` (gmail.com …), `foundIn` (mailto-link, page-text, json-ld), `sourceUrls` |
| `phones[]` | Details for each number: `e164`, `extension`, `national`, `international`, `country`, `kind` (phone or fax), the `raw` text as written on the page, `sourceUrls` |
| `socials[]` | `platform`, `url`, `handle`, `sourceUrls` |
| `socialLinksDropped` | How many social profile links were found but not returned because they are not the company's own accounts (personal profiles such as `linkedin.com/in/…`, team members' accounts, unrelated profiles). 0 when `allSocialLinks` is on |
| `pagesCrawled`, `pagesSkippedByRobots`, `robotsTxt` | The pages that were read, their HTTP status, and how robots.txt was applied |
| `junkEmailsFiltered`, `obfuscatedEmailsNotDecoded`, `personalEmailsDropped` | How many junk strings were dropped, how many emails the site deliberately hid (see below), and how many addresses that may belong to a person were dropped by `onlyRoleBasedEmails` |
| `charged`, `crawledAt`, `crawlSeconds` | Billing flag and timing |

Example row (shortened):

```json
{
  "domain": "katzsdelicatessen.com",
  "status": "ok",
  "emailList": ["press@katzsdelicatessen.com"],
  "phoneList": ["+12122542246"],
  "socialLinks": {"facebook": ["https://www.facebook.com/katzsdeli"], "instagram": ["https://www.instagram.com/katzsdeli"], "x": ["https://x.com/KatzsDeli"]},
  "lineLinks": [],
  "phones": [{"e164": "+12122542246", "national": "(212) 254-2246", "kind": "phone", "sourceUrls": ["https://katzsdelicatessen.com/address"]}],
  "pagesCrawled": [{"url": "https://katzsdelicatessen.com/", "httpStatus": 200, "result": "ok"}, {"url": "https://katzsdelicatessen.com/contact", "httpStatus": 200, "result": "ok"}]
}
```

The dataset has an **Overview** view (one row per domain, easy to export to CSV or Excel) and a **Details** view.

### Quality: precision first

The output is filtered to cut down the clean-up you need to do:

- **Filters obvious junk** (e.g. image filenames, placeholder emails). The filter covers image and asset names that look like emails (`logo@2x.png`, `icon@3x.webp`), placeholders (`you@example.com`, `name@domain.com`), tracking hashes, `noreply@` addresses, and strings whose top-level domain does not exist (checked against the official IANA list).
- **Phone numbers must look like phone numbers in context.** That means a `tel:` link, a phone keyword nearby (Tel, Phone, 電話, 客服 …), or the national format. Dates, prices, order numbers and Taiwan company tax IDs (統一編號) are filtered out. Extensions (`分機`, `轉`, `ext.`, `#`) are kept separately.
- **Social links are filtered to the company's own accounts** (default, `allSocialLinks` off). A profile is kept when it sits in the site's header, footer or navigation, when it is listed in the homepage's schema.org `sameAs`, when its handle matches the brand or domain, or when it is the only profile for that platform in a contact or follow block on a page that does not list people's profiles. Personal profiles such as `linkedin.com/in/…`, team members' accounts in about or team pages, author bylines and share buttons are dropped; the number dropped is in `socialLinksDropped`. A link to a single post is not returned as a post: on Instagram, YouTube and LinkedIn it is ignored, and on X and Facebook it is reduced to the account that posted it, which then goes through the same rules. Turn `allSocialLinks` on to keep every social link found, including personal profiles; do this only if you have a legal basis to process personal data.
- Results are **deduplicated across pages**, with every source page listed.

Internal test 2026-10-18, build 1.0.16, 26 sites incl. redis.io and Taiwanese sites (21 of them returned contact data; 3 had none, 1 was skipped because of robots.txt and 1 because of an anti-bot wall): 0 unrelated social profiles; personal profiles such as `linkedin.com/in/…` are dropped and counted in `socialLinksDropped`. Results on other sites may differ.

**Emails hidden by the site are not decoded.** Some sites protect their addresses from harvesting (for example Cloudflare email obfuscation). We respect that choice: these emails are counted in `obfuscatedEmailsNotDecoded` but not decoded or returned.

### How to use

1. Put the websites into **Start URLs**. Bare domains like `example.com` work too. Several URLs from the same domain are merged into one row.
2. Optionally set **Max pages per domain** (default 4: the homepage plus 3 contact or about pages; up to 20).
3. Set **Default country** if the sites are local businesses without a country domain. Phone numbers written without a country code are read in that country's format. Without it, the country is inferred from the domain (`.tw`, `.jp`, `.de` …), then the page language, then Chinese-text cues.
4. Run it, then download the dataset as JSON, CSV or Excel, or use the API.

Other options:

- `useBrowser`: `off` (default; fast plain HTTP), `auto` (render the homepage in Chromium only when it is an empty JavaScript shell), or `always`. Browser mode needs 2 GB of memory (set it in the run options).
- `onlyRoleBasedEmails` (default **on**): keeps only generic business mailboxes such as info@, sales@, service@ and drops addresses that may belong to a person (jane.doe@…). The number dropped is in `personalEmailsDropped`. Turn it off only if you have a legal basis to process personal data.
- `includeFax`, `probeCommonPaths` (try `/contact`, `/about` … when the homepage links to none), `maxCrawlDelaySecs`, `maxConcurrency`, `requestTimeoutSecs`.

### Pricing

Pay per event. Charged per domain with results (US$0.003), not per page crawled; domains with nothing found are free.

| Event | Price |
|---|---|
| Domain with contacts (`domain`) | **$0.003** per domain ($3 per 1,000 domains), no matter how many pages were read |
| Actor start (`apify-actor-start`) | $0.00005 per GB of memory, charged at the start of each run (Apify's standard start event) |

**Free:** domains with no contacts, domains blocked by robots.txt or by anti-bot protection, unreachable sites, errors, and duplicates. With `onlyRoleBasedEmails` on (the default), a domain where the only contacts found were personal email addresses is reported as `no-contacts-found` and is not charged; `personalEmailsDropped` shows how many addresses were removed. If the same domain also has a phone number or a social link, it counts as a domain with contacts and is charged.

Set **Maximum cost per run** and the run stops charging, and processing, before it would go over your limit. Domains skipped because of the limit are reported as `skipped-spending-limit` and are free. If a run is restarted, domains already in the dataset are not charged again.

**Start fee:** Apify's small platform start fee (`apify-actor-start`, US$0.00005 per GB of memory) is charged for every run, even when every domain is free or every input is invalid.

The default 512 MB memory is enough for HTTP mode. By default 10 domains are processed in parallel, but the pages of each site are fetched one at a time, at least 1 second apart.

### Responsible use: personal data, GDPR and Taiwan's PDPA (個人資料保護法)

This Actor reads only what a company publicly posts on its own website, and it is designed for **business contact information** (info@, sales@, switchboard numbers, official social pages). Public pages can still contain **personal data**, though: a named employee's email address, a sole proprietor's mobile number, a staff member's LinkedIn profile. **Publicly available does not mean free to use for every purpose.** Before you run it, make sure you have a lawful basis and a clear purpose.

**Your role.** Under the [Apify Standard Actor Contract](https://docs.apify.com/legal/standard-actor-contract) (section 5.2.1), when you run this Actor on personal data, **you are the data controller** and the Actor's developer acts as a processor on your behalf. You decide which sites to crawl and what to do with the results, so you are responsible for complying with the law. See also Apify's [GDPR information](https://docs.apify.com/legal/gdpr-information) and [Data Processing Addendum](https://docs.apify.com/legal/data-processing-addendum).

**What Apify says about scraping personal data.** Apify's own articles [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/) and [What is ethical web scraping?](https://blog.apify.com/what-is-ethical-web-scraping-and-how-do-you-do-it/) explain that personal data is protected even when it is public. They also say you should collect only what you need for a legitimate purpose, and that building databases of people for lead generation is legally difficult in jurisdictions with strong data protection.

**GDPR (EU/EEA and UK).** You need a lawful basis, usually legitimate interest backed by a documented balancing test. You must also follow data minimisation and purpose limitation, inform the people concerned (Art. 14) within a reasonable time, honour objections and erasure requests, and follow local direct-marketing (ePrivacy) rules, which often require prior consent for marketing email to individuals.

**Taiwan PDPA (個人資料保護法).**

- Non-government entities may collect and process personal data that **the data subject has made public or that has been lawfully made public** (當事人自行公開或其他已合法公開之個人資料), but **only for a specific purpose** (特定目的) ([Art. 19(1)(3)](https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=I0050021)).
- The [Enforcement Rules, Art. 13](https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=I0050022) explain what counts as data the subject made public themselves (自行公開) and data that was lawfully made public (已合法公開).
- For data collected indirectly, the notice obligation can be waived when the data was publicly disclosed ([Art. 9(2)(2)](https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=I0050021)). The use must still stay within the specific purpose.
- If you use the data for **marketing**, you must stop as soon as the person objects, and you must offer a way to refuse when you first contact them (Art. 20).

**Email marketing and anti-spam laws.** Finding an address on a website does not give you permission to email it.

- **United States (CAN-SPAM, [15 U.S.C. §7704](https://www.law.cornell.edu/uscode/text/15/7704)).** Every commercial email must identify itself as an advertisement, offer a working opt-out, and include a valid postal address (§7704(a)(5)). Sending commercial email to addresses that were collected by automated means from a website that states it will not give, sell or transfer its addresses for emailing is an aggravated violation (§7704(b)(1)). Check each site for such a notice before you email anyone.
- **EU/EEA and UK.** People can object to direct marketing whenever they choose, and you must stop when they do (GDPR [Art. 21(2)–(3)](https://gdpr-info.eu/art-21-gdpr/)). You must also tell them where you got their data ([Art. 14](https://gdpr-info.eu/art-14-gdpr/)). Many countries' ePrivacy rules require prior consent before sending marketing email to individuals.
- **Taiwan.** There is no separate anti-spam act; the PDPA applies. Offer a way to refuse marketing in your first message, and stop as soon as someone objects ([Art. 20(2)–(3)](https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=I0050021)).

Do not use this Actor to build bulk cold-email lists of individuals, to send unsolicited bulk email, or to resell contact lists.

**電子郵件行銷與反垃圾郵件法規（中文）。** 在網站上找到電子郵件地址，不代表你可以寄信給對方。

- **美國（CAN-SPAM，[15 U.S.C. §7704](https://www.law.cornell.edu/uscode/text/15/7704)）。** 每封商業電子郵件都必須標示為廣告、提供有效的退訂方式，並附上有效的實體郵寄地址（§7704(a)(5)）。若網站已聲明不會提供、出售或轉讓其電子郵件地址，而你寄商業郵件到以自動化方式從該網站擷取的地址，屬於加重違規（§7704(b)(1)）。寄信前，請先確認每個網站是否有這類聲明。
- **歐盟／歐洲經濟區與英國。** 當事人可以對直接行銷提出異議，一旦提出，你就必須停止（GDPR [第21條第2–3項](https://gdpr-info.eu/art-21-gdpr/)）。你也必須告知對方資料從何處取得（[第14條](https://gdpr-info.eu/art-14-gdpr/)）。許多國家的 ePrivacy 規定要求，寄送行銷郵件給個人之前須先取得同意。
- **台灣。** 台灣沒有反垃圾郵件專法，適用個人資料保護法。第一次行銷時就要提供拒絕接受行銷的方式，對方一表示拒絕就應停止（[第20條第2–3項](https://law.moj.gov.tw/LawClass/LawAll.aspx?pcode=I0050021)）。

請勿使用本 Actor 建立個人的大量陌生開發（cold email）名單、濫發未經同意的大量郵件，或轉售聯絡人名單。

**Practical rules we recommend:**

- Prefer role-based addresses. `onlyRoleBasedEmails` is on by default and keeps only addresses such as info@ and sales@. Turn it off only if you have a lawful basis to process personal email addresses. Role-based detection uses a list of common mailbox names, so treat it as a filter, not a guarantee. It applies to emails only: phone numbers are returned either way and social profiles follow the `allSocialLinks` setting, so review those too.
- Keep the `sourceUrls` as proof of where and when each contact was published.
- Do not build profiles of individuals, do not combine this data with sensitive data, and delete what you no longer need.
- Respect opt-outs and "no solicitation" notices on websites.

This section is general information, not legal advice. If in doubt, ask a lawyer in the relevant country.

**What the Actor itself does to stay polite.** It obeys robots.txt (RFC 9309), including `Crawl-delay`; a site whose robots.txt cannot be fetched because of a server error is treated as "do not crawl". If a bare domain such as `example.com` does not resolve, has a certificate only for `www.`, or does not answer, the Actor tries `www.example.com` once and obeys that host's own robots.txt. The row then has a note saying so. It identifies itself honestly with the user agent `RondelBot/1.0`, does not log in, does not use proxies to hide, and stops at the first anti-bot challenge without trying to bypass it. It reads at most `maxPagesPerDomain` pages per site and does not decode emails that the site deliberately obfuscates.

### Limitations

- Contacts that appear only inside images, PDFs or contact forms are not found. That happens often on Taiwanese restaurant and retail sites.
- Sites that need JavaScript to show anything need `useBrowser: auto` or `always`.
- Sample and placeholder phone numbers are filtered out: numbers right after hint words such as "ex:", "e.g.", "例：", "範例" or "請輸入", and numbers built from repeated or sequential digits (for example 0900-123-123, 0912-345-678, 02-1234-5678, 123-456-7890, 0911-111-111). Input placeholder attributes are not read. This is heuristic: a real number made of such digits (for example 02-2222-2222) is also dropped, and an unusual sample number can still slip through. Check `phones[].raw` and `sourceUrls` when in doubt.
- With `maxPagesPerDomain: 4` the crawl favours precision and low cost over completeness. Large sites with many branch pages may list more numbers deeper in the site.
- Sites behind anti-bot protection (Cloudflare challenges, DataDome and similar) are reported as `blocked-by-anti-bot` and are not charged.

# Actor input Schema

## `startUrls` (type: `array`):

One URL per company website (e.g. https://www.example.com.tw). Duplicates of the same domain are merged and charged once. One dataset row per domain.

## `maxPagesPerDomain` (type: `integer`):

Homepage + up to N-1 contact/about/locations pages linked from it. Charged per domain with results (US$0.003), not per page crawled; domains with nothing found are free.

## `defaultCountry` (type: `string`):

ISO country code (e.g. TW, US, GB) used to read numbers without a +country prefix when the country cannot be inferred from the domain (.tw, .co.uk…) or the page language (zh-TW…). Leave empty to only accept such numbers when the country is known.

## `includeFax` (type: `boolean`):

Numbers labelled 傳真 / Fax are returned with kind = fax. Turn off to drop them.

## `onlyRoleBasedEmails` (type: `boolean`):

Return only generic business mailboxes such as info@, sales@, service@, contact@ and drop addresses that may belong to a person (jane.doe@…). Recommended if you have no legal basis to process personal data.

## `allSocialLinks` (type: `boolean`):

By default only the site's own accounts are kept: links in the site-wide header/footer/navigation, profiles listed in the homepage's schema.org sameAs, profiles whose handle matches the brand, and a single profile per platform in a contact or follow block. Personal profiles (linkedin.com/in/…) and team members' accounts on about/company pages are dropped and counted in socialLinksDropped. Turn on to keep every social link found, including personal profiles; do this only if you have a legal basis to process personal data.

## `probeCommonPaths` (type: `boolean`):

If the homepage links to no contact/about page, try /contact, /contact-us, /about, /about-us (still within maxPagesPerDomain and robots.txt).

## `useBrowser` (type: `string`):

off = plain HTTP (fast, cheapest). auto = render the homepage in headless Chromium only when the HTML is almost empty (JavaScript-only sites); needs 2 GB memory. always = render every homepage. Rendering does not try to bypass bot protection.

## `maxCrawlDelaySecs` (type: `integer`):

robots.txt Crawl-delay is obeyed. If a site asks for a longer delay than this, only its homepage is fetched (instead of waiting minutes).

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

How many different domains are crawled at the same time (pages of one domain are fetched one by one, at least 1 s apart).

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout. Slow sites that exceed it are reported as unreachable (not charged).

## Actor input object example

```json
{
  "startUrls": [
    "https://www.pxmart.com.tw",
    "https://www.innocentdrinks.co.uk"
  ],
  "maxPagesPerDomain": 4,
  "defaultCountry": "",
  "includeFax": true,
  "onlyRoleBasedEmails": true,
  "allSocialLinks": false,
  "probeCommonPaths": true,
  "useBrowser": "off",
  "maxCrawlDelaySecs": 10,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

## `contacts` (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 = {
    "startUrls": [
        "https://www.pxmart.com.tw",
        "https://www.innocentdrinks.co.uk"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("raspberry_rondel/website-contact-extractor").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 = { "startUrls": [
        "https://www.pxmart.com.tw",
        "https://www.innocentdrinks.co.uk",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("raspberry_rondel/website-contact-extractor").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 '{
  "startUrls": [
    "https://www.pxmart.com.tw",
    "https://www.innocentdrinks.co.uk"
  ]
}' |
apify call raspberry_rondel/website-contact-extractor --silent --output-dataset

```

## MCP server setup

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

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/JvJElSeUSiisntdPH/builds/rW0ppqwIJ67uUnbVA/openapi.json
