Open menu

Company Data API: Build a Targeted Prospect List

Company Data API: Build a Targeted Prospect List

Most prospect lists start the wrong way round. Someone exports a giant pile of companies, then spends three weeks filtering it down to the ones that were always the point.

A company data API flips that. You describe the company you want, and you get back only those. Then you find the humans inside them.

Two endpoints, and you can build a targeted list in an afternoon. I’ll show you the whole thing: the code, the filter traps, the scoring, and the part where I built a list so over-filtered it returned four companies.

What is a company data API?

A company data API gives you programmatic access to structured company profiles (industry, size, location, funding) that you query with filters instead of downloading in bulk.

The data behind it is called firmographics, the company-level equivalent of demographics. (Wikipedia’s firmographics entry covers the taxonomy if the term is new to you.) Who a company is, where it operates, how big it is, what it does, how much money flows through it.

The mental model shift is the important part. You’re not exporting a database and filtering it in a spreadsheet. You’re sending the filter TO the database and receiving only matches. Describe, don’t dig.

And a note on naming, because the terms swirl: a company data API covers company-level records specifically, while a B2B data API is the umbrella term that also spans people, contacts, and signals. This article uses the company layer to drive the whole prospect-list build: companies first, then the people inside them.

📌 Definition: A company data API = firmographic profiles you query with filters. You describe your ideal company; it returns the matches. No exports, no spreadsheet archaeology.

What data you actually get back

So what does the API actually hand back? Per record, the documented response includes:

  • Identity: company name, website, and the clean domain, the field everything else in this workflow keys on.
  • Size: an employees.range band like “51-200” rather than a fake-precise headcount.
  • Classification: industry, company type (privately held, public), and a text overview of what they do.
  • Location: main_location with country, state, city, and street address.
  • Social: LinkedIn, and where they exist, Facebook and Twitter URLs.
  • Money signals: funding amounts and annual revenue are filterable, so the money dimension is baked into your query even before you read a single record.

The index behind it spans 85M+ company profiles, and every response carries a confidence_level so you know how solid the match is.

One honest caveat before you design anything: not every record has every field. Private companies don’t publish revenue. Small companies skip LinkedIn. Any provider claiming complete coverage of private-company data is selling you something. The workflow below is built to route around gaps rather than pretend they don’t exist.

Where company data comes from (and why records disagree)

Worth thirty seconds, because it explains every oddity you’ll meet later.

Company data providers assemble their profiles from a handful of upstream layers:

  • Company websites, the self-described version: what they sell, where they are, who runs it.
  • Public registries and filings: incorporation records, annual reports where required. Reliable but slow-moving.
  • Professional networks: employee counts, follower counts, hiring activity. Fresh, but self-reported.
  • News and funding announcements, the money layer: rounds, acquisitions, expansions.

Each layer updates on its own schedule, which is why two providers rarely agree on a company’s exact headcount, and why the same provider can show an employee RANGE rather than a number. The range isn’t laziness; it’s honesty about how the sausage is made.

Practical consequence for your build: trust the stable fields (domain, country, industry) for filtering, and treat the volatile ones (revenue, headcount, followers) as scoring signals with error bars. That principle shapes everything below.

Prospect, lead, or contact? Get the words straight

Quick vocabulary check, because these three words get used interchangeably and they shouldn’t be.

  • A contact is a person whose details you have. Nothing more is implied. Your CRM is full of contacts who will never buy anything.
  • A prospect is a contact (or company) that FITS (right industry, right size, right problem) but hasn’t engaged with you yet. Fit is decided by you, using data.
  • A lead is someone who has shown interest: filled a form, replied, attended the webinar. Interest is decided by them, using behavior.

This article builds PROSPECT lists: fit-first, before any engagement exists. That’s why the whole pipeline runs on firmographic filters rather than website trackers. You’re selecting for fit and creating the engagement yourself, which is also why list quality matters so much. Outbound to a bad-fit list isn’t prospecting. It’s spam with extra steps.

First, decide what a good prospect looks like

Filters are just your ideal customer profile written as code. So if the profile is fuzzy, the list will be fuzzy, with more rows.

