Website Contact Scraper – Emails, Phones & Socials avatar

Website Contact Scraper – Emails, Phones & Socials

Pricing

from $2.00 / 1,000 domains

Go to Apify Store
Website Contact Scraper – Emails, Phones & Socials

Website Contact Scraper – Emails, Phones & Socials

Extract emails, phone numbers, WhatsApp numbers and social media profiles from any list of websites. Get one clean row per website: contact pages are crawled first, junk is filtered out and duplicates are merged. Export to Excel, CSV or JSON, or connect via API.

Pricing

from $2.00 / 1,000 domains

Rating

5.0

(1)

Developer

Emiliano Mastragostino

Emiliano Mastragostino

Maintained by Community

Actor stats

6

Bookmarked

1.1K

Total users

66

Monthly active users

8 days ago

Last modified

Features

Limited permissions

Share

πŸ”Ž Website contact scraper for emails, phone numbers and social profiles

Website Contact Scraper finds public email addresses, phone numbers, WhatsApp numbers and social media profiles on company websites. Submit one website or a list of thousands and get one clean row per website, ready for your CRM, lead list or spreadsheet.

  • βœ… One consolidated result for each website
  • βœ… Predictable costs
  • βœ… Contact pages crawled first
  • βœ… Filtered and deduplicated contact data
  • βœ… Progressive output suitable for bulk runs
  • βœ… Validated phone numbers, no look-alikes
  • βœ… Automation-ready status and diagnostics

What you get

A simplified view of the output, one row per website:

Start URLStatusEmailsPhonesSocials
https://www.bakery.example/completedinfo@bakery.example+390212345678https://instagram.com/bakery
https://facebook.com/bakery
https://lawfirm.example/completedoffice@lawfirm.example
privacy@lawfirm.example
+442079460958
+442079460959
https://linkedin.com/company/lawfirm
https://studio.example/completedβ€”β€”β€”
https://offline.example/failedβ€”β€”β€”
  • studio.example was crawled successfully but publishes no contacts.
  • offline.example could not be reached; its record includes the error and whether a retry makes sense.

Export the results to Excel, CSV, JSON, XML or HTML, or read them through the Apify API. The full output example shows every field.

No configuration is needed: add your website URLs and run. Scraping websites from a single country? Set Default phone country (for example US, GB or IT) to get more phone numbers, all in international format.


πŸ“Š What contact data does the Actor extract?

DataOutput
EmailsFiltered, normalized, and deduplicated public email addresses
Phone numbersValidated numbers, in E.164 when the country is known
WhatsApp numbersInternational numbers (digits only) extracted from WhatsApp links
Social profilesCanonicalized LinkedIn, Facebook, Instagram, X, YouTube, TikTok, GitHub, Pinterest, Telegram
Website metadataCanonical start URL and redirected start-page URL
Crawl diagnosticsStatus, page counters, timing, termination reason, and sanitized errors

✨ Why use Website Contact Scraper?

One website, one consolidated result

Generic website scrapers usually return one record per visited page.

Website Contact Scraper combines contacts found across the crawled pages into one structured record. Each unique valid start URL normally produces one result.

This makes it easier to:

  • join contacts back to an existing lead or company record;
  • avoid merging page-level results manually;
  • import data into a CRM or database;
  • build predictable automation workflows.

It also keeps costs predictable: you pay for one result per unique website, while duplicate and invalid URLs are free (see Pricing below).

Contact pages crawled first

The Actor visits the pages most likely to list contacts first, in this order:

  1. contact pages;
  2. legal notices, such as an Impressum or mentions lΓ©gales;
  3. privacy policies, which usually name a contact for data requests;
  4. support and help pages;
  5. about and company pages;
  6. other legal pages, such as terms and cookie policies.

It recognizes these pages in English, German, Spanish, French, and Italian, for example kontakt, contacto, contatti, nous-contacter, and impressum.

Within the page budget, pages are always chosen in this order, so repeated runs on the same website visit the same pages. If one of them fails, the next candidate takes its place.

You control the page budget, crawl depth, URL scope, concurrency, proxy use, and optional early stopping.

Built for bulk automation

Results are written as soon as each website finishes, so faster websites do not wait for the slowest one in the run.

