Company Contact Details Scraper - Emails, Phones, Socials avatar

Company Contact Details Scraper - Emails, Phones, Socials

Pricing

$4.00 / 1,000 company with contacts

Go to Apify Store
Company Contact Details Scraper - Emails, Phones, Socials

Company Contact Details Scraper - Emails, Phones, Socials

Company contact details from any list of websites: emails from domains, website phone numbers, social media links and named people per company, CRM-ready. Paste domains or a CSV link, or chain a Google Maps dataset. $0.004 per company, only when contacts are found.

Pricing

$4.00 / 1,000 company with contacts

Rating

0.0

(0)

Developer

Luke Hunter

Luke Hunter

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

2

Monthly active users

2 days ago

Last modified

Share

Company contact details for sales teams, agencies, recruiters and growth teams who build B2B lead lists from a list of company domains. Give it company websites (emails from domains, website phone numbers, website social links) and you get one row per company: emails, phones, social links and the people behind the company - the email addresses and phone numbers the company itself publishes, the names and job titles of the people its team, about and leadership pages list (with an email or LinkedIn profile only where the page links one), each with the page it was found on, plus the company's social profile links, name, postal address (when the site declares one), best contact page and whether its email domain accepts mail. Turn on change mode and run it on a schedule to hear only about companies whose contacts or people changed.

It takes your list the way you have it: paste a spreadsheet column (domains, URLs or email addresses, messy is fine), give it a link to a CSV file, or hand it another Actor's dataset (for example the results of a Google Maps scraper, which have each business's website but usually no email: see "Enrich Google Maps results with emails"), and choose one row per company or one row per contact (CRM-ready: each email, phone and person on its own flat row, ready to import).

Pay-per-result: $0.004 per company with an email, phone or named person found - 1,000 companies with contacts = $4, in either output mode (never per row). Companies where nothing was found, that blocked the request, are unreachable or disallow reading in robots.txt are delivered so you can see why, and are never charged.

Emails from domains: quick start (1 minute)

  1. Open the Input tab and paste company domains, website URLs or email addresses (one per line, or a whole column or CSV row at once).
  2. Click Start, then export the dataset to CSV/Excel/JSON or pull it through the API shown below.
{
"websites": ["plausible.io", "mailerlite.com", "stumptowncoffee.com"],
"maxPagesPerSite": 6,
"maxItems": 100
}

That prefilled example reads three company sites, about six pages each at one page per second, and costs at most $0.012 (only companies with an email, phone or named person are charged). Any page URL works too ("https://www.mailerlite.com/contact"): it is read as the first extra page after the home page.

Paste a column from a spreadsheet

Copy a column (or a whole row, or a messy mix) and paste it into Company websites. Each entry can be a domain, a URL with a path, query or www., an email address (its company domain is read), or several of these on one line separated by commas, semicolons, spaces or tabs:

{
"websites": [
"plausible.io, https://www.mailerlite.com/contact; stumptowncoffee.com",
"jason@basecamp.com",
"info@gmail.com"
]
}

That reads four companies: plausible.io, mailerlite.com (starting at its contact page), stumptowncoffee.com and basecamp.com. info@gmail.com is a free-mail address, not a company, so it is skipped and reported, not an error. Entries are de-duplicated by registrable domain (www.example.co.uk, example.co.uk and sales@example.co.uk are one company). Everything skipped - invalid entries, free-mail addresses (gmail, outlook, hotmail, yahoo, icloud, proton, gmx, web.de, mail.ru, qq, 163, aol, yandex, zoho mail), duplicates - is listed with its reason in the run status and in the INPUT_REPORT record of the run's key-value store. A bad entry never fails the run; only a list with nothing usable does.

Give it a file of domains

Have your list in a spreadsheet? Publish it and pass the link in Website list file URL (websitesFileUrl):

{
"websitesFileUrl": "https://docs.google.com/spreadsheets/d/e/YOUR-SHEET-ID/pub?output=csv",
"outputMode": "contacts",
"maxItems": 500
}

For Google Sheets use File > Share > Publish to web, choose the sheet and Comma-separated values (.csv), and copy the link. A Dropbox shared link or a GitHub Raw link to a .csv or .txt file works too. The file is downloaded once (at most 5 MB and 20,000 lines), every cell is read exactly like an entry above (so a column of domains, a column of emails or a whole CRM export all work; header cells such as "Website" are skipped and reported), and the result is merged with websites. The link must open without signing in. robots.txt is not applied to this download: the file is your own input document, not a website being crawled. It is never fetched from private or internal addresses (IP addresses, localhost and internal names are refused, also after redirects). The companies' own websites are still read politely, with robots.txt checked for every page.

CRM-ready rows: one row per contact

Set Output to contacts and every email, phone and person becomes its own flat row, with the company columns repeated on each, so the dataset imports into a CRM or a spreadsheet as it is:

{
"websites": ["stumptowncoffee.com", "mailerlite.com/contact"],
"outputMode": "contacts"
}

The price does not change: $0.004 once per company with at least one contact, however many rows it produces. See "CRM mode: one row per contact" below for the columns.

Enrich Google Maps results with emails

Google Maps scrapers (for example Google Maps Scraper by Compass, used here only as an example source) return each business's website, phone and address, but usually no email. This Actor takes that run's dataset as its input: it reads the website of every row, opens each business's own website (never Google Maps) for the emails, phones and people it publishes, and puts the fields of the source row next to the result in a source object, so you can join the enriched rows back to the Google Maps rows. Any dataset with a website column works, from any Actor.

Chain it automatically (Integrations)

  1. In Apify Console open the source Actor (or its saved Task), for example your Google Maps Scraper, and go to Integrations.
  2. Add an Actor integration, choose Website Contact Scraper (lukehunter/company-contact-scraper), and set the trigger to Run succeeded.
  3. Use this as the integration's input:
{
"datasetId": "{{resource.defaultDatasetId}}"
}

{{resource.defaultDatasetId}} is filled in by Apify with the dataset of the run that just finished, so every Google Maps run is enriched as soon as it ends. Add the other inputs as usual, for example "maxItems": 1000 (see the note on maxItems below) and "outputMode": "contacts". If you prefer a webhook, use the event ACTOR.RUN.SUCCEEDED on the source Actor, call this Actor's run endpoint, and put the same datasetId in the payload template.