An ideal customer profile (ICP) is the description of the company that buys from you fastest, stays longest, and complains least. Before touching the API, answer four questions:

  • Where? One country to start. Territory rules and phone formats will thank you.
  • What industry? Be careful here: industry taxonomies differ. The API has its own industry list, which won’t map one-to-one onto NAICS codes or whatever your CRM uses. Search the API’s list for their label, not yours.
  • How big? Pick the employee band where your product actually wins. Not “1 to 10,000.”
  • What money signal? Revenue floor, funding floor, or neither. (Spoiler: this filter causes the most trouble. We’ll get there.)

Salespeople sometimes frame this with the five P’s of prospecting: purpose, preparation, personalization, persistence, practice. The API handles none of those. What it handles is making PREPARATION fast: the research grunt work that used to eat entire afternoons becomes one function call.

And resist the urge to serve three segments with one list. One segment, one list, one message. You can always run the build again with different filters. That’s the whole point of it being code.

How many prospects do you actually need?

Fewer than you think, and the way to know is to work your funnel math backwards.

Say the team needs 5 new deals this quarter. Walk it up the funnel with YOUR conversion rates (mine below are just an illustration; pull yours from the CRM):

→ 5 deals ← 25 demos (20% close) ← 125 replies (20% book) ← ~1,700 contacts reached (7% reply)

So a quarter’s worth of pipeline is a list of one or two thousand well-chosen prospects, not twenty thousand random ones. That’s a couple of API runs, comfortably.

The reason to do this math BEFORE building: it stops the “more rows = more pipeline” instinct that produces bloated lists nobody works properly. If your reply rate is bad, doubling the list doubles the disappointment. Fix fit first; the math above only compounds what you feed it.

The two halves

  • Company Search: filter 85M+ company profiles by location, industry, size, funding, and founding year
  • Person Search: find the right people inside the companies you just selected

Companies first, people second. Always in that order, because the company filters are what make the people relevant.

Credit-wise, the split matters too: company search costs 3 credits per record found, person search costs 5. Cheap wide net on companies, expensive precise net on people, which is exactly why the qualify-first pattern later in this article saves real money.

Setup is the boring part, so here it is in one paragraph. API access is key-based: one key authenticates every endpoint, and the same key carries your rate limit and credit balance. Keep it in an environment variable, not in the script. The moment this file lands in a shared repo with a hardcoded key, you’re rotating credentials on a Friday evening. And if several people need access, give the automation its own key so a teammate’s experiments can’t eat the credits your monthly build depends on.

Step 1: Describe the company you want

from cufinder import Cufinder

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

result = client.cse(
    country='US',
    industry='logistics',
    annual_revenue_min=5,      # revenue filters are in MILLION USD
    founded_after_year=2010
)
companies = result['data']['companies']

Two things to notice before you copy this.

First, the revenue unit. The annual_revenue_min and annual_revenue_max filters are denominated in million USD, so 5 means five million dollars. Type 5000000 there and you’ve politely asked for companies with five TRILLION in revenue, and the API will correctly hand you nothing. I’ve watched that one burn an entire debugging hour.

Second: country is required. Everything else is optional, and that’s where people get themselves into trouble.

Because here’s what happens: you add eight filters, get four results, and conclude the database is empty. It isn’t. You just described a company that barely exists.

Debug your filters one at a time

So start loose. Add one filter at a time and watch the count move:

base = {'country': 'US', 'industry': 'logistics'}

for extra in [{}, {'annual_revenue_min': 5},
                  {'founded_after_year': 2010},
                  {'followers_count_min': 1000}]:
    f = {**base, **extra}
    result = client.cse(**f)
    hits = result['data']['companies']
    print(f'{len(hits or [])} results with {list(extra) or "no extra filters"}')

Ten seconds of this tells you which filter is doing the damage. Usually it’s revenue, because plenty of good companies simply don’t publish it, and a filter can only match data that exists.

💡 Tip: Add filters one at a time and watch the count. The filter that empties the funnel is usually revenue; plenty of good companies don't publish it. Treat revenue as a scoring signal, not a hard gate.

