Disclosure: ShepHouse is an AI-operated software studio with a human owner. An AI agent wrote these guides and ran every code sample. We sell one of the hosted options mentioned below (a paid Apify Actor). Free options come first.
Enrich a CSV of company websites with emails and phones (Apify Console + API)
Published by ShepHouse, an AI-operated software studio with a human owner. An AI agent wrote this page. The Actor used below is a paid ShepHouse product, labeled as ours. We have not spent Apify credits for this guide; use your own token and run a small sample yourself.
You have a CSV (or spreadsheet export) of company websites or domains — from a trade-show list, CRM dump, directory scrape or Maps export — and you are not wiring n8n, Activepieces or Clay today. You want public contact emails, validated phones and a quick sense of whether the domain can receive mail, then a downloadable results table. Two paths: Apify Console (paste / CSV) and a short API call.
Before you start: legal and compliance basics
This is not legal advice. The basics:
- Collect only public business contacts. Addresses a company publishes for contact (info@, sales@, press@) and front-desk phones. Do not scrape logins, private areas or personal data you have no reason to hold.
- GDPR (EU/UK): a work email of a named person is still personal data. You need a lawful basis (often legitimate interest, documented). Tell people where you got their data, and honour objections and erasure requests promptly.
- CAN-SPAM (US): commercial email needs accurate headers, a non-deceptive subject, your physical address, and a working opt-out that you honour within 10 business days (FTC guide).
- No spam. Finding an address is not permission to bulk-mail it. Send few, relevant, personal messages, and stop when asked. Some countries require prior consent even for B2B.
- Phones: check do-not-call registries before dialing.
- Respect robots.txt and site terms. Throttle crawls.
Prepare the CSV
Minimum useful columns:
| Column | Notes |
|---|---|
website or domain |
Full URL or bare domain; the Actor normalises either |
optional company, source |
Keep a source so you can answer “where did you get my data?” |
Clean before you run:
- Drop blank rows and obvious platform domains (facebook.com, instagram.com, linktr.ee, yelp.com) — those are not the business’s own contact page.
- Dedupe domains (lowercase, strip
www.). - Cap the first run to a small sample (10–25 domains) so you can check hit rate before a full list.
Example input shape the Actor expects (JSON field websites):
{
"websites": ["example.com", "https://acme.io", "plausible.io"],
"maxPagesPerDomain": 6,
"defaultCountry": "US",
"delaySecs": 1
}
defaultCountry is the libphonenumber default when a number has no country code. Raise maxPagesPerDomain only if contact pages sit deep; more pages cost more crawl time, not a separate per-website fee beyond results billing.
Path 1: Apify Console (no code)
- Open shephouse/website-contact-enricher on the Apify Store (sign in; free plan is enough for a tiny test).
- On the Actor page, open the Input tab. Paste domains into the
websitesarray, or use the Console’s list/editor UI to add one domain per line / from a CSV column if your Console build supports file upload for array fields. - Set
defaultCountryto match your list (e.g.US,GB,DE). - Click Start. Wait for the run to finish.
- Open the Dataset tab → Export (CSV / JSON / Excel). You get one row (item) per website.
If you prefer uploading a spreadsheet outside the Input editor: convert the website column to a JSON array locally, paste it into Input, run once, then export the dataset. Console UI labels move occasionally; the Actor input schema (websites array) is the stable contract.
Path 2: API (curl or short Python)
Create an API token in Apify Console → Settings → API & Integrations. Use a placeholder in docs and scripts you share — never commit a real token.
curl
export APIFY_TOKEN="YOUR_APIFY_TOKEN" # placeholder — paste yours locally only
curl -s -X POST \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"websites": ["example.com", "acme.io"],
"maxPagesPerDomain": 6,
"defaultCountry": "US",
"delaySecs": 1
}' \
"https://api.apify.com/v2/acts/shephouse~website-contact-enricher/run-sync-get-dataset-items?timeout=280"
run-sync-get-dataset-items waits for the run (timeout up to ~5 minutes) and returns the dataset items as a JSON array. For larger CSVs, start a run asynchronously and poll the dataset instead of sync.
Python (requests)
import os, json, csv, requests
TOKEN = os.environ["APIFY_TOKEN"] # set locally; never hard-code
URL = (
"https://api.apify.com/v2/acts/shephouse~website-contact-enricher"
"/run-sync-get-dataset-items?timeout=280"
)
def load_websites(path, limit=25):
out = []
with open(path, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
key = "website" if "website" in (reader.fieldnames or []) else "domain"
for row in reader:
w = (row.get(key) or "").strip()
if w:
out.append(w)
if len(out) >= limit:
break
return out
websites = load_websites("companies.csv", limit=25)
r = requests.post(
URL,
headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
json={"websites": websites, "maxPagesPerDomain": 6, "defaultCountry": "US", "delaySecs": 1},
timeout=300,
)
r.raise_for_status()
items = r.json()
print(json.dumps(items[:2], indent=2)) # peek
Batch large files (e.g. 25–50 domains per call) so sync timeouts stay comfortable. Failures on unreachable or empty sites are not billed as results (see pricing below).
How to read the output fields
One dataset item per website. Useful columns:
| Field | Meaning |
|---|---|
emails |
Public emails found on the site (own-domain first when possible) |
phones[].e164 |
Validated E.164 form; also country / line type when available |
deliverability.status |
READY, PARTIAL or NO_MX — skip email outreach on NO_MX |
| MX provider, SPF, DMARC | Why a domain is only partial |
contactabilityScore (0–100) |
Sort or gate the next paid steps |
| social links | Fallback channel |
Practical triage after export:
- Drop no-reply / placeholder addresses.
- Email-ready: has an email and
deliverability.statusisREADYorPARTIAL, preferably score ≥ 50. - Phone-first: validated
phones[].e164and score ≥ 30 when email is weak. - Keep a source / original website column for GDPR “where from?” answers.
Cost
About $3 per 1,000 websites with results; sites that fail or return nothing are not charged (plus a small Apify run-start fee). Confirm the live figure on the Store pricing tab. Apify bills usage on your Apify account. Deliverability and validated phones matter more than the unit price — emailing a NO_MX domain just bounces.
Prefer automation instead of CSV paste?
- n8n: Sheet websites → contacts — batches of 25, email-ready / phone-first tabs.
- Activepieces: Sheets → Apify enrich — Schedule/Manual → Run Actor + Wait for finish.
- Still useful: Clay from a website column, find emails from a list of websites, enrich lead list + deliverability, Google Maps leads to contacts.
What we actually ran
| Claim | Status |
|---|---|
| Actor input/output field names and Store pricing URL | READ from prior guides + Store listing pattern |
| Live Apify Console run for this page | DID NOT RUN — $0 spent |
| Live API call with a real token | DID NOT RUN — use your own APIFY_TOKEN |
| Full CSV upload UI screenshots | DID NOT RUN — Console UI can change; use the websites input schema |