Open menu

API Workflow: Find a CEO Name From a Business Name and Domain

API Workflow: Find a CEO Name From a Business Name and Domain

You’ve got a spreadsheet of company names. Your boss wants the CEO for each one. And you’ve got 400 rows.

I’ve been there, and I’m going to be honest with you: I did this by hand once, back in 2021, for a partner event. Company name into Google, click around LinkedIn, copy the name, paste it back. Four hundred times. It took me two days, and I still got a chunk of them wrong, because roughly 15% of the “CEOs” I found had left the company months earlier.

So let’s not do that. Here’s the API workflow I use now to find a CEO name from a company name, and it runs in about fifteen minutes.

The workflow in one line

Three calls, chained. That’s the whole thing:

Company name → resolve the domain → search for the CEO at that company → enrich the person

Why does the domain step matter? Because company names are messy and domains are unique. “Apple” could be a hundred things. But apple.com is exactly one company, and every downstream call gets more accurate once you’ve pinned it down.

You’ll use three endpoints:

Each step feeds the next. Name gives you domain. Domain gives you the executive. Executive gives you the full contact record. And at the end of the chain, every row in your spreadsheet carries a CEO name, a verified email, a LinkedIn URL, and the company’s firmographics, instead of a company name and a prayer.

Let’s walk through the steps one at a time, then wire the whole thing up over a CSV.

Step 1: Turn the company name into a domain

Start here. Always. This one call fixes more downstream failures than anything else you’ll do.

from cufinder import Cufinder

client = Cufinder('your-api-key-here')

# company name + country code
result = client.cuf('stripe', 'US')
print(result)

The response comes back as JSON with the verified domain and a confidence score:

{
    "status": 1,
    "data": {
        "confidence_level": 94,
        "query": "stripe",
        "domain": "http://stripe.com/",
        "credit_count": 9997
    }
}

Notice the country code. It’s required, and it genuinely changes your answer. The reference docs use a nice example: search “Nestlé” with the country set to Switzerland and you get nestle.ch. Leave the country loose and you might get the wrong regional site entirely.

So if your list spans countries, store the country alongside the company name. Don’t guess it later.

Cost-wise, this is the cheap step: one credit per record found. And if you’d rather work in TypeScript, the same call looks like this:

import { Cufinder } from '@cufinder/cufinder-ts';

const client = new Cufinder('your-api-key-here');

const result = await client.cuf('stripe', 'US');
console.log(result);

Same method names across the official SDKs: TypeScript, Python, Go, Ruby, and Rust all exist, so use whichever your stack already speaks.

Step 2: Find the CEO at that company

Now you search for a person, filtered to the company you just resolved. Person Search takes a stack of filters, and company_domain is the one that matters most here, because it ties the person to the exact company rather than a fuzzy name match.

Here’s the part that trips people up, and it tripped me too. There’s no free-text “job title” filter on this endpoint. The documented filter is job_title_level, and it takes seniority levels: cxo, owner, partner, director, vp, manager, and a few more. For a CEO hunt, cxo is your level.

result = client.pse(
    country='united states',
    company_domain='stripe.com',
    job_title_level='cxo'
)
📌 Remember: Person Search filters by job_title_level, not by free-text title: 'cxo' is the level you want for executives, with 'owner' as the small-company fallback.

Two details worth knowing before you run it. First, country is a required filter, and it takes country names like 'united states', unlike the domain call in step 1, which wants a code like 'US'. Mixing those two formats up is the most common “why is this empty” bug in this workflow.

Second, the response gives you a peoples array, and each person carries their current_job.title, their company block, and their LinkedIn profile. The cxo level catches every chief officer: CEO, CFO, CTO, all of them. So you still check the title text on your side to keep the actual chief executive:

LEVELS = ['cxo', 'owner']
TITLE_WORDS = ('ceo', 'chief executive', 'founder',
               'managing director', 'president')