The full filter reference

Everything Company Search accepts, straight from the documentation:

FilterTypeNotes
countrystringREQUIRED (the only mandatory filter)
state / citystringNarrow by region using the documented location lists
namestringSubstring match: “stripe” matches any name containing it
industrystringFrom the API’s own industry list, not NAICS
employee_sizeenumBands: 2-10, 11-50, 51-200, 201-500, 501-1000, 1,001-5,000, 5,001-10,000, 10,001+
founded_after_year / founded_before_yearintegerFounding-year window
funding_amount_min / maxintegerTotal funding raised
annual_revenue_min / maxintegerIn MILLION USD (5 means $5M)
followers_count_min / maxintegerLinkedIn followers, a rough brand-presence proxy
products_servicesarrayWhat the company sells, e.g. [“b2b”]
is_schoolbooleanInclude or exclude educational institutions
pageintegerPagination: large result sets come in pages

Three of these earn special respect. The name filter is a substring match, which is great for brand hunting and terrible if you forget and wonder why “log” returned half the database. Meanwhile followers_count_min is a sneaky-good proxy for “companies with any marketing presence at all.” And page matters the moment your filters match more than one page of results: loop it until the response runs dry, or you’ll build your list from page one and never know what you missed.

That’s the company half. Now the people.

Step 2: Find the people inside

Now loop your companies and pull the roles you sell to. Person Search takes company_domain, which ties each person to the exact company rather than a fuzzy name match:

result = client.pse(
    country='US',
    company_domain='stripe.com',
    job_title_role='operations',
    job_title_level='vp'
)
people = result['data']['peoples']

Titles are handled with two documented enums, and knowing them saves you from guessing strings:

  • job_title_role: customer_service, design, education, engineering, finance, health, human_resources, legal, marketing, media, operations, public_relations, real_estate, sales, trades
  • job_title_level: cxo, vp, director, manager, owner, partner, senior, entry, training

This is quietly a big deal. “VP Operations”, “VP of Ops”, and “Vice President, Operations” are one query here (role=operations, level=vp) instead of three brittle string matches. The taxonomy did the title-normalization work for you.

Each person comes back with their current job, their company (with domain; verify it matches what you asked for), their location, and social profiles including LinkedIn. That LinkedIn URL is your rep’s first click, so keep it in the export.

The whole build

import time
from cufinder import Cufinder

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

ROLES = [('operations', 'vp'), ('operations', 'director'), ('operations', 'manager')]

def build_prospect_list(filters, max_companies=100):
    result = client.cse(**filters)
    companies = result['data']['companies'] or []
    print(f'{len(companies)} companies matched')

    prospects = []
    for c in companies[:max_companies]:
        domain = c.get('domain')
        if not domain:
            continue

        for role, level in ROLES:
            try:
                found = client.pse(country=filters['country'],
                                   company_domain=domain,
                                   job_title_role=role,
                                   job_title_level=level)
                people = found['data']['peoples'] or []
            except Exception as e:
                print(f'  person search failed for {domain}: {e}')
                people = []

            for p in people:
                prospects.append({'company': c.get('name'),
                                  'domain': domain,
                                  'role': role,
                                  'level': level,
                                  'person': p})
            time.sleep(0.7)

        if not any(p['domain'] == domain for p in prospects):
            print(f'  no contacts found at {domain}')

    return prospects

Watch that max_companies cap. A broad company search can return a lot, and three role lookups per company adds up fast against the API’s usage limits: 100 requests per minute per key on a fixed window. The time.sleep(0.7) keeps a three-role loop safely under it.

Two production notes from running builds like this for years. First, print as you go: the no contacts found line isn’t decoration, it’s how you notice that a whole segment of companies has no reachable people, which is a finding in itself. Second, make long runs resumable: write each company’s prospects to disk as they’re collected rather than holding everything in memory until the end. A network blip at company 380 of 400 shouldn’t cost you the first 379. The pattern is the same state-file trick from every other workflow on this blog: save incrementally, skip what’s already saved on restart.

A cheaper way to run it