Or do it by hand

  1. Run the Google Maps scraper and wait until it has finished.
  2. Open the run's Storage tab (or the Output tab) and copy the dataset ID, the code at the end of the dataset's address (.../storage/datasets/WkzbQMuFYuamGv3YF). A link to the dataset also works.
  3. Start Website Contact Scraper and paste it into Enrich a dataset: dataset ID (datasetId):
{
"datasetId": "WkzbQMuFYuamGv3YF",
"maxItems": 1000,
"datasetPassthroughFields": ["title", "placeId", "address", "totalScore"]
}

Set maxItems to at least the number of businesses you want enriched (default 100, up to 5,000 companies per run). It is also your cost cap: at most maxItems x $0.004.

What you get

One result row per company (a business with several locations is one company), exactly the rows described below, plus source. An illustrative row (an invented business):

{
"inputUrl": "https://www.corner-bakery-example.co.uk/",
"domain": "corner-bakery-example.co.uk",
"companyName": "Corner Bakery",
"emails": [{ "email": "hello@corner-bakery-example.co.uk", "type": "generic", "sourceUrl": "https://www.corner-bakery-example.co.uk/contact" }],
"phones": [{ "phone": "020 7946 0000", "e164": "+442079460000", "sourceUrl": "https://www.corner-bakery-example.co.uk/contact" }],
"status": "ok",
"source": {
"title": "Corner Bakery",
"placeId": "ChIJexample0000000000000000",
"address": "1 Example Street, London",
"totalScore": 4.6,
"reviewsCount": 212,
"url": "https://www.google.com/maps/search/?api=1&query=Corner%20Bakery&query_place_id=ChIJexample0000000000000000",
"datasetRow": 41,
"sourceRowCount": 3,
"otherRows": ["ChIJexample0000000000000001", "ChIJexample0000000000000002"]
}
}