def find_leader(client, domain, country):
    for level in LEVELS:
        res = client.pse(country=country,
                         company_domain=domain,
                         job_title_level=level)
        people = res.get('data', {}).get('peoples') or []
        for person in people:
            title = ((person.get('current_job') or {}).get('title') or '').lower()
            if any(word in title for word in TITLE_WORDS):
                return person
        if people:
            return people[0]   # senior match, even if the title is unusual
    return None

What does a hit actually look like? Each entry in peoples is a compact record, enough to decide whether you’ve found your executive before you spend anything more:

{
    "full_name": "iain mckenzie",
    "current_job": { "title": "engineering" },
    "company": {
        "name": "stripe",
        "website": "https://stripe.com",
        "industry": "technology, information and internet",
        "main_location": { "country": "united states",
                           "state": "california",
                           "city": "south san francisco" }
    },
    "social": { "linkedin": "linkedin.com/in/iain-mckenzie" }
}

That LinkedIn URL in the social block is your free verification handle later. Hold onto it.

Why the owner fallback? Because in small companies the founder IS the chief executive, even when no profile says “CEO” anywhere. You’ll also see Managing Director in the UK, President in older US firms, and plain Founder at startups. The title check catches those variations instead of forcing one word.

This step costs five credits per record found, so keep the filters tight. A whole-company search without the level filter would pull far more people than you need, and charge you for each of them.

Step 3: Enrich the person

Search gives you a match. Enrichment gives you the detail: job title, business email, phone, LinkedIn URL, plus the company’s size, industry, and location, all in one record.

result = client.tep('iain mckenzie', 'stripe')

Two arguments: full name and company. That’s it. The response nests everything under data.person, and it’s the richest record in the whole chain:

{
    "status": 1,
    "data": {
        "confidence_level": 97,
        "person": {
            "full_name": "iain mckenzie",
            "job_title": "engineering",
            "email": "iain.mckenzie@stripe.com",
            "phone": null,
            "linkedin_url": "linkedin.com/in/iain-mckenzie",
            "company_name": "stripe",
            "company_website": "https://stripe.com",
            "company_size": "1,001-5,000",
            "company_industry": "technology, information and internet",
            "company_country": "united states"
        }
    }
}

Name, verified business email, LinkedIn URL, and the company’s firmographics in one shot. That’s your outreach-ready row, the thing the whole workflow exists to produce.

One thing to respect here: at ten credits per record found, this is the most expensive call in the chain. The reference lists it at 97% confidence, which is why it’s worth those credits. But only run it on the ONE person you actually kept from step 2, not on everyone the search returned.

Putting it together: the bulk Python pipeline

Here’s the whole workflow over a CSV, using Python’s standard csv module. It’s deliberately boring code, because boring code is what you want running against 400 rows at 2am.

import csv, time
from cufinder import Cufinder

client = Cufinder('your-api-key-here')

LEVELS = ['cxo', 'owner']
TITLE_WORDS = ('ceo', 'chief executive', 'founder',
               'managing director', 'president')

def bare(domain):
    return domain.replace('http://', '').replace('https://', '').strip('/')

def resolve_domain(name, country_code):
    try:
        res = client.cuf(name, country_code)
        return res.get('data', {}).get('domain')
    except Exception as e:
        print(f'domain lookup failed for {name}: {e}')
        return None

def find_leader(domain, country):
    for level in LEVELS:
        res = client.pse(country=country,
                         company_domain=domain,
                         job_title_level=level)
        people = res.get('data', {}).get('peoples') or []
        for person in people:
            title = ((person.get('current_job') or {}).get('title') or '').lower()
            if any(word in title for word in TITLE_WORDS):
                return person
        if people:
            return people[0]
    return None