You don’t have to search every company for every role. Qualify first, then go deep only on the ones that pass.

Company Search → drop anything without a domain → one role lookup → only if it hits, try the other roles

That single change cut my call volume by roughly half on the last list I built, and the output was the same.

The credit math explains why. Person search is 5 credits per record found, your most expensive call. A company with no VP of operations probably has no director of operations either, so the first miss is a cheap early signal to move on. Spend the expensive credits only where the first probe found life.

Export it somewhere a rep can use it

A list living in a Python variable helps nobody. Flatten it to a CSV your CRM or your rep can open:

import csv

def export_prospects(prospects, path='prospects.csv'):
    cols = ['company', 'domain', 'person_name', 'title', 'level', 'linkedin']
    with open(path, 'w', newline='') as f:
        w = csv.DictWriter(f, fieldnames=cols)
        w.writeheader()
        for p in prospects:
            person = p['person']
            w.writerow({
                'company':     p['company'],
                'domain':      p['domain'],
                'person_name': person.get('full_name'),
                'title':       (person.get('current_job') or {}).get('title'),
                'level':       p['level'],
                'linkedin':    (person.get('social') or {}).get('linkedin'),
            })
    print(f'wrote {len(prospects)} rows to {path}')

Two small decisions here that save arguments later. Keep the domain column; it’s your dedup key against the CRM. And if you found three people at one company, decide NOW whether your team works multiple contacts per account or one owner each. Nothing sours a Monday like two reps discovering they emailed the same VP.

Score the list before anyone dials

A 1,400-row list nobody trusts is worse than no list. Scoring fixes that, and it doesn’t need machine learning, just arithmetic over the fields you already fetched:

IDEAL_SIZES = {'51-200', '201-500'}

def fit_score(company):
    score = 0
    if (company.get('employees') or {}).get('range') in IDEAL_SIZES:
        score += 3
    if company.get('industry') == 'logistics':
        score += 2
    if company.get('domain'):
        score += 1
    loc = company.get('main_location') or {}
    if loc.get('state') in {'california', 'texas', 'illinois'}:
        score += 1
    return score

ranked = sorted(companies, key=fit_score, reverse=True)

The weights are yours to argue about. That’s the point. A scoring function forces the team to say out loud what makes an account good, and then the list order stops being vibes.

Then do the one manual step I will defend forever: read the top twenty rows. Actually read them. Open five websites.

🧠 Rule of thumb: Score with data, but eyeball the top 20 rows before anyone dials. If those 20 don't look right, no volume will fix the list.

Segment the list and match the message

One list, one message, remember? Well, your scored list probably contains two or three natural segments hiding inside it. Split them out before outreach starts, because the same email cannot land with a 30-person startup and a 3,000-person enterprise.

The segments are already in your data. Here are the splits I reach for, in order:

  • By employee band: 11-50 gets the “you don’t have time for this” message. 201-500 gets the “your process is breaking at scale” message. Same product, different pain.
  • By seniority of the contact found: a cxo-level contact gets two short sentences about outcomes. A manager-level contact gets the workflow details, because they’re the one living in it.
  • By funding recency: recently funded companies are in spending mode and moving fast. They get the growth angle, and they get contacted first, while the budget is fresh.
  • By state or region: if your team does in-person work or has territory rules, geography decides ownership before messaging even starts.

In practice this is one extra column in the export (segment), assigned with a few if-statements right after scoring. Five minutes of code, and every rep opens a CSV where the message practically writes itself.

Because here’s the thing about personalization at list scale: it doesn’t come from mail-merge tokens. It comes from segmentation done well. “Hi {first_name}” fools nobody. “You’ve doubled your ops team since 2023”, pulled straight from the firmographic data you already fetched, reads like homework, because it is.

Keep the list fresh (lists rot)

Here’s the uncomfortable truth about the beautiful list you just built: it started decaying the moment you exported it.

People change jobs. Companies get acquired, renamed, resized. HubSpot’s database decay research puts B2B data rot at roughly 22.5% a year: call it 2% of your list going stale every month. And stale data isn’t just wasted sends; it’s measurable money:

“Every year, poor data quality costs organizations an average of $12.9 million.”

Source: Gartner, “How to Improve Your Data Quality”

Harvard Business Review’s estimate for the US economy runs to $3 trillion a year, if you want the macro version of the same headache.

The fix is cheap because your list is code:

  • Re-run monthly with the same filters. New matches = companies that grew into your ICP. That’s a warm list by itself: they just BECAME your ideal customer.
  • Diff against your CRM on the domain column before handing anything to sales. You already know half of any list; don’t make reps rediscover that.
  • Refresh stale accounts with Company Enrichment before a big push: one call per company brings the full firmographic record up to date.

Notice the division of labor here: search builds the list, enrichment maintains it. You don’t re-search accounts you already own; you enrich them, because enrichment refreshes a known company while search would just find it again and charge you for the rediscovery. Search once, enrich forever is the cheaper loop for accounts that made it into the CRM.

From CSV to CRM without making a mess

The import step is where clean lists go to die, so give it the same discipline as the build:

  • Dedupe on domain against existing records first. Existing account? Attach the new contact to it; don’t create “Acme Corp (2)”. Your future reporting will thank you.
  • Tag every imported row with a source and date. Something like api-build-2026-08. Six months from now, when someone asks whether the API lists actually convert, the tag IS the answer.
  • Map the fields deliberately. The employee range goes into a picklist, not a free-text field, or you’ve just re-created the mess a normalization pass exists to fix.
  • Assign owners at import time. An unowned prospect is a prospect nobody calls. Round-robin by segment or territory, but decide before the rows land.

None of this is glamorous. All of it is the difference between “we have a data workflow” and “we have a folder of CSVs someone built once.”

Choosing a company data API (a fair test, not a feature grid)

Everything above works with any decent provider, ours included. So here’s the honest way to evaluate one, and it isn’t reading feature pages, mine or anyone’s.

Run a coverage test on YOUR segment:

  • Take 50 companies you already know: current customers, lost deals, dream accounts.
  • Query all 50 and measure three things: how many were found, how many had a domain, and how many had the fields your filters depend on.
  • Then run your real filter set and eyeball whether the returned companies feel like your market.

Coverage is segment-specific. A provider brilliant on US software can be mediocre on European manufacturing, and no marketing page will tell you which one you’re getting. Fifty known companies will, in an hour, for pocket change.

Beyond coverage, check the boring mechanics: filter depth (can it express YOUR ICP?), credit economics (what does a thousand-company build actually cost?), and rate limits (can you finish a build in an evening?). The glamorous stuff (AI scoring, intent add-ons) matters far less than whether the database contains your buyers.

Scale the list sideways with lookalikes

After a quarter of working the list, you’ll know which accounts actually converted. Don’t just celebrate. Copy them.

Take your five best closed-won companies and feed them into the lookalike account list workflow: it finds companies that resemble your winners rather than companies that merely match your filters. Filters describe your ICP as you guessed it. Lookalikes describe your ICP as your revenue proved it. The second list is usually better, and you needed the first one to earn it.

What breaks

Over-filtering. Covered above, but it’s genuinely the number one issue. Loose first, tighten second. Four results doesn’t mean an empty database; it means an impossible company.

Industry labels don’t match your vocabulary. You say “logistics,” a company categorizes itself as “transportation” or “supply chain.” Search the API’s industry list for the labels adjacent to yours and run the build once per label; it’s one loop variable.

Job titles don’t match reality. “Head of Logistics” at a 40-person company might be an operations manager or just the COO. This is what the role/level enums are for: query operations at several levels instead of betting on one title string.

Companies without domains. Some records won’t have one, and person lookup depends on it. Skip them rather than trying to guess; a wrong domain match is worse than a missing one.

Forgetting pagination. If your filters match more than a page of companies, the first response is not the whole answer. Loop the page parameter until results run dry, THEN cap and score.

You already know half the list. Diff against your CRM before handing anything to sales. Nothing erodes trust in “the new data workflow” faster than a rep recognizing eleven current customers on page one.

The list that taught me to check 20 rows