Every record includes machine-readable information for routing results, identifying retryable failures, and monitoring crawl coverage.

What gets filtered out

Websites are full of strings that look like contacts but are not. A typical scraper returns them; this Actor filters or fixes them:

Found on the pageTypical scraper returnsWebsite Contact Scraper returns
500043-012 (a product code)a phone numbernothing
VAT No. GB 314 1592 65 (a UK VAT number)314 1592 65 as phonenothing
24/07/2025 (a date)a phone numbernothing
\u003ehelp@acme.com (from embedded page code)u003ehelp@acme.comhelp@acme.com
yourname@gmail.com (a form placeholder)an emailnothing
youtube.com/embed/2VY4BPACEww (a video)a social profilenothing
twitter.com/Acme and x.com/acme2 profileshttps://twitter.com/acme

βš™οΈ How it works

For every valid submitted start URL, the Actor:

  1. opens the website;
  2. identifies and prioritizes likely contact pages;
  3. crawls within the configured URL scope, depth, and page budget;
  4. extracts email, phone, WhatsApp, and social-profile candidates;
  5. validates, normalizes, and deduplicates the contacts;
  6. writes one consolidated result when the website finishes.

πŸš€ How to scrape website contacts

  1. Add one representative URL per website, preferably the homepage.
  2. Keep the defaults or adjust the crawl settings.
  3. Run the Actor.
  4. Review the consolidated records.
  5. Export the data or consume it through the Apify API, SDKs, webhooks, or schedules.

Basic input example

{
"startUrls": [
{
"url": "https://apify.com"
}
]
}

Full input example

{
"startUrls": [
{
"url": "https://example.com"
},
{
"url": "https://another-example.com"
}
],
"maxPagesPerStartUrl": 12,
"maxDepth": 2,
"defaultPhoneCountry": "US",
"enqueueStrategy": "same-domain",
"allowedSubdomains": ["www", "support"],
"earlyStopMode": "balanced",
"maxRequestsPerCrawl": 500,
"maxConcurrency": 10,
"proxyConfigurationOptions": {
"useApifyProxy": true
}
}

This example shows the full available configuration. For most runs, you can start with the defaults and change only the fields you actually need.

Output example

Each result contains the consolidated contacts together with crawl status and diagnostics:

{
"schemaVersion": 1,
"status": "completed",
"hasContacts": true,
"startUrl": "https://acme.example/",
"finalUrl": "https://www.acme.example/privacy-policy",
"contacts": {
"emails": ["info@acme.example", "privacy@acme.example"],
"phones": ["+14155552671"],
"socials": [
{
"type": "linkedin",
"value": "https://linkedin.com/company/acme"
}
],
"whatsapps": ["14155552671"]
},
"crawl": {
"pagesVisited": 4,
"pagesAttempted": 4,
"pagesSucceeded": 4,
"pagesFailed": 0,
"visitedSample": [
"https://www.acme.example/",
"https://www.acme.example/contact",
"https://www.acme.example/impressum",
"https://www.acme.example/privacy-policy"
],
"stoppedReason": "crawler-finished",
"terminationReason": "crawl_completed",
"startPageFinalUrl": "https://www.acme.example/",
"startPageHttpStatusCode": 200,
"startedAt": "2026-01-01T12:00:00.000Z",
"finishedAt": "2026-01-01T12:00:04.200Z",
"durationMs": 4200,
"error": null,
"updatedAt": "2026-01-01T12:00:04.200Z"
}
}

🎯 Common use cases

Enrich company and lead lists

Append public emails, phone numbers, WhatsApp numbers, and social profiles to existing website records.

Prepare CRM-ready records

Produce one consolidated result per submitted website and join it to your source data through startUrl (see Start URLs for how URLs are canonicalized).

Enrich directory and marketplace data

Use the Actor after collecting company websites from directories, maps, marketplaces, or other scrapers.

Build and audit business datasets

Add contact channels to supplier, agency, dealership, local-business, property, ecommerce, or service-provider lists.

You can also distinguish websites that:

  • expose supported contacts;
  • were processed successfully but contain no supported contacts;
  • failed because of blocking, DNS, TLS, or network errors.