def run(path):
    out, seen = [], {}
    with open(path) as f:
        for row in csv.DictReader(f):
            name = row['company']
            code = row.get('country_code', 'US')
            country = row.get('country', 'united states')

            if name in seen:                    # names repeat; don't pay twice
                domain = seen[name]
            else:
                domain = resolve_domain(name, code)
                seen[name] = domain

            if not domain:
                out.append({'company': name, 'status': 'no_domain'})
                continue

            leader = find_leader(bare(domain), country)
            if not leader:
                out.append({'company': name, 'domain': domain,
                            'status': 'no_leader_found'})
                continue

            detail = client.tep(leader['full_name'], name)
            out.append({'company': name, 'domain': domain,
                        'name': leader['full_name'],
                        'detail': detail.get('data', {}).get('person'),
                        'status': 'ok'})
            time.sleep(0.6)   # stay under 100 requests per minute
    return out

See that time.sleep(0.6)? Don’t skip it. Your key is capped at 100 requests per minute on a fixed 60-second window, and this loop fires up to four calls per row. Without the pause you’ll hit a 429 Too Many Requests somewhere around row 30 and lose your place. For anything bigger than a few hundred rows, swap the flat sleep for retries with backoff and jitter; the usage limits page has the exact numbers.

And notice the seen cache. Company names repeat across lists way more than you’d expect, and there’s no reason to pay for the same bulk name-to-domain resolution twice.

What does this cost per company?

Between two and four API calls per company, and the credit costs are documented per endpoint. Here’s the arithmetic for a clean row:

💡 Credit math: domain lookup (1) → executive search (5 per record found) → enrichment (10) → ~16 credits for a complete, verified CEO record. A 400-row list lands around 6,400 credits IF every row resolves; dedupe first and it drops.

Two things keep that number honest. A lookup that returns no match generally doesn’t charge you, so failed rows are cheap. But repeated identical inputs DO get charged separately, because nothing you upload is stored between requests. Deduplicate before you send; it’s the easiest money you’ll ever save.

And if 16 credits per company sounds like a lot, compare it to the alternative. Three to five minutes of a salesperson’s time per manual lookup, multiplied by 400 companies, is a week of salary spent on copy-paste. The credit math wins by an embarrassing margin, which is why the real cost question isn’t “API or not,” it’s “how few calls can I get away with per row.”

Can you find a CEO without an API?

Yes: for a handful of companies, free manual methods work fine. It’s only at volume that they collapse.

Here’s what I actually use when it’s five companies and not four hundred:

  • The company website. Leadership, About, or Team pages name the chief executive more often than not, and they’re usually current.
  • LinkedIn. Open the company page, check the People view, and filter by title. Slow but reliable.
  • Press releases. Funding and product announcements almost always quote the CEO by name.
  • SEC filings. For US public companies, EDGAR is the authoritative source: the DEF 14A proxy statement lists executives and what they’re paid. You can’t get more official than a regulatory filing.
  • A plain search. Typing the company name plus “CEO” into Google answers the easy cases instantly: big brands, recent news, anyone with a Wikipedia page. It’s the messy mid-market companies where this stops working.

So when does the API win? Simple math. Manual lookup takes three to five minutes per company when you’re careful. At 400 companies, that’s two full days of your life. I know, because I spent them. The three-call workflow does the same list in under half an hour of machine time, and it tells you which rows failed instead of silently guessing.

Five companies? Do it by hand. A hundred or more? Use the workflow.

Verify before you send anything

Quick answer: open the LinkedIn URL the API handed you and read the current role with your own eyes.

It takes ten seconds per contact, and it catches the one failure mode no data provider can fully solve: the executive who changed jobs last month. The search response already includes the profile link, so you’re not hunting for anything. Click, confirm, move on.

For the rows that matter most (the ones getting a personalized pitch or a partnership email), add two more checks. Make sure the domain in your record matches the company the person actually lists on their profile. And glance at the confidence_level field in each response; it’s the API telling you how sure it is, and a low number on an important row is your cue to double-check by hand.

Because here’s the thing about executive data: being 97% right is fantastic at scale and embarrassing in a single email. “Congrats on your new role” sent to the OLD CEO is a story your prospect will tell at dinner. Don’t be that story.

