Screen Startup Dealflow With Public Company Firmographics
Pre-screen inbound startup dealflow on public firmographics: founded year, company type, employee band, HQ country and stated specialties, in one pass.

To pre-screen inbound startup dealflow, run the LinkedIn Company Enrichment Actor across your list of LinkedIn URLs, slugs, company names or bare domains, then read
founded_year,company_type,company_size,hq_country,specialtiesandlocations_countoff each returned record. Those fields answer stage fit, ownership status, headcount band, geography and sector in one consistent pass, so partner time only goes to companies that clear the mandate. It is a pre-screen for investment fit, not a substitute for a statutory registry check.
Why screen startup dealflow with public company firmographics
Inbound dealflow arrives as a list of names and links, and almost none of it fits. Research by Gompers, Gornall, Kaplan and Strebulaev, summarised in Harvard Business Review, found that the median venture firm considers roughly 100 opportunities for every deal it closes. Corp-dev teams face the same funnel shape with a narrower mandate. The expensive failure mode is not missing a good company; it is a partner spending forty minutes on a call with a 2009-founded public subsidiary in a geography the fund does not invest in, because nobody checked first.
A pre-screen fixes that only if it is consistent. An analyst eyeballing websites applies a slightly different bar on Friday afternoon than on Monday morning, and the reasoning never makes it into the tracker. What you want instead is a fixed set of columns, populated identically for every company in the batch, so that "did not clear the mandate" is a reproducible statement with a stated reason attached.
Every company that maintains a LinkedIn page publishes exactly those columns to logged-out visitors: the year it says it was founded, whether it describes itself as privately held or public, a headcount band, an HQ address broken into country and region, a list of self-declared specialties, and the number of offices it lists. That is enough to triage a hundred inbound names down to the fifteen worth a first call.
How does this compare to the alternatives?
Building this yourself means writing a resolver that turns four different identifier formats — full LinkedIn URLs, bare slugs, company names, website domains — into one canonical company page, then parsing structured fields that are optional on the source and absent on plenty of pages. A generic scraping API returns markup and leaves the resolution and parsing to you. A commercial dealflow platform gives you clean records but prices per seat, locks the fields behind a plan tier, and rarely covers the long tail of pre-seed companies that make up most inbound.
| Approach | Pricing model | Reliability | Setup time | Maintenance |
|---|---|---|---|---|
| DIY Python scraper | Your engineering time | Breaks on page layout and rate-limit changes | 1-2 weeks | Ongoing, on you |
| Generic scraping API | Per request, HTML only | Fetches pages, no resolved company fields | 3-5 days of parsing work | You own every parser |
| Dealflow data platform | Per seat, annual contract | High, but thin on pre-seed long tail | Procurement cycle | None, plus renewal |
| Thirdwatch LinkedIn Company Enrichment | Transparent pay-per-result | Identifier resolution and rate-limit handling built in | Under 10 minutes | None |
How to pre-screen startup dealflow in 4 steps
How do I turn a messy inbound list into structured company records?
Pass the list exactly as it arrives. The companies input accepts full LinkedIn company URLs, bare slugs, plain company names and website domains in the same array, so you do not have to normalise a pitch tracker before you can screen it. Each resolved company produces exactly one record.
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("thirdwatch/linkedin-company-enrichment-scraper").call(run_input={
"companies": [
"https://www.linkedin.com/company/hashicorp/",
"stripe.com",
"Datadog",
"shopify",
"figma.com",
],
"maxResults": 500,
"concurrency": 10,
"proxyConfiguration": {"useApifyProxy": True},
})
companies = list(client.dataset(run["defaultDatasetId"]).iterate_items())
print(len(companies), "companies enriched")Every row carries source_query, the exact input string it came from. Keep that column — it is how you join results back to the pitch tracker row, and how you spot a domain that resolved to the wrong company.
How do I filter on stage fit using founded year and headcount?
Combine founded_year with the parsed headcount band. The Actor returns company_size as the label LinkedIn displays (11-50 employees), plus employee_count_min and employee_count_max as integers so you can compare numerically without regex. employees_on_linkedin is a separate, usually lower count of members who list the company as their employer.
from datetime import date
THIS_YEAR = date.today().year
def stage_fit(row, max_age=8, max_headcount=250):
founded = row.get("founded_year")
ceiling = row.get("employee_count_max")
if founded and THIS_YEAR - founded > max_age:
return False, f"founded {founded}, too mature for the mandate"
if ceiling and ceiling > max_headcount:
return False, f"{row.get('company_size')} exceeds headcount ceiling"
if not founded:
return None, "founded_year missing — manual review"
return True, "stage fit"
for row in companies:
verdict, reason = stage_fit(row)
row["stage_verdict"] = verdict
row["stage_reason"] = reasonNote the three-state return. A missing founded_year is not a rejection; LinkedIn treats founded year as optional and plenty of early companies never fill it in. Sending those to manual review keeps the false-negative rate honest.
How do I check ownership status and geography against the mandate?
Read company_type and hq_country. company_type returns the label the company selected — Privately Held, Public Company, Nonprofit, Government Agency, Partnership, Self-Employed — which is the cheapest available signal that an inbound "startup" is actually a listed subsidiary or a state entity. hq_country comes pre-split from the formatted headquarters string, alongside hq_city, hq_region and hq_postal_code, so you can filter geography without parsing addresses.
ALLOWED_TYPES = {"Privately Held", "Partnership"}
MANDATE_GEOS = {"United States", "Canada", "United Kingdom", "Germany", "India"}
for row in companies:
flags = []
ctype = row.get("company_type")
country = row.get("hq_country")
if ctype and ctype not in ALLOWED_TYPES:
flags.append(f"company_type={ctype}")
if country and country not in MANDATE_GEOS:
flags.append(f"hq_country={country}")
if (row.get("locations_count") or 0) >= 5:
flags.append(f"{row['locations_count']} offices — likely past seed")
row["mandate_flags"] = flagslocations_count is a useful secondary maturity signal. A company claiming to be pre-seed while listing seven offices is either mislabelled in your tracker or worth a question on the first call.
How do I check sector fit without reading every About page?
Match against specialties, the array of self-declared focus areas the company publishes on its own page, and fall back to the industry label and the tagline and description text. Specialties are more granular than the single industry value — a company can be tagged Software Development while listing observability, distributed tracing and incident response as specialties.
THESIS_TERMS = {
"devtools": ["developer", "ci/cd", "observability", "infrastructure", "api"],
"fintech": ["payments", "lending", "compliance", "banking", "kyc"],
"healthtech": ["clinical", "patient", "ehr", "telemedicine", "diagnostics"],
}
def thesis_buckets(row):
haystack = " ".join(filter(None, [
" ".join(row.get("specialties") or []),
row.get("industry"),
row.get("tagline"),
(row.get("description") or "")[:600],
])).lower()
return [name for name, terms in THESIS_TERMS.items()
if any(term in haystack for term in terms)]
for row in companies:
row["thesis"] = thesis_buckets(row)The same run works over plain HTTP if you would rather not add a dependency:
curl -X POST "https://api.apify.com/v2/acts/thirdwatch~linkedin-company-enrichment-scraper/runs?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"companies": ["https://www.linkedin.com/company/hashicorp/", "figma.com"],
"maxResults": 100,
"concurrency": 10
}'How do I produce a screening sheet the partners will actually read?
Export one row per company with the verdict, the reason and the evidence columns side by side, so a partner can disagree with a rejection in five seconds rather than re-researching it. Attach the run to a weekly Apify Schedule pointed at the previous week's inbound batch, and keep scraped_at so a stale record is obvious.
import csv
FIELDS = [
"source_query", "name", "linkedin_url", "website_domain",
"founded_year", "company_type", "company_size", "employees_on_linkedin",
"hq_city", "hq_country", "locations_count", "industry",
"stage_verdict", "stage_reason", "mandate_flags", "thesis", "scraped_at",
]
with open("dealflow_screen.csv", "w", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=FIELDS, extrasaction="ignore")
w.writeheader()
for row in companies:
w.writerow({
**row,
"mandate_flags": "; ".join(row.get("mandate_flags") or []),
"thesis": ",".join(row.get("thesis") or []),
})Sample output
Two records, lightly redacted. The first clears a seed-stage software mandate; the second is the kind of inbound that looks like a startup in a tracker and is not one. Every row also carries slug, company_id, logo_url, banner_image_url, recent_post_count and latest_post_at if you want an activity signal alongside the firmographics.
[
{
"source_query": "figma.com",
"name": "Northwind Telemetry",
"slug": "northwind-telemetry",
"company_id": "78412093",
"linkedin_url": "https://www.linkedin.com/company/northwind-telemetry/",
"website": "https://northwindtelemetry.com",
"website_domain": "northwindtelemetry.com",
"tagline": "Distributed tracing for teams that ship daily",
"industry": "Software Development",
"company_size": "11-50 employees",
"employee_count_min": 11,
"employee_count_max": 50,
"employees_on_linkedin": 34,
"follower_count": 2810,
"founded_year": 2022,
"company_type": "Privately Held",
"specialties": ["observability", "distributed tracing", "incident response"],
"headquarters": "548 Market St, San Francisco, California 94104, US",
"hq_city": "San Francisco",
"hq_region": "California",
"hq_country": "United States",
"locations": ["San Francisco, California, US"],
"locations_count": 1,
"scraped_at": "2026-07-28T08:12:44Z"
},
{
"source_query": "Meridian Industrial Systems",
"name": "Meridian Industrial Systems",
"linkedin_url": "https://www.linkedin.com/company/meridian-industrial-systems/",
"industry": "Industrial Machinery Manufacturing",
"company_size": "1001-5000 employees",
"employee_count_min": 1001,
"employee_count_max": 5000,
"employees_on_linkedin": 2317,
"founded_year": 1987,
"company_type": "Public Company",
"specialties": null,
"headquarters": "Osaka, Osaka Prefecture, JP",
"hq_country": "Japan",
"locations_count": 14,
"scraped_at": "2026-07-28T08:12:51Z"
}
]The second record is rejected on three independent columns — founded_year 1987, company_type Public Company, hq_country outside the mandate — which is what a defensible pre-screen looks like. specialties is null there, a reminder that the field is optional on the source.
Common pitfalls
Domain inputs resolve to the correct LinkedIn company roughly three times in four, and plain company names are fuzzier still — a common name can resolve to a larger company that shares it. Always keep source_query next to linkedin_url and spot-check that pairing before a rejection reaches a tracker; where you already hold a LinkedIn URL or slug, pass that, because those resolve deterministically. Expect nulls: founded_year, specialties and employees_on_linkedin are optional on LinkedIn and simply absent where the company never entered them, so build your filters to route nulls to review rather than to reject. Everything here is self-reported by the company on its own page — company_type and company_size are selections it made, not verified facts, and employees_on_linkedin counts members who list the company as employer, which undercounts non-desk workforces badly. LinkedIn also rate-limits repeat callers by IP, so keep the default proxy configuration on and lower concurrency if the run log shows repeated retries. Most important: this is a mandate-fit pre-screen. Whether an entity legally exists, who its directors are and whether it is in good standing are registry questions, and the answer belongs in a filing check, not a firmographic record. The Actor absorbs the identifier resolution and rate-limit handling; the coverage and self-reporting limits above belong to the source.
Related use cases
- Size a B2B market with LinkedIn company firmographics — the same fields aggregated into a bottom-up market count with the method shown.
- Backfill firmographics on CRM accounts — fill blank Industry, Employee Count and Founded fields across an existing account table.
- Build a company enrichment API with Apify — wrap the same run behind your own cached endpoint with provenance fields.
- Scrape MCA India company data for due diligence — the statutory registry check that firmographics never replaces.
- Verify a company LEI for KYB — confirm legal entity identity once a company clears the pre-screen.
- LinkedIn Company Enrichment Actor page, the business data scraping guide, and every walkthrough on the Thirdwatch blog.
Frequently asked questions
Can firmographic screening replace a due-diligence check?
No. These fields are self-published by the company on its own page, so they answer mandate fit, not legitimacy. Confirm incorporation, directors and filing status against a statutory registry before any term sheet or investment committee memo.
What if a company has no founded year on its page?
The field comes back null. Founded year, specialties and employees-on-LinkedIn are optional fields on LinkedIn itself, so absence means the company never filled them in, not that the company is new. Route null-founded rows to manual review rather than rejecting them.
Can I feed the Actor a list of website domains from a pitch tracker?
Yes, but expect roughly three in four domains to resolve to the right company page. Ambiguous or heavily-branded domains can miss. Where you already hold a LinkedIn company URL or slug, pass that instead — those resolve deterministically.
Does company_type reliably distinguish private companies from public ones?
It is a good first filter. The field returns labels such as Privately Held, Public Company, Nonprofit and Government Agency exactly as the company selected them. Treat anything other than Privately Held as a reason to look, not as a hard rejection.
How many companies can I screen in a single run?
Up to 10,000 per run via maxResults, with concurrency controlling parallel lookups. A weekly inbound batch of a few hundred finishes in minutes at the default concurrency of 10; lower it if the run log shows repeated retries.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.