Find the Decision Makers at Your Target Accounts by Job Title
Turn a list of company domains into a CRM-ready CSV of people by job title, with LinkedIn URLs, labeled emails and a company check on every row.
The actor referenced in this article. Pay only for results delivered.
You have a list of accounts. Marketing picked them, or sales did, or they came out of a fit model. What you do not have is the people: who runs marketing at each one, who the CEO is, and how to reach them. Filling that in by hand is twenty minutes of LinkedIn searching per account, and the database tools that do it for you sell a seat and a contract.
Try it live: B2B Leads Finder, Like Apollo. Pay per result delivered. Failed and empty results are never charged.
The B2B Leads Finder takes up to five company domains per run, searches the public web for LinkedIn profiles of people at each one, labels every person against the job titles you name, and adds a business email with a plain statement of how it was made. This guide turns that into an account list for a CRM, and it is honest about the rows you should not send to.
What you get and who it is for
| Field | What it holds |
|---|---|
company, domain | The account, as you named it |
name, job_title, linkedin_url | The person and their LinkedIn headline, from search results |
title_match | true when the headline matches one of your job titles |
email | A business email at the company’s domain, when one could be built |
email_confidence | found (published on the company site), pattern_matched (built from a format the site revealed) or guessed (built from the most common format) |
email_note | The same thing in words, on built addresses |
email_source_url | The page a found address came from |
phone, phone_source | The company switchboard from its own site, never a direct line |
source | linkedin-serp or website-scrape |
charged | true on person rows, false on free company inboxes like info@ |
This is for account-based sales and marketing teams who already know which companies they want, agencies building contact lists for a client’s named accounts, and founders doing their own outbound to a short list. It is not a database export: every row is found live, during your run, from public search results and the company’s own website. If the account list itself does not exist yet, US business listings by category and city are one way to build one.
The input
{
"companies": ["stripe.com", "notion.so", "linear.app", "vercel.com", "supabase.com"],
"jobTitles": ["CEO", "Head of Marketing", "VP Marketing"],
"maxLeadsPerCompany": 5,
"scrapeWebsite": true,
"monitorMode": false
}
companiesis the only required field. Give domains, not names: an email is only built when the actor has a domain to build it on. Five companies per run is the limit, and any extra are skipped and never charged.jobTitlessets who you are after. The first three are searched by name, and every lead is then labeledtitle_matchtrue or false against the full list, with matches delivered first. Leave it empty to get every role the search turns up.maxLeadsPerCompanydefaults to 10. A run returns at most 25 leads across all companies, so with five accounts at 10 each, the first two or three can use up the cap. Set it to 5 when you send five companies, so each account gets a share.scrapeWebsiteis on by default. The actor reads the home page and the team, contact and about pages it links to (7 pages and 20 seconds at most per company). Addresses published there come back asfound, and they teach the actor the company’s real email format.proxycan stay as it is. The residential default is included in the price.
What the output looks like
These rows are from our run yisuDxWL4YOiM3Z5X on October 1, 2026, with stripe.com and notion.so, job titles CEO and Head of Marketing, and maxLeadsPerCompany at 10. Names, emails and profile URLs are masked here.
[
{
"company": "Stripe",
"domain": "stripe.com",
"name": "P*** C***",
"job_title": "Stripe CEO",
"title_match": true,
"linkedin_url": "https://www.linkedin.com/in/***",
"email": "p***.c***@stripe.com",
"email_confidence": "guessed",
"email_pattern": "first.last",
"email_note": "Guess: first.last is the most common format and stripe.com publishes no staff address to learn from. Mailbox not verified, check before sending.",
"phone": "+1 888 926 2289",
"phone_source": "company-website",
"source": "linkedin-serp",
"charged": true
},
{
"company": "Notion",
"domain": "notion.so",
"name": "S*** N***",
"job_title": "CEO at Colorful Notion",
"title_match": true,
"linkedin_url": "https://www.linkedin.com/in/***",
"source": "linkedin-serp",
"charged": true
},
{
"company": "Notion",
"domain": "notion.so",
"name": "E*** R***",
"job_title": "Notion",
"title_match": false,
"linkedin_url": "https://www.linkedin.com/in/***",
"source": "linkedin-serp",
"charged": true
}
]
The run hit the 25-lead cap: 13 Stripe rows (three more than the 10 asked for, because a search page already fetched is delivered in full) and 12 Notion rows. The dataset held 27 items, the extra two being a free summary row and a free info row. Read the rows closely, because this is what an account list built from web search really looks like:
- Every Stripe email was a guess. 12 of 13 Stripe rows carried a
first.lastaddress markedguessed, because Stripe publishes no staff addresses for the actor to learn from. The 13th had no email at all. - No Notion row had an email.
notion.sohas no MX records, so the actor built no addresses there. If a company’s staff use a different mail domain from its website, give that domain instead. - Three of the six CEO matches were the wrong company. At Notion, all three rows marked
title_match: trueran other businesses: Colorful Notion, Foster That Notion and notion innovators corp. The actor had already rejected 12 wrong-company candidates in this run (the summary row counts them incandidates_rejected_wrong_company), but a short company name lets some through. - Headlines are not job titles.
job_titleis the LinkedIn headline from the search snippet. Five of 25 were just the company name, and five were cut off with.... A person whose headline is only the company name getstitle_match: falsewhatever their real title is. - Nobody matched Head of Marketing at either company in this run, so a title you ask for is not a title you are promised.
For a different picture, our run SyXLLtsW1xWUJAs6V on September 30 with nwapllc.com, a small accounting firm, returned five people from the firm’s own team page, each with email_confidence: "found" and the page in email_source_url. Small companies that list their staff are where the emails are real. Large companies rarely publish staff addresses, so expect guesses there.
Run it in Apify Console
- Open https://apify.com/themineworks/b2b-leads-finder and click Try for free.
- In Target companies, enter up to five domains, one per line, most important first.
- In Job titles to target, enter the roles you want, most important first, since only the first three are searched by name.
- Set Max leads per company to 5 for a five-company run.
- Leave Scrape company website for emails on, then click Start.
- Open the Output tab when the run finishes. The last two rows are the run summary and an info row: check
companies_skipped,engine_refusedandcandidates_rejected_wrong_companyin the summary. - Click Export and choose CSV. Before importing it anywhere, drop the two report rows and the rows with
chargedset tofalse(company inboxes).
Run it from Python
pip install apify-client
Put your accounts in target_accounts.csv with a domain column. The script sends them five at a time, then adds two columns for whoever reviews the list: email_action from the email label, and company_check for headlines that may belong to another business.
import csv
import re
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
JOB_TITLES = ["CEO", "Head of Marketing", "VP Marketing"]
with open("target_accounts.csv", newline="", encoding="utf-8") as f:
domains = [row["domain"].strip() for row in csv.DictReader(f) if row["domain"].strip()]
CONNECTORS = {"", "at", "@", "of", "for", "in", "|"}
LEGAL = re.compile(r"\b(inc|corp|llc|ltd|gmbh|pvt)\b")
def company_check(headline, company):
h = re.sub(r"[]", "", headline or "").lower()
c = company.lower()
i = h.find(c)
if i < 0:
return "company not in headline"
before = h[:i].split()
word = before[-1] if before else ""
if word not in CONNECTORS:
return f"another company? '{word} {c}'"
if LEGAL.search(h[i:].split("|")[0]):
return "another company? different legal name"
return ""
EMAIL_ACTION = {
"found": "ok, published on the company site",
"pattern_matched": "verify, built from the company's format",
"guessed": "verify before sending, format guessed",
}
leads, summaries = [], []
for start in range(0, len(domains), 5):
batch = domains[start:start + 5]
run = client.actor("themineworks/b2b-leads-finder").call(run_input={
"companies": batch,
"jobTitles": JOB_TITLES,
"maxLeadsPerCompany": 5,
"scrapeWebsite": True,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item.get("_type") == "summary":
summaries.append((batch, item))
if item.get("_type") or not item.get("charged"):
continue # report rows and free company inboxes
item["email_action"] = EMAIL_ACTION.get(item.get("email_confidence"), "no email")
item["company_check"] = (
company_check(item.get("job_title"), item["company"])
if item.get("source") == "linkedin-serp" else "")
leads.append(item)
leads.sort(key=lambda r: (r.get("domain", ""), not r.get("title_match"), bool(r["company_check"])))
fields = ["company", "domain", "name", "job_title", "title_match", "linkedin_url",
"email", "email_confidence", "email_action", "company_check",
"source", "email_source_url", "phone"]
with open("decision_makers.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
writer.writeheader()
writer.writerows(leads)
for batch, s in summaries:
if s.get("engine_refused") or s.get("companies_skipped"):
print(f"Run again later: {batch} (search refused or companies skipped)")
ready = [r for r in leads if r.get("title_match") and not r["company_check"]]
print(f"{len(leads)} leads, {len(ready)} title matches with a clean company check")
On the 25 rows from our October 1 run, company_check flagged six. It caught all three wrong-company CEO matches at Notion, a consultant whose headline says he helps businesses with Notion, one Stripe row whose cut-off headline never names Stripe, and one row that is a real Notion cofounder with the headline “Building Notion”. That last one is why the column is a flag for a person to look at, not a filter that deletes rows. Opening the profile settles each one.
For the CRM import, map name, job_title, linkedin_url and email to contact fields, and keep email_confidence as a custom field so your sequencer can treat a found address differently from a guessed one. Send the guessed and pattern_matched addresses through a verifier first. Our bulk email verification guide shows what a check can and cannot tell you.
Automate the handoff
If your account list lives in a sheet or a CRM view, run the actor from Make, Zapier or n8n: read five domains, start a run, wait for it to finish, and write back only the rows with a charged value of true and no _type. Keep the company_check logic in a code step, or skip it and route every title_match: true row to a review queue before it reaches a sequence.
When the account list is settled and you want to know who joins those companies later, that is a different job with a different setting. See Track New People at Your Target Accounts Every Week.
What it costs
From October 13, 2026 the price is $6.10 per 1,000 leads on the Bronze plan, $4.95 on Silver and $4.00 on Gold and above, plus a flat $0.005 per run. Until October 12 it is $3.75, $3.50 and $3.00 per 1,000 leads with no run fee.
You pay for person rows only. The summary and info rows, company inboxes, companies past the fifth, wrong-company candidates the actor rejects, and searches an engine refuses are never charged.
- Our October 1 run: 25 leads. From October 13 that is $0.1525 plus $0.005, about $0.16 on Bronze, or $0.105 on Gold.
- A 40-account list: eight runs of five companies at up to 25 leads each, so at most 200 leads. On Bronze that is $1.22 plus $0.04 in run fees, $1.26 in all. On Silver, $0.99 plus $0.04.
Count the useful rows, not the total. In our run, three of 25 rows were CEO matches that passed the company check, and none had a verified email. Budget for review and verification time as well as for leads.
Limits worth knowing
- Five companies and 25 leads per run. Longer lists need batches, and a big company can fill its share with people outside your target titles.
- Most emails at large companies are guesses. The actor confirms the domain accepts mail, but cannot test the mailbox, because that needs a port that cloud platforms block. Treat
guessedas a starting point. - Generic names pull in other businesses. Notion brought in Colorful Notion and three others. Use the company check, and prefer a distinctive domain.
- Headlines are not titles. A CEO whose headline is just the company name gets
title_match: false. Scan the unmatched rows too. - Search can be refused. In our September 30 run, the search engine refused requests for
stripe.com, so it returned no leads and was skipped at no charge. The summary row says so inengine_refused; run those companies again later. - Phones are the company switchboard. The same number from the company’s site is attached to every row from that company.
- The data is personal. Names, titles and work emails are personal data under GDPR, CCPA and similar laws, and cold email has its own rules. You are responsible for how you use the list.
Related
Explore the scraper referenced in this article: inputs, outputs, and pricing, then run it on Apify.
Frequently asked questions
Are the emails verified? +
No. The actor checks that the domain accepts mail before it builds an address, but it does not test the mailbox. Each row says how the address was made in email_confidence, and guessed addresses carry a note that the mailbox is not verified. Run them through a verifier before you send.
How many accounts can I put in one run? +
Five companies and 25 leads per run. For a longer account list, split it into batches of five, as the script in this guide does, and set maxLeadsPerCompany to 5 so the run cap does not run out before the last company.
Why did some rows come back with no email? +
An address is only built when the input gives a domain and that domain has mail (MX) records. In our run, notion.so has no MX records, so none of the 12 Notion rows carried an email. One Stripe row, a profile showing only a first name and an initial, also came back without an email.
Why did I get people from a different company? +
Profiles are found through web search, and a short or common name like Notion matches other businesses with Notion in their name. The actor rejects many of these before charging, but some get through. The company check in the script flags them for a quick look.
Track New People at Your Target Accounts Every Week
Schedule B2B Leads Finder in monitor mode so each weekly run returns only people you have not been sent before, and never bills the same person twice.
B2B Leads Finder: Business Emails and LinkedIn Profiles Without Apollo or ZoomInfo
Find business emails, LinkedIn profiles, and job titles for decision-makers at target companies. No Apollo, ZoomInfo, or Lusha API key required. $0.003 per lead.
Maps Leads: Verified Email Extraction from Google Maps Business Listings
How to pull B2B leads from Google Maps with MX-verified emails, past the official API's 120-result cap, and pay only for contactable businesses.
Realtor.com Scraper: Property and Agent Data Without the MLS Paywall
Scrape Realtor.com listings in Python: price, beds, baths, county, and the listing agent + brokerage office on every record. No login, no API key.