What actually breaks

That’s the happy path. Here’s what breaks, and I’d love to tell you this runs clean every time, but it doesn’t.

The company has no single CEO. Partnerships, agencies, and co-founded startups often have two or three people who’d all answer to “who runs this.” Return the list, don’t force one winner.

The name resolves to the wrong company. Usually a country problem. If you’re getting odd domains, check whether you’re passing the right country code before you blame the data.

The CEO left. No B2B data API is real-time on executive changes. So for anything high-stakes, treat the result as a strong lead and verify before you send a personalized pitch to someone who quit in March. Ask me how I know.

Big companies return too many people. At enterprise scale the cxo level surfaces regional chiefs and divisional presidents too. Tighten with company_state or city filters when that happens, and let the title check do the rest.

The title vocabulary drifts. “CEO” isn’t universal; that’s the whole reason the client-side TITLE_WORDS check exists. If your list skews toward one region, add its local executive titles to the tuple.

A few things I’d do differently now

Cache your domain lookups. I said it above and I’ll say it again, because on my first real run I paid for the same twelve companies three times across overlapping event lists. The seen dict in the pipeline is there because of that invoice.

Log the failures separately from the successes. When 60 rows come back empty, you want to see the pattern, and it’s almost always one country or one industry, not random. Mine was a batch of German GmbHs with the country column still set to US.

🧠 Sanity check: run 20 rows before you run 400. The failures in a small batch tell you what your data is actually like, for free, before the big spend.

And if your real goal is a whole list of decision-makers rather than one CEO per company, flip the order of operations: build a targeted prospect list company-first, then pull the people inside each account.

Frequently asked questions

How do I find out who the CEO of a company is?

Check the company’s leadership page, its LinkedIn People view, or a recent press release. For US public companies, SEC filings name the executives officially. At scale, chain a domain lookup, an executive search, and a person enrichment call: the three-step workflow above.

Can I find a CEO from just a domain, without the company name?

Yes. Skip step one and pass the domain straight into Person Search as company_domain, with job_title_level='cxo'. The name-to-domain call only exists to clean up messy input.

How can I find out who owns a company?

Run the same search with job_title_level='owner'; that level exists precisely for owner-operated businesses. For very small companies with no data footprint, national business registries are the fallback.

How many API calls does each company cost?

Between two and four: one for the domain, one or two for the executive search levels, and one for enrichment. In credits that’s roughly 16 for a fully resolved company (1 + 5 + 10), and less for rows that stop early.

What’s the rate limit?

100 requests per minute per key, on a fixed 60-second window. Go over and you get a 429, so build in a pause or a backoff rather than retrying immediately.

Do failed lookups cost credits?

A lookup that finds nothing generally doesn’t charge you. Duplicate inputs do get charged separately though, which is exactly why the pipeline caches domains and why you should dedupe the CSV first.

Is it okay to email a CEO directly?

Yes, if you keep it short, specific, and relevant to their business; executives answer sharp emails more often than you’d think. Verify the address first, send one thoughtful follow-up at most, and respect a no.

Go run it

Grab 20 rows from your list, drop in your key, and see what comes back. You’ll learn more from that first run than from anything I’ve written here, especially from the rows that fail, because those tell you what your data is actually like. Twenty rows, fifteen minutes, real answers.

And then you never have to do it by hand again. Which, after my two-day spreadsheet weekend, feels like a genuinely good trade. When you’re ready to chain more endpoints into bigger pipelines, the rest of the API workflow guides pick up right where this one ends.

How would you rate this article?
Bad
Okay
Good
Amazing
Comments (0)
Comments (0)
98% accuracy, GDPR & CCPA ready

Prefer to Explore on Your Own?

Skip the call and start free: 15 credits, no credit card required. Upgrade or talk to us whenever you’re ready.

Free plan available · 50 credits/month · no credit card required