πŸ’³ Pricing

You pay per website result: each unique website you submit produces one row, and you are charged only for rows written to the dataset.

  • Duplicates are free: URLs that repeat in your input are merged and billed once.
  • Invalid URLs are free: malformed and non-HTTP(S) URLs are skipped and never billed.
  • Interrupted runs: if a run is aborted or times out, websites that did not finish produce no row and no charge.

The cost of a run is therefore predictable:

100 unique website URLs
↓
Normally 100 rows
↓
Normally 100 billable results

In a completed run, every valid website URL gets a row, including websites that cannot be crawled, for example because they are offline or block automated visits. Those rows are billed too: they tell you which websites failed and whether a retry makes sense (crawl.error.retryable), instead of leaving them silently missing from your results.

Check the Actor's pricing page for the current price per result and available pricing tiers.


πŸ› οΈ Input configuration

Input reference

FieldRequiredDefaultDescription
startUrlsYesβ€”Website URLs processed independently
maxPagesPerStartUrlNo8Maximum pages per start URL
maxDepthNo1Maximum link depth
defaultPhoneCountryNoEmptyOptional fallback country for ambiguous local numbers
enqueueStrategyNosame-hostnameControls which links may be followed
allowedSubdomainsNoβ€”Optional subdomain allowlist for same-domain
earlyStopModeNooffControls adaptive early stopping
maxRequestsPerCrawlNoβ€”Optional global request limit
maxConcurrencyNoAutoscaledMaximum concurrent requests
proxyConfigurationOptionsNoProxy disabledApify proxy configuration

Start URLs

Submit one representative URL per website, preferably the homepage.