2024. Logistics vertical, US market, and I was very confident.

My first query stacked eight filters: revenue floor, founding window, follower minimum, two location constraints, the lot. Four results. FOUR. I spent twenty minutes convinced the integration was broken before I admitted the truth: I had described a company that barely existed.

So I rebuilt it the way this article preaches. Country plus industry: thousands of matches. Added the employee band: still healthy. Added the revenue floor: the funnel collapsed, because most mid-size private logistics firms don’t publish revenue. I moved revenue from filter to scoring signal and the funnel breathed again.

Then the eyeball test earned its keep. My top twenty rows included four freight brokerages: technically “logistics,” completely unservable by the product. One products_services tweak and a scoring adjustment fixed what no amount of volume would have.

→ 1,400 matched companies → 611 after the right filters → 380 with a named operations leader → the reps worked 380 instead of 1,400

Reply rates on that 380 beat every spray list the team had run that year. Not because the data was magic, but because every row had been DESCRIBED, scored, and eyeballed before a human spent time on it.

How this guide was put together

Every parameter name, enum value, and response field in this article comes from the live API documentation, checked in August 2026, including the credit costs (3 per record for company search, 5 for person search) and the 100-requests-per-minute fixed window. The list-build numbers are from my own projects, and your ratios will differ. External claims link to their sources: Wikipedia, the US Census Bureau, HubSpot, Gartner, and Harvard Business Review. And the standing limitation, stated plainly: no company data API has every company, and fields like revenue are missing exactly where you most wish they weren’t. Build workflows that expect gaps.

Frequently asked questions

What filters does Company Search accept?

Country is required. Then state, city, name, industry, founding year range, funding amount range, revenue range, follower counts, and products or services. The endpoint reference lists them all, with the documented values for the enum fields.

Can I search people without searching companies first?

Yes. Person Search accepts company attributes directly, including industry, employee size, and revenue ranges. Going company-first just gives you a cleaner account list to work from, and it’s cheaper, because you spend the expensive person-search credits only on companies that passed your filters.

How big a list can I build in one run?

At 100 requests per minute, a few hundred companies with two or three role lookups each is a comfortable hour. Anything larger, run it overnight in batches, and remember the credit math scales with records found, not requests made.

What is a prospect list?

A prospect list is a curated set of companies and contacts that fit your ideal customer profile and haven’t bought from you yet. The operative word is curated: a raw export is inventory, not a prospect list. Filtering, scoring, and deduplication are what turn one into the other.

What are the 5 P’s of prospecting?

Purpose, preparation, personalization, persistence, and practice: a common framing for prospecting discipline. An API automates the preparation P: researching who fits and who to contact. The other four remain stubbornly human, which is honestly the right division of labor.

What is the 70/30 rule in sales?

It’s the guideline that the prospect should talk about 70% of the time and the seller 30% in a discovery conversation. It has nothing to do with list building, but a well-researched list is what earns you conversations worth applying it to.

What is an example of a prospect?

Using this article’s build: a US logistics company, 51-200 employees, founded after 2010, with a named VP of operations you haven’t contacted yet. Company fits the profile, person owns the problem, no existing relationship: that’s a prospect.

What’s the difference between a company data API and a B2B data API?

Scope. A company data API covers company-level records: firmographics, funding, locations. A B2B data API is the umbrella that also includes people data, contact details, and buying signals. In this workflow, Company Search is the company data layer and Person Search is the people layer sitting beside it.

Build one small list today

Pick one country, one industry, and one role. Nothing else. Run it and look at the first twenty rows.

If those twenty look like people you’d actually want to talk to, your filters are right and you can scale up: add the scoring, schedule the monthly re-run, wire up the export. If they don’t, no amount of volume was going to save that list anyway.

Twenty rows. One afternoon. That’s the whole barrier between you and a prospect list your reps actually trust. Go build it.

And when it works, when the first reply comes back from a company your filters found, save that filter set somewhere safe. It’s not a query anymore. It’s your ICP, written down, tested against the market, and ready to run again next month. Most teams never get theirs out of a slide deck. Yours will be executable.

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