Company data is the half of B2B enrichment that gets taken for granted.
Person data is obviously hard. Names collide, people move, titles are not a controlled vocabulary. Company data feels like it should be easy by comparison. A company is a single stable entity with a website. How difficult can it be?
Difficult enough that most account scoring models are built on fields that are wrong in ways nobody has checked. This post covers what a company data API actually returns, which fields you can rely on, which ones quietly break scoring models, and how to use them properly.
What firmographic enrichment returns
The standard payload from a company lookup is reasonably consistent across providers:
| Field group | Typical contents |
|---|---|
| Identity | Name, slug, website, opaque identifier, logo |
| Classification | Industry, specialties, description, tagline |
| Size | Employee count, employee range band |
| Location | Headquarters, additional locations |
| History | Founded date |
| Signals | Follower count, funding signals where available |
That looks like a clean record. The reliability underneath it varies enormously by field, and knowing which is which is most of the value of understanding this category.
Which fields you can trust
Ordered by how much weight they can carry in a model.
Website and domain: highly reliable. This is the closest thing to a primary key a company has. It is machine-verifiable and rarely ambiguous.
Founded date: reliable and immutable. It does not change. If it is right once it is right forever, which makes it unusually safe for segmentation.
Headquarters location: reliable, slow to change. Relocations happen but rarely.
Name: reliable, with caveats. The trading name is usually right. Whether it matches the legal entity, and which of several legal entities it corresponds to, is a different question.
Industry classification: moderately reliable, and the one that breaks models. More on this below, because it deserves its own treatment.
Employee count: directionally useful, precisely wrong. Treat it as a band, never as a number.
Follower and funding signals: useful as relative indicators, unreliable as absolutes.
The two fields that break scoring models
Almost every account scoring model is built on industry and headcount. Both have specific failure modes that nobody checks for.
Industry classification is a judgment call
There is no single authoritative industry taxonomy, and the common ones disagree with each other.
Consider a company selling software to hospitals. Is it healthcare or software? A payments company serving retailers: fintech, financial services, or software? A logistics company whose product is an app: logistics or technology?
Different providers answer differently, and both answers are defensible. Which means if you build a scoring rule that says "industry equals software," you are not filtering on reality, you are filtering on one provider's classification decision for ambiguous cases.
Three practical consequences:
Use industry as a broad filter, not a precise one. It is good for excluding obviously wrong segments and bad for defining narrow ones.
Never switch providers without re-validating your rules. A provider change silently reshuffles every industry-based rule you have, and nothing will error.
Where classification matters a lot, corroborate. What a company says about itself in its description, and what roles it hires for, are often better evidence than a taxonomy label.
Headcount is a band pretending to be a number
Employee count comes back as a specific figure, which invites precision it does not have.
The sources of error are structural. Contractors, part-time staff, and recently departed employees are counted inconsistently. Subsidiaries may or may not roll up into a parent. Fast-growing companies are stale within months. And companies with a large share of staff who do not maintain professional profiles are systematically undercounted, which skews whole sectors.
The failure this produces in practice is a threshold rule. If your rule is "500 or more employees," a company reported at 480 falls out, and the true figure may be 520. Your model made a decision on noise.
The fix: use bands, not thresholds. Under 50, 50 to 200, 200 to 1000, 1000 to 5000, over 5000. Score on the band. Accept that boundaries are fuzzy and build rules that degrade gracefully across them rather than flipping.
Resolving a domain to a company
The most common operation, and the entry point for most integrations.
1from renidly import Renidly
2
3renidly = Renidly() # reads RENIDLY_API_KEY from the environment
4
5company = renidly.data.companies.retrieve(slug="northwind-logistics")
6
7if company:
8 print(company.name, company.industry, company.staff_count)If your stack is Node, the same call with camelCase on the response fields:
1import { Renidly } from "renidly";
2
3const renidly = new Renidly();
4
5const company = await renidly.data.companies.retrieve({ slug: "northwind-logistics" });
6
7if (company) {
8 console.log(company.name, company.industry, company.staffCount);
9}The parameter that matters more than it looks: retrieve accepts either a slug or an opaque id. Prefer the id, which looks like org_5ehe8408s7tme, and store it the first time you resolve a company.
Slugs are human-readable and can change. IDs cannot. If you key your account records on a slug, your pipeline will break silently when a company rebrands, and you will get a not-found rather than an error. The reasoning is in what B2B identity resolution actually is.
Note also that a not-found returns 200 with a flag rather than a 404, and it is billed, because a definitive "no match" takes the same work as a match. Branch on the response body rather than the HTTP status.
Searching rather than resolving
Company search filters a set rather than fetching one record, and it has a constraint that catches people out: name or website is required, with a minimum of three characters. You cannot search on filters alone.
1for company in renidly.data.companies.search(
2 name="logistics",
3 staff_count_min=200,
4 staff_count_max=2000,
5 hq_country_code="US",
6).auto_paging_iter():
7 store(company.id, company.name)1for await (const company of renidly.data.companies.search({
2 name: "logistics",
3 staff_count_min: 200,
4 staff_count_max: 2000,
5 hq_country_code: "US",
6})) {
7 await store(company.id, company.name);
8}That anchor requirement is a design decision worth understanding rather than fighting. It means company search is for finding companies matching a known concept, not for enumerating a market. If you want every company in a size band and country, that is a different operation and you should expect to assemble it from a seed list rather than from one query.
Two notes. Filter parameters keep their snake_case names in both languages, while method names follow the language convention. And every page of an auto-paginated walk is separately billed, so bound your loops and read meta.credit_consumed to know what a segment build actually cost.
People at a company
The endpoint that turns a company record into a list of humans, and the one with the most underused parameter in the whole API.
1leavers = renidly.data.companies.employees(
2 slug="northwind-logistics",
3 current_only=False,
4 sort="recently_left",
5 limit=50,
6)1const leavers = await renidly.data.companies.employees({
2 slug: "northwind-logistics",
3 current_only: false,
4 sort: "recently_left",
5 limit: 50,
6});sort="recently_left" with current_only=False returns people who recently departed that company. One call, two completely different plays depending on whose account it is.
If it is your customer, those are departures that may include your champion, and knowing today rather than at renewal is the difference between a save and a surprise. If it is a competitor's account, those are people who just left and know the incumbent's weaknesses, which makes them unusually receptive.
Other parameters: title does partial matching with a three-character minimum, start_year and optional start_month match people who joined in a given period, and geo_country_code and geo_city narrow geographically. Combining current_only=True with title targets people in a role right now, which is the common case.
For movement across many companies rather than within one, the dedicated event endpoint is better, and the patterns are in job change signals.
Enriching many companies
For a list, batch rather than looping. Up to 1000 per job, results keyed to your inputs, plus a dedicated not-found list.
1job = renidly.data.companies.enrich_batch(slugs=chunk)
2result = job.wait()
3
4for row in result.results:
5 upsert(row.matched_input, row)
6
7record_misses(result.not_found)1const job = await renidly.data.companies.enrichBatch({ slugs: chunk });
2const result = await job.wait();
3
4for (const row of result.results) await upsert(row.matched_input, row);matched_input ties each result back to exactly what you submitted, so partial results and retries stay safe. Persist the job handle before you wait rather than after, so a process crash does not throw away work you already paid for. The full pattern is in batch enrichment at scale.
The subsidiary problem
The structural issue nobody addresses, and it will eventually affect any account-based model.
Large organizations are not one entity. They are a parent, regional subsidiaries, acquired companies operating under their own names, and brands that share infrastructure but not legal identity.
Which means the question "how big is this company" has several correct answers depending on why you are asking. For territory assignment you probably want the local entity. For contract value you want the parent. For technical fit you want whichever unit actually runs the systems.
Most company data APIs return one entity per lookup and do not express hierarchy fully. So:
Decide which level you are modelling, explicitly, and write it down. Most scoring failures in enterprise accounts trace back to this being left implicit.
Expect multiple domains per organization. Regional domains, acquired-brand domains, and legacy domains all resolve to different records that are commercially the same account.
Maintain an account mapping for your top accounts manually. For the fifty accounts that matter most, a human-maintained mapping of which domains belong to which commercial account beats any automated resolution, and it takes an afternoon.
Using company data for scoring
A reasonable model, given the reliability caveats above:
1def account_score(company) -> int:
2 s = 0
3
4 # Size: bands, never thresholds
5 n = company.staff_count or 0
6 if 200 <= n < 1000: s += 30 # core ICP
7 elif 1000 <= n < 5000: s += 20
8 elif 50 <= n < 200: s += 15
9
10 # Industry: broad inclusion, not narrow definition
11 if company.industry in BROAD_TARGET_INDUSTRIES: s += 20
12
13 # Geography: reliable
14 if company.hq_country_code in TARGET_MARKETS: s += 15
15
16 # Maturity: immutable, safe to use
17 if company.founded and company.founded < 2020: s += 10
18
19 return sTwo deliberate choices. Size uses overlapping bands with graceful degradation rather than a hard cutoff. And industry adds points rather than gating, so a misclassification costs you twenty points instead of removing the account entirely.
The general principle: give the least reliable fields the least power to exclude. A field that is wrong 15% of the time should not have a veto.
For scoring people rather than accounts, and for the filter-ladder technique that keeps a segment from collapsing, see ICP filtering.
Keeping it current
Company data decays more slowly than person data, which is why teams forget to refresh it at all.
Rough half-lives: headcount is volatile at growth-stage companies and worth refreshing every six to twelve months. Industry and headquarters are stable for years. Founded date never changes.
The event worth watching for is acquisition, because it invalidates a company record completely: name, domain, headcount, and often the entire account relationship. There is no clean signal for it in firmographic data, which is why a sudden large headcount change or a cluster of departures is worth investigating rather than ingesting.
The field-level refresh model that applies here is in CRM data decay.
Frequently asked questions
What is a company data API?
An API that returns structured firmographic data about an organization, usually from a domain or company identifier. Typical fields are name, industry, employee count, headquarters, founded date and description.
What are firmographics?
Company-level attributes used to segment businesses, in the same way demographics describe people. Size, industry, location, age and revenue band are the common ones.
How accurate is employee count data?
Directionally useful and precisely unreliable. Treat it as a band rather than a number, because contractors, subsidiaries and reporting lag all introduce error.
Why do providers disagree on industry classification?
There is no single authoritative taxonomy, and many companies legitimately sit in more than one category. A software company selling to hospitals can be classified either way and both are defensible.
Can I look up a company by domain?
Yes, and domain is the most reliable identifier a company has. Store the provider's opaque ID after the first resolution, because slugs and names change while IDs do not.
How often should company data be refreshed?
Headcount every six to twelve months, more often for growth-stage companies. Industry, headquarters and founded date rarely need refreshing at all.
The one-line version
Company data looks more reliable than person data and is not. Domain and founded date are solid, headquarters and name are good, and the two fields everyone builds scoring models on, industry and headcount, are the two least dependable. Use bands rather than thresholds, let unreliable fields add points rather than gate accounts, and decide explicitly which level of a corporate hierarchy you are modelling before you discover the question the hard way in an enterprise deal.
The single highest-return habit is storing the opaque identifier on first resolution. Everything else is recoverable. A database keyed on names and slugs breaks silently and expensively.
If you want to see what a full company record contains before building anything, the Data API documentation lists every field, and the Quickstart takes about five minutes. For volume or a procurement conversation, talk to us directly.