Each URL is validated and canonicalized before crawling:

  • malformed and non-HTTP(S) URLs are skipped: they produce no result and are not billed;
  • the #fragment and tracking parameters are removed: utm_* and other campaign parameters ending in _source, _medium, _campaign, _content or _term, ad click IDs such as gclid, fbclid and msclkid, and ref;
  • a bare hostname gets a trailing slash (https://acme.example β†’ https://acme.example/), and trailing slashes on deeper paths are removed;
  • URLs that are identical after these steps are merged into one.

The canonical URL is returned in startUrl. When joining results back to your source data, apply the same normalization or match on hostname.

Page budget and depth

maxPagesPerStartUrl controls the page budget for each start URL.

maxDepth controls link traversal:

  • 0 β€” only the submitted page;
  • 1 β€” also follow eligible links from the submitted page;
  • 2 β€” also follow eligible links discovered at depth one.

The page budget applies regardless of depth.

Crawl scope

StrategyBehavior
same-hostnameSame hostname only
same-domainRegistrable domain and subdomains
same-originSame protocol, hostname, and port
allExternal links that pass the remaining filters

Use same-hostname for most runs.

Use same-domain when relevant content may exist on subdomains. Combine it with allowedSubdomains to restrict which subdomains may be visited.

Use all with care: external pages count against the same page budget, and contacts found on other websites are merged into the record of the submitted URL.

Scope is evaluated against the start page after redirects, so https://acme.example/ redirecting to https://www.acme.example/ crawls www.acme.example under same-hostname.

Early stopping

  • off (default) β€” use the full page budget and depth;
  • balanced β€” after at least 3 pages, stop once 2 emails, phone numbers or WhatsApp numbers are found, or when the last 3–4 pages added nothing new;
  • aggressive β€” after at least 2 pages, stop at the first email, phone number or WhatsApp number, or when the last 2 pages added nothing new.

Social profiles do not count toward these thresholds. Because contact pages are visited first, early stopping usually ends the crawl after the pages that matter most.

Phone country fallback

defaultPhoneCountry has no default value. Set it only when one fallback country is appropriate for ambiguous local numbers.

For example:

{
"defaultPhoneCountry": "US"
}

Country precedence is:

  1. explicit international number, such as +44 20 7946 0958;
  2. reliable website country context, such as a .it domain or a country in the page's structured address data;
  3. defaultPhoneCountry;
  4. no country: a number written in the page text is kept only when a phone label such as "Tel:" sits next to it, and it stays in local form.

For mixed international lists, leave this field empty.

Proxy and advanced controls

A proxy may improve access to websites that restrict direct requests, but it does not guarantee access or render JavaScript-only content.

Advanced controls include:

  • maxRequestsPerCrawl β€” global request cap;
  • maxConcurrency β€” concurrent request limit (autoscaled when unset).

Suggested configurations

GoalPagesDepthEarly stop
Faster bulk run41balanced
Standard enrichment81off
Broader exploration202off

These are starting points. Performance depends on website structure, response time, blocking, proxies, and Actor resources.


πŸ“¦ Output semantics and result delivery

Core fields

FieldMeaning
schemaVersionOutput contract version
statusWebsite-level execution outcome
hasContactsWhether at least one supported contact was found
startUrlCanonical form of the submitted URL (see Start URLs)
finalUrlLegacy field containing the last successfully processed page
contactsConsolidated contact data
crawlCounters, timing, redirects, termination reason, and errors

Use crawl.startPageFinalUrl for the redirected final URL of the original start page. finalUrl is retained for backward compatibility.

Status values

StatusMeaning
completedAt least one page succeeded and the crawl reached a normal configured end
partialSome pages succeeded, but execution ended before normal completion
failedNo page was successfully processed

A failed record can also come from a run that timed out or was aborted before any page of that website succeeded. Check crawl.terminationReason to tell these apart from website-side failures.

Contact presence does not determine status.

{
"status": "completed",
"hasContacts": false
}

This means the website was processed successfully, but no supported contact was found.

Automation example

// The run, not the website, cut these crawls short; a re-run may find more.
const INTERRUPTED = new Set(['run_timed_out', 'user_aborted', 'global_request_limit_reached']);
const interrupted = INTERRUPTED.has(result.crawl.terminationReason);
if (result.status === 'completed' || result.status === 'partial') {
await saveResult(result);
if (result.hasContacts) {
await sendToCrm(result);
}
if (result.status === 'partial' && interrupted) {
await queueForRetry(result.startUrl);
}
} else if (interrupted || result.crawl.error?.retryable) {
await queueForRetry(result.startUrl);
} else {
await queueForManualReview(result);
}

partial records still contain the contacts found before the interruption, so save them instead of discarding them.

Termination reasons and errors

crawl.terminationReason may contain:

  • crawl_completed
  • early_stop
  • page_limit_reached
  • global_request_limit_reached
  • start_url_unreachable
  • start_url_blocked
  • request_failures
  • run_timed_out
  • user_aborted
  • internal_error

A failure may include:

{
"code": "DNS_ERROR",
"message": "The URL hostname could not be resolved.",
"retryable": true,
"url": "https://does-not-resolve.invalid/"
}

Supported error codes include:

  • DNS_ERROR
  • CONNECTION_ERROR
  • TLS_ERROR
  • HTTP_BLOCKED
  • HTTP_ERROR
  • REQUEST_TIMEOUT
  • MAX_RETRIES_EXCEEDED
  • RUN_TIMEOUT
  • INTERNAL_ERROR

Error messages are sanitized before being written to the dataset.

Page counters

pagesVisited = pagesSucceeded
pagesAttempted = pagesSucceeded + pagesFailed

Retries and redirect hops do not inflate these counters.

Ordering, interruptions, and duplicates

Dataset rows follow completion order, not input order. Always match records through startUrl, not row position.

A normal completed run produces one result per unique valid start URL.

Results already written remain available if a run is aborted or times out. Websites still incomplete may not produce a result.

Each start URL is emitted at most once within a single Actor process attempt. In rare cases, a platform restart or migration may produce a duplicate. Deduplicate by startUrl when downstream uniqueness is required.

Compatibility

The current output contract uses schemaVersion: 1.

crawl.stoppedReason is retained for backward compatibility. New integrations should use crawl.terminationReason.

Field names never change within a schema version, but how values are formatted can improve between releases, for example phone numbers in E.164 or social handles in lowercase. Each such change is listed in the Actor's changelog with upgrade notes. If you match or deduplicate on exact contact strings, check it before upgrading.


βœ… Contact extraction and validation

Emails

The Actor extracts emails from the page HTML and from mailto: links, including links with query parameters or multiple recipients.

Candidates are syntax-checked, normalized, deduplicated, and filtered for malformed values, unknown domain endings, placeholder addresses (such as yourname@ or email@gmail.com), asset filenames, and other common false positives. Leftovers from page code or formatting (such as u003e or ** in front of an address) are removed.

The Actor does not perform SMTP verification, mailbox existence checks, deliverability verification, catch-all detection, or contact-owner identification.

Phone numbers

Phone numbers are found in tel: links, structured data (schema.org telephone), and page text, and validated with libphonenumber-js.

  • When the country is known (see Phone country fallback), valid numbers are returned in E.164, for example +14155552671.
  • North American numbers written with the country code but without +, such as 1-800-555-0199, are also returned in E.164.
  • Without a country, a number from the page text is kept only when a phone label such as "Tel:" sits next to it, and it stays in local form. Numbers from tel: links and structured data are kept even without a label.
  • Dates, US ZIP+4 codes, year/number references such as 2016/679, VAT, tax and company-register numbers next to their label (such as VAT No., P.IVA, USt-IdNr., EIN, Tax ID or Company No.), and placeholder numbers such as 9876543210 are rejected. On websites without a known country, product codes and other unlabeled digit runs are rejected too.
  • Equivalent local and international formats of the same number are deduplicated.

WhatsApp numbers

The Actor detects wa.me and api.whatsapp.com links and returns the associated numbers in international format as digits only, without a leading +:

{
"whatsapps": ["14155552671"]
}

Prepend + if you need E.164.

Social profiles

Supported platforms include LinkedIn, Facebook, Instagram, Twitter/X, YouTube, TikTok, GitHub, Pinterest, and Telegram.

Each link is reduced to the profile it belongs to: https is enforced, www., mobile and country subdomains and query parameters are removed, alias hosts are folded (for example x.com β†’ twitter.com), and handles are lowercased. Links to a post or sub-page count as the account (linkedin.com/company/acme/about β†’ linkedin.com/company/acme). Videos, embeds, share buttons, login and legal pages, and tracking URLs are excluded. The same profile linked in different ways therefore appears only once.


⚠️ Limitations and FAQ

Does the Actor execute client-side JavaScript?

No. It uses lightweight HTML parsing with Cheerio.

This keeps bulk runs efficient, but contacts rendered only through client-side JavaScript may be missed.

Why did a website fail with HTTP_BLOCKED?

The website refused automated visits, usually through bot protection such as Cloudflare. The Actor already retries each page several times with a fresh session before giving up.

  • Some websites block every automated visit from a given IP address; others block only part of them, so the same website can succeed in one run and fail in the next.
  • These records have crawl.error.retryable: true. Retrying them in a later run, ideally with Apify Proxy enabled (residential proxies work best against bot protection), often succeeds.
  • A proxy does not guarantee access, and websites that require a JavaScript challenge cannot be crawled by this HTTP-based Actor.

Why did a completed website return no contacts?

status: "completed" with hasContacts: false means the website was processed successfully, but no supported contact was found.

Try increasing the page budget or depth, and check whether the contacts require JavaScript rendering.

Why is one of my URLs missing from the output?

Usually for one of these reasons:

  • the URL was malformed or not HTTP(S), so it was skipped (see the run log);
  • it duplicated another URL after canonicalization, so both share one record;
  • the run was aborted or timed out before that website finished.

Why does startUrl differ from the URL I submitted?

startUrl is the canonical form of your URL: tracking parameters and fragments are removed and trailing slashes are normalized. See Start URLs.

Are failed records billed?

Yes, when the failed record is written to the dataset. A website that produces no dataset record does not create a result charge.

Can the Actor process large website lists?

Yes. Websites are processed in parallel, and each result is written as soon as its website finishes, so you can start working with the first rows while the run continues.

Throughput depends on crawl settings, website speed, blocking, proxies, and Actor resources. Test a representative sample before a very large run.

Can I use the Actor through an API?

Yes. Results are available through:

  • Apify API;
  • JavaScript and Python SDKs;
  • webhooks and schedules;
  • JSON, CSV, Excel, XML, and HTML exports.

πŸ†˜ Support

This Actor is actively maintained. Need help or want to customize it?

⌂ 捒网址