(The row also has all the usual columns: finalDomain, people, socials, address, emailDomainHasMx, pagesCrawled, scrapedAt, ...)

  • source holds the columns you chose in Dataset: columns to copy into source (datasetPassthroughFields). Leave it empty for automatic: title, placeId, address, city, countryCode, phone, categoryName, totalScore, reviewsCount and url, whichever the row has. At most 15 columns, and 2,000 characters per value (longer text is cut; a list or object is copied as JSON text). Join on source.placeId (or source.datasetRow, the row's number in the source dataset, counting from 0).
  • Chains. When several rows have the same website (a chain's branches), they are one company: one result row, charged once. It keeps the first row's source columns, source.sourceRowCount says how many rows shared the website, and source.otherRows lists the placeId (else the title) of the others, at most 20.
  • In contacts output every row of a company carries the same source object.
  • Company websites you also list in websites or a file are merged in (one company per registrable domain); a company that came only from there has source: null.

Which row gives the website

The website is taken from the column you name in Dataset: website column (datasetWebsiteField), or else from the first of these columns that has a value: website, websiteUrl, web, url, domain, homepage, companyWebsite, company_website, site. A value that is a Google Maps link (a Google Maps Scraper row's url is one), a social network (Facebook, Instagram, LinkedIn, X, YouTube, TikTok, ...) or a marketplace link is not a company website and is skipped, never crawled. Every skipped row, with its row number in the dataset (counting from 0) and the reason (no website, not a company website, invalid value, free-mail address, a duplicate of an earlier row, over the 5,000-company limit), is listed in the INPUT_REPORT record of the run's key-value store, and the run status says how many.

Cost

The same as always: $0.004 per company with an email, phone or named person found, never per source row, never for companies where nothing was found, and nothing for reading the dataset. 1,000 Google Maps rows that point at 700 different websites cost at most $2.80 (700 x $0.004), and only for the ones that publish a contact. Rows without a website are not delivered or charged.

What you can use it for

  • B2B lead lists. Turn a list of company domains from a directory, a conference page or another Actor into contact details you can work with: role mailboxes (info@, sales@), the named people with their job titles (founders, directors, heads of department), phone numbers and LinkedIn/X/Facebook pages.
  • Add emails to Google Maps results. Chain it after a Google Maps scraper (or any Actor whose rows carry a website) and get the emails, phones and people of every business, with the source row's title and place ID attached so the two datasets join. See "Enrich Google Maps results with emails".
  • Enrich a CRM or spreadsheet of domains. Paste the domain (or email) column, or point the Actor at the published CSV, then export the dataset - or switch to one row per contact and import it straight into your CRM - and merge it back on domain. emailDomainHasMx tells you which email domains accept mail before you send anything.
  • Agency prospecting. Find the contact page, impressum or about-page details of local businesses in any country: the crawler reads contact pages in English, German, French, Spanish, Italian, Dutch, Portuguese and more.

Example output

One company row, as the parser produced it from the real stumptowncoffee.com home page (captured 2026-10-04; a live run reads up to six pages, so it may list more, and emailDomainHasMx and scrapedAt are filled in at run time). Its people entry is real: the home page's JSON-LD names the founder.

{
"inputUrl": "www.stumptowncoffee.com",
"domain": "stumptowncoffee.com",
"finalDomain": "stumptowncoffee.com",
"companyName": "Stumptown Coffee",
"emails": [
{ "email": "info@stumptowncoffee.com", "type": "generic", "sourceUrl": "https://www.stumptowncoffee.com/" }
],
"phones": [
{ "phone": "877-711-3385", "e164": null, "sourceUrl": "https://www.stumptowncoffee.com/" }
],
"people": [
{ "name": "Duane Sorenson", "title": "Founder", "email": null, "linkedin": null, "sourceUrl": "https://www.stumptowncoffee.com/" }
],
"contactPageUrl": null,
"socials": {
"linkedin": null,
"facebook": "https://www.facebook.com/stumptowncoffee",
"instagram": "https://www.instagram.com/stumptowncoffee",
"x": "https://twitter.com/stumptowncoffee",
"youtube": "https://www.youtube.com/channel/UC0yk_H5Np-uGvy98UM-jj6w",
"tiktok": "https://www.tiktok.com/@stumptowncoffee",
"pinterest": "https://www.pinterest.com/stumptowncoffee",
"github": null
},
"address": {
"street": "700 SW 5th Ave, 3rd Floor, #400",
"locality": "Portland",
"region": "OR",
"postalCode": "97214",
"country": null
},
"emailDomainHasMx": true,
"pagesCrawled": 1,
"status": "ok",
"statusReason": null,
"changedFields": null,
"scrapedAt": "2026-10-04T08:00:00.000Z"
}

e164 is null here because a US-looking number on a .com is not enough to be sure of the country; the Actor never guesses. contactPageUrl is null because this one-page capture did not show a contact page; in a live run it is the best contact page found (for example https://plausible.io/contact). A company whose home page redirects to another domain shows both: for www.the-ivy.co.uk (real capture) domain is the-ivy.co.uk and finalDomain is ivycollection.com, its address groupdining@theivy-collection.com is kept because it is published in a mailto: link, and its phone is {"phone": "+44 20 7836 4751", "e164": "+442078364751"}.

And what a team page adds. This people list is illustrative (an invented firm, modelled on a typical WordPress "Meet the team" page; the Actor's tests use pages like this one), showing the three optional fields:

{
"people": [
{ "name": "Helen Marsh", "title": "Managing Director", "email": null, "linkedin": "https://www.linkedin.com/in/helen-marsh-acme", "sourceUrl": "https://www.acme-plumbing.co.uk/our-team/" },
{ "name": "Raj Patel", "title": "Head of Operations", "email": "raj.patel@acme-plumbing.co.uk", "linkedin": null, "sourceUrl": "https://www.acme-plumbing.co.uk/our-team/" },
{ "name": "Sophie O’Neill", "title": "Senior Heating Engineer", "email": null, "linkedin": null, "sourceUrl": "https://www.acme-plumbing.co.uk/our-team/" }
]
}

A site that was read but publishes no email, phone or named person is delivered, not charged, and says so (real capture of bluebottlecoffee.com's home page data):

{
"inputUrl": "bluebottlecoffee.com",
"domain": "bluebottlecoffee.com",
"finalDomain": "bluebottlecoffee.com",
"companyName": "Blue Bottle Coffee",
"emails": [],
"phones": [],
"people": [],
"status": "no-contacts-found",
"statusReason": "Read 1 page; none published an email address or phone number, or a named person."
}

(The other fields are delivered too, with empty or null values; they are left out here for space.)

CRM mode: one row per contact

With Output set to contacts ("outputMode": "contacts"), the dataset has one flat row per contact found instead of one row per company. Nothing is nested, every column is always present (null when empty), and the same columns repeat on each row of a company:

ColumnsContents
typeemail, phone, person or none
Contactemail, emailType (generic or other), phone, e164, personName, personTitle, personLinkedin, sourceUrl (the page this contact was found on)
Companydomain, finalDomain, inputUrl, companyName, contactPageUrl
AddressaddressStreet, addressLocality, addressRegion, addressPostalCode, addressCountry (JSON-LD PostalAddress only)
Socialslinkedin, facebook, instagram, x, youtube, tiktok (the company's own profiles as its site links them)
Statusstatus, statusReason, changedFields (change mode, joined with , ), scrapedAt
  • An email row carries the address, a phone row the number and its e164, a person row the name and job title. A person with an email is one row carrying both (type: "person", email filled); that address is not repeated as a separate email row.
  • A company where nothing was found, that blocked the request or is unreachable gives one row of type: "none" carrying status and statusReason, so every company you asked about is accounted for.
  • Rows of a company are delivered together, in the order emails, phones, people.

Real rows from the captured stumptowncoffee.com home page (the company columns are the same on each row; scrapedAt is filled in at run time):

{ "type": "email", "domain": "stumptowncoffee.com", "companyName": "Stumptown Coffee", "email": "info@stumptowncoffee.com", "emailType": "generic", "phone": null, "e164": null, "personName": null, "personTitle": null, "personLinkedin": null, "sourceUrl": "https://www.stumptowncoffee.com/", "finalDomain": "stumptowncoffee.com", "inputUrl": "www.stumptowncoffee.com", "contactPageUrl": null, "addressStreet": "700 SW 5th Ave, 3rd Floor, #400", "addressLocality": "Portland", "addressRegion": "OR", "addressPostalCode": "97214", "addressCountry": null, "linkedin": null, "facebook": "https://www.facebook.com/stumptowncoffee", "instagram": "https://www.instagram.com/stumptowncoffee", "x": "https://twitter.com/stumptowncoffee", "youtube": "https://www.youtube.com/channel/UC0yk_H5Np-uGvy98UM-jj6w", "tiktok": "https://www.tiktok.com/@stumptowncoffee", "status": "ok", "statusReason": null, "changedFields": null, "scrapedAt": "2026-10-04T08:00:00.000Z" }
{ "type": "phone", "domain": "stumptowncoffee.com", "companyName": "Stumptown Coffee", "email": null, "emailType": null, "phone": "877-711-3385", "e164": null, "personName": null, "personTitle": null, "personLinkedin": null, "sourceUrl": "https://www.stumptowncoffee.com/", "...": "company columns as above" }
{ "type": "person", "domain": "stumptowncoffee.com", "companyName": "Stumptown Coffee", "email": null, "emailType": null, "phone": null, "e164": null, "personName": "Duane Sorenson", "personTitle": "Founder", "personLinkedin": null, "sourceUrl": "https://www.stumptowncoffee.com/", "...": "company columns as above" }

What you pay. One charge of $0.004 per company with at least one email, phone or named person - the same as in company mode, whatever the number of rows. The company's other rows are free. none rows are free. Maximum companies (maxItems) counts companies, not rows, so it is still your cost cap. In change mode, a company whose contacts changed delivers all of its rows again (and is charged once); an unchanged company delivers nothing. Without outputMode, nothing changes: you get the one-row-per-company output described below.

How it works

For each website the Actor:

  1. Reads robots.txt of the host (once per host) and checks every path against it before requesting it. A disallowed path is never fetched.
  2. Reads the home page, then the same-site pages most likely to carry contacts and people: links whose path or text says contact, kontakt, contacto, contatti, impressum, mentions légales, about, team, our people, leadership, founders, equipo, équipe, über uns, support, locations and similar, best match first, up to Pages per website. Team pages rank just below contact and impressum pages (when people are on).
  3. If the home page did not link a contact, impressum or team page, it reads one sitemap file - the first Sitemap: line of robots.txt, else /sitemap.xml (and, when that is an index, one child that lists ordinary pages, never posts or products; each at most 2 MB) - only to find contact, impressum, about and team page addresses the home page did not link. Those pages go through the same robots.txt check as any other and count towards Pages per website.
  4. Extracts what the pages literally publish: mailto: links, tel: links, JSON-LD (email, telephone, contactPoint, sameAs, address, employee, founder), addresses written in the page text, simple encodings of an address that is already on the page (name [at] domain [dot] com, @, Cloudflare's email obfuscation), phone numbers in the text that are clearly labelled (after "Tel", "Phone", "Call us", "Telefon", "Téléphone", "Teléfono", or written with a + and a country code) and the people on team pages (see below).
  5. Drops noise: image names (logo@2x.png), example and placeholder addresses, no-reply boxes, Sentry/Wix keys and the addresses of third-party services (for example a "back in stock" widget's sender address), unless the domain is the company's own. Dates, prices, opening hours, VAT and company numbers, order ids and fax numbers are not taken for phones.
  6. Delivers one flat row per company, sorted best first, with the page each contact came from.

Which emails are kept: addresses on the company's own domain (the domain you gave or the one its home page redirects to), and addresses on another domain only when the site publishes them in a mailto: link or JSON-LD (some companies use a different mail domain, or a webmail address). An address on another domain that only appears as plain text or in a form placeholder is dropped. Nothing is ever constructed or guessed: no first.last@ patterns, no info@ just because a domain exists.

It reads politely: about one request per second per site (slower when a site's robots.txt asks for a Crawl-delay, up to 10 seconds), a few sites at a time, an honest self-identifying User-Agent, a time budget per site. It follows at most three redirects per page, each checked before it is requested. A home page may redirect to another domain (greggs.co.uk to greggs.com): that is the company's own redirect and is recorded as finalDomain. A redirect to a social network, a consent or sign-in page, or an internal address is not followed. If a site blocks it, the row says blocked and the Actor stops requesting that site instead of working around it.

People: names and job titles from team pages

With Include the people behind the company on (the default), each row has a people list: {name, title, email, linkedin, sourceUrl} for every person the site publishes as part of the company. Lead lists are mostly about people, and the people are where the company already tells you who they are.

Where people come from - only what the site publishes:

  • JSON-LD: an Organization's employee or founder (a founder named without a title is "Founder"), and a standalone Person that states a jobTitle (not a blog post's author).
  • Team cards on team, about and leadership pages: a repeated block (WordPress columns, Webflow lists, Squarespace summary items, a plain grid of <h3>Name</h3><p>Title</p>) with a name heading and a job-title line. A LinkedIn /in/ link inside a person's card becomes their linkedin; one mailto: link in the card becomes their email.
  • mailto: links whose text is a person's name (then title is null).

What it will not do: it never guesses or builds an email address (no first.last@company.com), takes no personal data from social networks and does not look people up. An email is only an address the page itself links; role mailboxes such as info@ or sales@ are never attached to a person. A card or a list is accepted only when it reads as a team: most of its cards are name plus job title and the titles are job titles. Testimonials (a quote above or below the name, "CEO at Acme"), blog authors and bylines, product or service cards, speaker line-ups, customer logos and footer link lists are left out, and so are names made of ordinary words ("Privacy Policy", "Sales Team"). The aim is precision: you get fewer people than are on the page rather than wrong ones.

A company that publishes only people (no email or phone) is a result: its row is ok and is charged like any company with contacts. Set includePeople to false if you only want emails, phones and social links; people are then not read, people is [], and team pages are not given priority in the crawl.

Input

FieldTypeDefaultDescription
websitesstring[](one of websites, websitesFileUrl or datasetId)Company domains, website URLs or email addresses; an entry may hold several, separated by commas, semicolons, spaces or tabs (a pasted column or CSV row). An email's domain is read; free-mail addresses are skipped. Up to 5,000 companies per run. De-duplicated by registrable domain (the first entry for a domain is read). A URL with a path is read as the first extra page. Skipped entries are reported in the run status and in the INPUT_REPORT record.
websitesFileUrlstring(none)https link to a .csv or .txt file of company websites (Google Sheets "publish to web" CSV, Dropbox, GitHub raw). Downloaded once (5 MB, 20,000 lines max); every cell is read like a websites entry and merged with websites. robots.txt is not applied to this download (your own input file); private and internal addresses are refused.
datasetIdstring(none; one of websites, websitesFileUrl or datasetId)ID of an Apify dataset whose rows carry a website, typically another Actor's run dataset (a Google Maps scraper). Use {{resource.defaultDatasetId}} in an integration. Read with this run's own access, no token needed; up to 20,000 rows. Rows are read like websites entries and merged with them; skipped rows are reported with their row number in INPUT_REPORT.
datasetWebsiteFieldstring(automatic)Column of the dataset that holds the website. Automatic: the first non-empty of website, websiteUrl, web, url, domain, homepage, companyWebsite, company_website, site. Google Maps, social network and marketplace links are never read as a company website.
datasetPassthroughFieldsstring[](automatic)Columns of the source row copied into source on each result row. Automatic: title, placeId, address, city, countryCode, phone, categoryName, totalScore, reviewsCount, url. At most 15 columns, 2,000 characters per value.
outputModestringcompanycompany: one row per company (default). contacts: one flat row per email, phone or person, CRM-ready. Same price in both: $0.004 once per company with contacts.
outputFormatstringfullfull: every field (default). compact: a smaller row for AI agents (see "Use with AI agents (MCP)"). Charging is identical.
maxPagesPerSiteinteger61-20. Pages read per website, home page included.
maxItemsinteger1001-5,000. Hard cap on company rows delivered in the run - your cost cap.
onlyChangedSinceLastRunbooleanfalseChange mode: deliver a company only when its emails, phones, social links or people (name plus job title) changed since the last run.
stateStoreNamestringcompany-contact-scraper-stateName of the store that remembers each company's last contact set (emails, phones, social links, people) in change mode. Letters, numbers and hyphens.
includePageLevelSourcesbooleantruePut the page each email, phone and person was found on in sourceUrl. Off: sourceUrl is null.
includePeoplebooleantrueAlso read the people the site publishes (name, job title, and the email or LinkedIn link the page itself shows) into people. A company with only people is ok and charged. Off: people is [].

Input details

The Store form and the MCP tool schema show a short description for each field (an AI agent sees only the first 500 characters of each); the longer rules are here.

  • websites: Paste anything, one per line or a whole spreadsheet column or CSV row. Each entry may be a domain (plausible.io), a URL with a path, query or www, an email address (its domain is used; free-mail addresses such as gmail, outlook, yahoo, icloud, proton, gmx, web.de, mail.ru, qq, 163 or aol are skipped), or several of these on one line separated by commas, semicolons, spaces or tabs. Entries are de-duplicated by registrable domain (www.example.co.uk and example.co.uk are one company; the first one given is read). Entries that cannot be used are never an error: they are skipped and listed, with the reason, in the run status and in the INPUT_REPORT record of the run's key-value store. The home page is always read first, then the contact, impressum, about, team, leadership, support and locations pages of the same site (up to maxPagesPerSite); a sitemap, if the site has one, only points to contact, impressum, about and team pages the home page did not link. A URL with a path is read as the first extra page. Up to 5,000 companies per run.
  • websitesFileUrl: For example a Google Sheets "Publish to web" CSV link (File > Share > Publish to web > "Comma-separated values (.csv)"), a Dropbox shared link or a GitHub "Raw" link. The file is downloaded once (at most 5 MB and 20,000 lines) and every cell is read exactly like an entry of websites; it is added to the websites you list and de-duplicated with them. The link must open without signing in. The file is your own input list, so robots.txt is not applied to downloading it; it is never requested from private or internal addresses. The websites themselves are still read politely, with robots.txt checked first.
  • datasetId: The dataset is typically the default dataset of another Actor's run, for example a Google Maps scraper (Google Maps Scraper by Compass is one). To chain it automatically, open the source Actor's Integrations tab, add this Actor and set the field to {{resource.defaultDatasetId}}; by hand, copy the dataset ID from the source run's Storage tab. It is read with this run's own access (the account that starts this Actor): you never give a token, and it must be a dataset that account can open. Up to 20,000 rows are read; rows without a website, with a Google Maps, social network or marketplace link instead of a website, with free-mail or invalid values, or that repeat a website are skipped and listed with their row number in INPUT_REPORT. It is added to websites and websitesFileUrl and de-duplicated with them. A link to the dataset in Console or the API is accepted too. source on every result row carries fields of the source row so results can be joined back.
  • datasetWebsiteField: the column name is matched exactly as spelled. Left empty, the first non-empty of website, websiteUrl, web, url, domain, homepage, companyWebsite, company_website, site is used. A value that is a Google Maps, social network (Facebook, Instagram, LinkedIn, X, YouTube, TikTok, ...) or marketplace link is never read as a website, whichever column it is in.
  • datasetPassthroughFields: left empty, title, placeId, address, city, countryCode, phone, categoryName, totalScore, reviewsCount and url are copied, whichever the row has. At most 15 columns and 2,000 characters per value (longer text is cut; a list or object is copied as JSON text). source also always has datasetRow (the row's number in the dataset, counting from 0), sourceRowCount (how many dataset rows had this website; a chain of branches shares one website) and otherRows (placeId or title of the other rows of the same website, at most 20); those three names are reserved. When several rows have the same website, the first row's columns are kept.
  • outputMode: contacts gives one flat row per contact for a CRM or spreadsheet import, with a type of email, phone or person (a person with an email is one row carrying both), the company columns on every row and, for a company with nothing found, one row of type none with the status. The price is the same in both modes: $0.004 once per company with at least one contact, never per row. maxItems counts companies, not rows.
  • maxItems: you are charged $0.004 only for a company row with at least one email, phone or named person. Rows for sites with nothing found, blocked, unreachable or disallowed by robots.txt are delivered (so you can see why) and count toward this cap, but are not charged.
  • maxPagesPerSite: contact-like and team-like pages are read first, in many languages (contact, kontakt, contacto, contatti, impressum, mentions legales, about, team, leadership, equipo, equipe, ueber uns, support, locations, ...). Pages found through a sitemap count too. More pages find more addresses and people on large sites but take longer: about one page per second per site.
  • onlyChangedSinceLastRun: the first run delivers every company and remembers it; later runs deliver only what changed (changedFields says what). A site that is blocked or down on a later run keeps its last known contacts and is not delivered. Unchanged companies are not delivered or charged.
  • stateStoreName: only used in change mode; keeps each company's last contact set (emails, phones, social links, people) across scheduled runs. Leave the default unless several tasks must keep separate histories.
  • includePageLevelSources: on by default; sourceUrl is the page of the company's own site a contact was found on, useful to verify it and to document where it came from. Off: the contacts are the same, sourceUrl is null.
  • includePeople: a company that publishes only people (no email or phone) then counts as a result and is charged $0.004 like any company with contacts. Only what the site publishes is used: no address is ever guessed or built from a name.
CategoryFields
IdentityinputUrl, domain, finalDomain, companyName
Contactsemails (email, type generic or other, sourceUrl), phones (phone, e164, sourceUrl), socials (linkedin, facebook, instagram, x, youtube, tiktok, pinterest, github)
Peoplepeople (name, title, email, linkedin, sourceUrl; at most 50 per company)
Companyaddress (JSON-LD PostalAddress only: street, locality, region, postalCode, country), contactPageUrl (the best contact page found, or null)
QualityemailDomainHasMx (public DNS MX lookup of the first email's domain), pagesCrawled
Source rowsource (only when datasetId was used: the chosen columns of the dataset row, plus datasetRow, sourceRowCount, otherRows; null for a company that came from websites or a file)
Statusstatus (ok, no-contacts-found, blocked, unreachable, robots-disallowed), statusReason, changedFields (change mode: emails, phones, socials, people), scrapedAt

This table is the default company output. With outputMode: "contacts" the rows are flat; see "CRM mode: one row per contact". Missing values are null or empty. contactPageUrl is a page that was read and whose address says contact (/contact, /kontakt, /contacto, /contatti, /get-in-touch, ...), else a contact link the site publishes, else a read imprint page that carries an email or phone; support and about pages are not contact pages. type is generic for a role mailbox from a fixed list (info@, hello@, sales@, support@, contact@, enquiries@, press@, careers@, ...) and other for every other address; it is not inferred beyond that list, so a department box such as groupdining@ is other. e164 is only set when the number carries a + prefix or the site's JSON-LD address or country-code domain names a country the Actor knows the numbering rules of (the UK, Ireland, the US and Canada, Australia, New Zealand and most of Western Europe).

Data protection

This Actor only reads what a company publishes itself on the websites you give it, keeps the source page of every contact and person, and respects robots.txt. It does not log in, does not collect personal social profiles (only profile links a company's own site publishes, and a person's LinkedIn link only when their own team card on that site links it), does not guess or build email addresses from names, and does not look anything up outside the sites you list (the only other lookup is the public DNS MX record of an email's domain). When you give it another Actor's dataset (datasetId), that dataset is read only to get the list of websites and the source columns you chose to copy back; nothing is collected from Google Maps or any other site that dataset came from, and you stay responsible for the lawful basis for that source data as well.

The people field contains named individuals - their names, job titles and, where the site links them, their business email addresses and LinkedIn profile links. That is personal data under the GDPR and similar laws, even though the company published it. You are responsible for having a lawful basis to use the data. Published business contact details can be personal data, especially when they name a person (people, and emails with type: "other"). Depending on where you and your recipients are, that means, for example, a documented legitimate-interest assessment under the GDPR, and compliance with CAN-SPAM, PECR/ePrivacy and similar rules before you send marketing email or call. Honour opt-outs and deletion requests, keep a record of where each contact and person came from (sourceUrl is there for that) and prefer role mailboxes (type: "generic") for cold outreach. If you do not need named people, turn Include the people behind the company off. This is general information, not legal advice.

Track contact changes on a schedule

Change mode turns the Actor into a contact-change feed for a watch list of companies. Every run after the first delivers - and charges for - only companies whose emails, phone numbers, social links or people changed since the previous run.

  1. Save this as a Task:
{
"websites": ["plausible.io", "mailerlite.com", "stumptowncoffee.com"],
"onlyChangedSinceLastRun": true,
"maxItems": 500
}
  1. Add a Schedule (weekly or monthly is plenty for contact data).
  2. The first run is a baseline: it delivers every company (changedFields is ["firstSeen"]) and remembers its contact set. You are charged normally for it.
  3. Later runs deliver only changes; changedFields lists which of emails, phones, socials, people moved. A person is identified by name plus job title: someone joining, leaving or changing title is a change; a newly published email or LinkedIn link for an existing person is not a change of people (a new address still shows up under emails).
  4. Attach a Webhook for ACTOR.RUN.SUCCEEDED or an Integration (Make, Zapier, n8n, Slack, Google Sheets).

Company name, address, contact page, how many pages were read and which page a contact came from are not changes. A site that is blocked or down on a later run keeps its last known contacts and is not delivered; when it comes back with the same contacts it is unchanged. A site that could not be read the first time is delivered once with its status, then stays quiet. A run where nothing changed succeeds with an empty dataset and charges nothing.

State is kept in a named key-value store (stateStoreName), so it survives between runs. Use another name for a separate watch list, or delete the store to start over. State saved by an earlier version of this Actor (before people existed) still loads: the first run after the update records each company's people as its new baseline without delivering or charging anything for it, and people changes are reported from the run after that. With Include the people behind the company off, the people a company had are kept as they were, so switching it off or on is not a change.

Send results to a Google Sheet or CRM

Use outputMode: "contacts" and an Apify Integration, so every run lands in your sheet or CRM without you touching a file:

  1. Save the input as a Task, for example { "websitesFileUrl": "<your published CSV link>", "outputMode": "contacts", "maxItems": 500 }.
  2. In the Task's Integrations tab add Google Sheets (Apify's integration appends or replaces the dataset's rows in a spreadsheet you choose; its default is "Run finished" and the dataset of that run). Because every row is flat, each column becomes a sheet column: domain, companyName, type, email, phone, personName, personTitle, sourceUrl, and so on.
  3. For a CRM (HubSpot, Pipedrive, Salesforce, Zoho, Airtable, Notion, ...) use the Task's Integrations tab to connect Make, Zapier or n8n, or a Webhook to your own endpoint, on ACTOR.RUN.SUCCEEDED. Map email, personName, personTitle, companyName, domain, phone, linkedin and sourceUrl to the CRM's contact or lead fields, and skip rows whose type is none. Or simply download the dataset as CSV from the run's Storage tab and use the CRM's own CSV import.
  4. To send only what changed, add "onlyChangedSinceLastRun": true and a Schedule: each run then delivers all rows of the companies whose contacts changed since the last run, and nothing else.

Rows of one company share domain, so a CRM can group them into one company with several contacts; use type to filter (for example only person rows with an email).

Use it as an API

from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("lukehunter/company-contact-scraper").call(
run_input={"websites": ["plausible.io", "mailerlite.com"], "maxPagesPerSite": 6}
)
for company in client.dataset(run["defaultDatasetId"]).iterate_items():
emails = [e["email"] for e in company["emails"]]
print(company["domain"], company["status"], emails)

Use with AI agents (MCP)

This Actor works as a tool for Claude, ChatGPT, Cursor and other MCP clients through Apify's MCP server. The only input a call needs is websites.

Claude Desktop (claude_desktop_config.json), with your own token from Apify Console > Settings > API & Integrations:

{
"mcpServers": {
"apify-contacts": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://mcp.apify.com?tools=lukehunter/company-contact-scraper",
"--header", "Authorization: Bearer ${APIFY_TOKEN}"
],
"env": { "APIFY_TOKEN": "<YOUR_APIFY_TOKEN>" }
}
}
}

Clients that support OAuth (Claude.ai, VS Code) can connect with just the URL https://mcp.apify.com?tools=lukehunter/company-contact-scraper and sign in to Apify. Never paste a real token into a shared file or chat.

Example. You ask: "Find the contact emails and the CEO for plausible.io and mailerlite.com." The agent calls the tool lukehunter--company-contact-scraper with:

{
"websites": ["plausible.io", "mailerlite.com"],
"outputFormat": "compact",
"maxItems": 10
}

outputFormat: "compact" returns a small row per company (emails as plain addresses, phones, people with titles, socials found) so the result fits an agent's context. It is charged exactly like the default "full" output: $0.004 per company with at least one contact found, nothing for sites with no contacts. Set maxItems to cap spend.

Pricing and cost control

Pay per result, and only for a result with something in it.

$0.004 per company with at least one email, phone or named person - 1,000 companies with contacts = $4.

Companies with contactsCost
3$0.012
100$0.40
1,000$4.00
5,000$20.00

There is no charge for starting a run. One company is one charge, however many emails, phones and people it has - and in contacts output, however many rows it produces. A company that publishes only people (no email or phone) is charged; turn Include the people behind the company off if you do not want people to count as a result. A company row with status no-contacts-found, blocked, unreachable or robots-disallowed is delivered but not charged, and neither is an entry that is not a website, a free-mail address, a duplicate domain, or (in change mode) a company whose contacts did not change. maxItems is a hard upper bound on cost (at most maxItems x $0.004), and the Actor never delivers a charged row your run's maximum-cost setting cannot pay for.

Limitations

  • It finds what companies publish on their own pages, nothing more. If a site shows no email, phone or named person (many show only a contact form), the row says no-contacts-found. It does not guess addresses, look people up, verify that a mailbox exists, or search other sites.
  • Only the pages it reads. By default six pages per site, chosen by their link and path (and, when the home page links none, by one sitemap file). Contacts on pages that are not linked as contact, about, team, legal, support or locations pages, or beyond the page limit, are not found. Raise maxPagesPerSite (up to 20) for big sites.
  • People: precision over recall. Only team cards that read as a team are used (a repeated block with a name heading and a job-title line, on a page about the team, most of them with job-title words), plus JSON-LD and mailto links with a name. A team shown only as photos, as a single sentence ("Jane, our CEO, and Tom, our CTO ..."), in a slider script, in an image, or with job titles that are not job-title words (a bare "Barista" in a grid of otherwise ordinary titles is kept, but a whole page of such titles is not) can be missed, and names in non-Latin scripts and names written in ALL CAPS are not recognised. Team members' own pages are not followed. Expect fewer people than the page shows, not more.
  • No JavaScript rendering. Pages that build their content in the browser (some single-page apps) may show nothing in their HTML; the row then says no-contacts-found. Contacts inside a page's own data (for example a Next.js payload) are found; contacts loaded later by scripts are not.
  • Phone numbers come from tel: links, JSON-LD and clearly labelled text. A number in the text is taken only right after "Tel", "Phone", "Call us", "Telefon", "Téléphone" or "Teléfono" (within about 30 characters), or when written with a + and a real country code; a bare number, a date, a price, an opening time, a VAT or company number, an order id or a fax number is not. e164 is null unless the country is clear.
  • generic / other is a fixed role list, not a judgement: role mailboxes it does not know (groupdining@, beans@) are labelled other.
  • Sites can block automated requests. If a site answers HTTP 401/403/429, shows a bot check or refuses robots.txt, the row is blocked and nothing is charged. This Actor does not use proxies, rotate identities, solve CAPTCHAs, accept cookies or try to get around a block.
  • Your list. Entries are read forgivingly, but the Actor cannot know which cell of a spreadsheet is a website: every cell is tried, and cells that are not websites (names, headers) are skipped and listed in INPUT_REPORT, never an error. Only an email's domain is used (never the address itself), and free-mail providers are skipped. A file must be a plain .csv or .txt reachable without signing in (an .xlsx is not read); at most 5 MB, 20,000 lines and 5,000 different companies per run, the rest is ignored and reported. A URL that itself contains a comma is split at it.
  • Dataset input (datasetId). The dataset must be one the account that starts this Actor can open (the default dataset of a run in the same account, which is what an integration passes); a private dataset of another account cannot be read, and the run then stops with that explanation. At most 20,000 rows are read per run and 5,000 companies per run (maxItems defaults to 100: raise it). It reads the website the row already has: it does not search for the website of a row that has none, and it never requests Google Maps or any other site named in the dataset except the companies' own websites. A business whose "website" is only a Facebook or Instagram page is skipped, not crawled. Column names are matched exactly as spelled in the dataset (use Dataset: website column for other spellings). source is a copy of the columns you chose: it is not checked or corrected.
  • robots.txt is respected for every website it reads, page by page. A disallowed page is not read (reported in statusReason), and a site whose robots.txt disallows its home page is robots-disallowed. The one thing it is not applied to is downloading your own list file (websitesFileUrl), which is an input document, not a crawled site.
  • emailDomainHasMx is a DNS check of the first email's domain, not a mailbox check, and is null when the lookup could not be completed.
  • Social links are the ones a site publishes. A site's footer can link a platform's or a partner's profile instead of its own; share buttons, posts and personal profiles are left out, but the links are only as correct as the site's own.
  • Speed: about one page per second per site and five sites at a time, so 1,000 sites at six pages each (plus robots.txt and, where needed, one sitemap file) take roughly 25-35 minutes. A site that is slow to answer is cut off after a per-site time budget.
  • This is an independent tool, not affiliated with any company whose website it reads. You are responsible for using the collected data in line with the law and the sites' terms.

Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

One API call that runs the Actor and returns the data (replace $APIFY_TOKEN with your token from Apify Console → Settings → Integrations):

curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~company-contact-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"websites": ["plausible.io", "mailerlite.com", "stumptowncoffee.com"], "maxPagesPerSite": 6, "maxItems": 20}'

JavaScript (npm i apify-client):

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~company-contact-scraper').call({
"websites": [
"plausible.io",
"mailerlite.com",
"stumptowncoffee.com"
],
"maxPagesPerSite": 6,
"maxItems": 20
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();

Python (pip install apify-client):

import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~company-contact-scraper').call(run_input={"websites": ["plausible.io", "mailerlite.com", "stumptowncoffee.com"], "maxPagesPerSite": 6, "maxItems": 20})
items = client.dataset(run['defaultDatasetId']).list_items().items

Schedules, webhooks and no-code tools: save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

Only new items each run: set "onlyChangedSinceLastRun": true on a scheduled Task and every run after the first delivers, and charges for, only items it hasn't delivered before for that same input. That makes it a ready-made change feed.

Reliability: an automated test runs this Actor's parser against the live site every day, and failures are fixed fast.

Feedback: an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

FAQ

Does it guess email addresses?

No. Every address in the output is written on a page of the company's own site, and its sourceUrl says which page. A person's email is only filled when the page links an address for them. If a site publishes nothing, you get no-contacts-found, not a pattern like first.last@company.com.

Why was a company not charged?

Only rows with at least one email, phone or named person are charged. no-contacts-found, blocked, unreachable and robots-disallowed rows are delivered free so you can see exactly what happened to each domain; statusReason explains it in plain English.

Why is there an address on a different domain than the website?

Some companies use another domain for mail (a booking address, a group's shared domain) or a webmail address. Such an address is kept only when the site publishes it in a mailto: link or JSON-LD. The same address in plain text on another domain is dropped, because it is usually a partner, a vendor or a form placeholder. Example: groupdining@theivy-collection.com on ivycollection.com.

What is finalDomain?

The registrable domain the site's home page ended up on after redirects. www.greggs.co.uk redirects to www.greggs.com, so domain is greggs.co.uk and finalDomain is greggs.com. Two entries that lead to the same company are delivered once.

What do generic and other mean?

generic is a role mailbox from a fixed list (info@, hello@, sales@, support@, ...). other is every other address, including named people (jason@basecamp.com) and department boxes the list does not know. Prefer generic addresses for cold outreach and treat other ones as personal data.

Can I just give it a domain, or do I need the contact page URL?

A domain is enough: the home page is always read, then the contact-like and team-like pages are found from its links (and, if needed, one sitemap file). If you do give a page URL (https://www.mailerlite.com/contact), that page is read right after the home page.

Can it find the emails of everyone at a company?

No. It collects the names, job titles and contact details a company publishes about itself, and attaches an email to a person only when their team card or mailto link on the site shows one. It does not guess addresses, and it does not read staff directories behind logins, social networks or other sites.

Which people does it list?

The people a company names on its own team, about and leadership pages (and founders and employees declared in its JSON-LD), each with the page they were listed on. Testimonial authors, blog authors, speakers, customers and product names are not people of the company and are left out. See "People: names and job titles from team pages" above.

Will I be charged if a company only lists people?

Yes: a company with at least one email, phone or named person is one charged result, $0.004. If you do not want that, turn Include the people behind the company off.

Can I paste a whole column, or a list with emails and junk in it?

Yes. Paste it into Company websites as it is. Domains, URLs, email addresses and several per line (commas, semicolons, spaces or tabs) are all understood; an email means its company domain (jane@acme.co.uk reads acme.co.uk), free-mail addresses (gmail, outlook, hotmail, yahoo, icloud, proton and so on) are skipped, duplicates are read once, and anything else that is not a website is skipped. What was skipped and why is in the run status and in the INPUT_REPORT record of the key-value store (an entry per line, with where it came from). The run fails only when nothing usable is left.

How do I use a Google Sheet or CSV as the input?

Publish the sheet (File > Share > Publish to web > Comma-separated values (.csv)) and put the link in Website list file URL. A Dropbox shared link or GitHub raw link to a .csv or .txt works too. The file is downloaded once (5 MB and 20,000 lines at most), every cell is read like a pasted entry, and it is merged with anything in Company websites. It has to open without signing in. If the link returns a web page (for example a sign-in page) the run stops with a message saying so, before anything is read or charged.

Does it respect robots.txt when it downloads my file?

No, and that is deliberate: the file is the input you gave it, not a website being crawled, so robots.txt does not apply to that one download (it is made with the same honest User-Agent, once, and never to private or internal addresses). Every company website it then reads is checked against that site's robots.txt page by page.

What is the difference between company and contacts output?

company (default) is one row per company with emails, phones and people as lists. contacts is one flat row per email, phone or person with the company columns repeated, for importing into a CRM or spreadsheet. A person with an email is one row carrying both. A company with nothing found is one row of type none with the status. The data and the price are the same.

Am I charged per contact row in contacts mode?

No. $0.004 once per company with at least one contact, however many rows it has. Rows of a company beyond the first are free, none rows are free, and a restarted run never charges a company twice.

How do I only get companies whose contacts changed?

Set onlyChangedSinceLastRun to true and put the Actor on a schedule - see "Track contact changes on a schedule" above.

Do I have to give it an API token to read the Google Maps dataset?

No. Every Actor run on Apify is started with its own access token for the account that started it, and this Actor reads the dataset with that. It can therefore open the datasets of your own account (the default dataset of your Google Maps run, which is what {{resource.defaultDatasetId}} is). It cannot open a private dataset of someone else's account. It only reads the dataset: it never writes to it, creates storages, or touches anything else in your account.

Does it work with other Actors than Google Maps Scraper?

Yes, with any dataset that has a website column: another maps or business-directory scraper, a company list, a dataset you built yourself. Name the column in Dataset: website column if it is not one of website, websiteUrl, web, url, domain, homepage, companyWebsite, company_website, site, and the columns you want back in Dataset: columns to copy into source.

Does it scrape Google Maps?

No. It reads the dataset a Google Maps scraper already produced through the Apify API, and then opens only each business's own website (checking its robots.txt, one page per second, with its honest User-Agent). A Google Maps url, a Facebook or Instagram link and a marketplace link are never opened.

Why is my result smaller than the dataset?

Chains share one company (one row, sourceRowCount says how many), rows without a website, with only a social network or Google Maps link, with an invalid or free-mail value are skipped, and maxItems (default 100) caps the companies. The run status gives the counts and the INPUT_REPORT record lists each skipped row with its row number (counting from 0, like the dataset API's offset) and why.

How do I join the results back to the Google Maps rows?

On source.placeId (the row's Google place ID, copied automatically when the row has one), or on source.datasetRow. For a chain, source.otherRows lists the place IDs of the other branches, which share the first branch's result row.

What is source in change mode?

Nothing that counts as a change. Change mode compares emails, phones, social links and people only, so a changed rating, address or title in the source dataset never makes a company deliver again (and never costs anything); the row you do get carries the current source.

What does it cost to enrich a Google Maps run?

$0.004 per company with an email, phone or named person found, exactly as for any list; nothing for reading the dataset, for source rows without a website, or for companies where nothing was found.

Other data tools from the same developer, built to the same standard: official or public sources, hard cost caps, and honest documentation of limits.

  • Domain Email Provider Lookup: the email provider (Google Workspace, Microsoft 365, Zoho and more), MX, SPF and DMARC setup of any domain list, with provider-change alerts.
  • French Company Leads: French companies (entreprises) from the official Sirene-based API, filtered by NAF code, department, headcount and creation date, with SIREN/SIRET, officers and new-company alerts.
  • Poland KRS Company Lookup: Polish companies from the official KRS API by KRS number: NIP, REGON, address, share capital, PKD and the board as published, with optional VAT status.
  • Impressum Scraper: legal entity data from the imprint of any company website in Germany, Austria and Switzerland: legal name and form, managing directors, HRB/HRA or FN register, VAT id and address, with a changed-data-only mode for scheduled runs.
  • Website Tech Stack Detector: the technologies a website runs (CMS, ecommerce platform, analytics, ad pixels, chat, payments, CDN, frameworks) from its home page, with the evidence for each match and a changed-technologies-only mode for scheduled runs.
  • Google Maps Email Extractor & Lead Enricher: emails, phones, social links, named people and the tech stack of every business in your Google Maps Scraper results, one enriched row per business, with filters such as only Shopify stores with an email.
  • Federal Register Rule Effective-Date Radar: final federal rules about to take effect, with citations, dockets and urgency.
  • UK Tenders Monitor: new UK public-sector tenders from Contracts Finder and Find a Tender, filtered by CPV code, value, region and buyer, with Slack, Discord and Telegram alerts.