Enrich a HubSpot company list with contact emails from their websites
HubSpot's built-in enrichment fills company fields but does not promise a contact email for every domain. If your companies have a domain and you want the business email each one publishes on its own site, you can run the list through the Actor and import the result into a custom company property. This route needs no paid HubSpot tier beyond importing.
Use your own token. Create an API token in Apify under Settings, Integrations, and put it in the tool's secret or credential store. Never paste it into a shared sheet or a public workflow. The examples say YOUR_APIFY_TOKEN; replace it. Sending the token in the Authorization: Bearer header is safer than putting it in the URL, because URLs end up in logs.
Steps
- Create a property: Settings, Properties, Company properties, new single-line text property "Website email".
- Export: in Companies, select your companies (or a saved view), Export, CSV, including the columns
Record IDandCompany domain name. - Run the script below on the export.
- Import: Import, from a file, Companies, one file, one object; map
Record IDto Record ID andWebsite emailto your new property. Matching on Record ID updates the existing companies instead of creating duplicates.
The script
import csv, os, requests
URL = "https://api.apify.com/v2/acts/datagleaner~website-contact-details-scraper/run-sync-get-dataset-items"
with open("hubspot-export.csv", newline="", encoding="utf-8-sig") as f:
rows = [r for r in csv.DictReader(f) if r.get("Company domain name")]
resp = requests.post(
URL,
params={"maxTotalChargeUsd": 2, "fields": "domain,bestEmail"},
headers={"Authorization": "Bearer " + os.environ["APIFY_TOKEN"]}, # your own token
json={"websites": [r["Company domain name"] for r in rows], "maxPagesPerSite": 4},
timeout=300,
)
resp.raise_for_status()
emails = {i["domain"]: i["bestEmail"] for i in resp.json() if i["bestEmail"]}
with open("hubspot-import.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["Record ID", "Website email"])
for r in rows:
d = r["Company domain name"].lower().removeprefix("www.")
if d in emails:
w.writerow([r["Record ID"], emails[d]])
Only companies where an email was found are written to the import file, so companies without one keep an empty property. HubSpot stores domains without https:// or www., which is the same form the Actor returns in domain, so the lookup matches directly.
Do not mass-email the result blindly
The address is whatever the company publishes: often info@ or sales@, sometimes a named person. Use role addresses for cold outreach, include an opt-out and check the rules for your target country (CAN-SPAM in the US, PECR and GDPR in the UK and EU). Import into a property rather than as marketing contacts, and let your normal consent process decide who gets mail.
API route
If you prefer no CSV step, a HubSpot workflow custom-code action (requires Operations Hub Professional) can make the same HTTP call per new company and write the result to the property. That route needs a paid HubSpot tier and we did not test it; the CSV route above works on any plan that allows imports.
What comes back
A JSON array with one object per site. Output order is not guaranteed to match input order (in our test, sitemaps.org came back before mozilla.org), so always match rows on domain, never on position. A real response for three sites, with fields=domain,bestEmail,status,failureReason:
[
{"domain": "basecamp.com", "bestEmail": "jason@basecamp.com", "status": "ok", "failureReason": null},
{"domain": "sitemaps.org", "bestEmail": null, "status": "noContacts", "failureReason": "no_contact_found"},
{"domain": "mozilla.org", "bestEmail": "trademark-permissions@mozilla.com", "status": "ok", "failureReason": null}
]
Without fields each object is large (around 60 keys: every email with the page it was found on and a kind, phones, 14 social networks, contact-form URLs, pages crawled). Ask only for what you need. A site that publishes no email still returns a row, with bestEmail: null. Junk input such as "not a site" returns a row with status: "invalidUrl" instead of failing the call.
What it costs
The Actor charges $0.002 for each site where an email is found and nothing for sites without one. 1,000 sites with 600 hits is about $1.20, and a 4-site test is under a cent. Apify's free plan includes monthly credit. The maxTotalChargeUsd query parameter in the examples is a hard cap on what one call can spend: the run stops when it is reached, so a typo cannot cost more than you set. Expect an email on roughly half to two-thirds of small-business sites and less on large brands; in our five-site test three sites gave one.
Try it: Website Contact Details Scraper on Apify. $0.002 per site with an email, nothing for sites without. The Apify free plan includes monthly credit, so a test run costs nothing out of pocket.
Related guides
- Find emails from a website list in Clay with an HTTP API column
- Clay: find a contact email per row with the Run Apify Actor action
- Find emails from a list of websites in n8n with Apify
- Find emails from a website list in Make (formerly Integromat)
- Find emails from a company website in Zapier with Webhooks
- Find emails from a website list in Google Sheets (Apps Script)