Skip to main content
Thirdwatchthirdwatch
Business & local data

Route Inbound Signups by Company Size and Industry Tier

Turn a work-email domain into an enterprise, mid-market or self-serve routing call within minutes, with a confidence tier for the domains that never resolve.

Jul 28, 2026 · 8 min read · 1,851 words
See the scraper →

TL;DR: The LinkedIn Company Enrichment actor turns the domain on an inbound signup into a firmographic record — industry, size band, employee count, HQ country, founded year — so a growth team can decide within minutes whether a new lead is enterprise, mid-market or self-serve. Feed it the work-email domains from a short micro-batch of new signups, gate on the resolution field so shaky matches never trigger a sales handoff, and route unresolved domains to a documented fallback instead of leaving them in limbo.

Why route inbound signups on firmographics rather than self-reported fields

A signup form tells you what someone typed; a firmographic record tells you who they actually work for. Asking for company size on the form costs conversion, and the answer is guessed as often as it is known. Meanwhile the one field you already collect — the work email — encodes the employer precisely, and nobody has to fill anything in to give it to you.

The reason this matters is speed, not curiosity. Harvard Business Review's audit of inbound lead response found that firms contacting a lead within an hour were roughly seven times more likely to have a meaningful qualifying conversation than those waiting even two hours (The Short Life of Online Sales Leads). A routing decision that arrives the next morning, after a weekly CRM enrichment job, has already spent most of the value it was supposed to protect.

So the job is narrow and time-boxed. A webinar registration, an ad-form lead or a trial signup lands. Within minutes you need three things: is this a company worth a human, which human, and what SLA applies. Everything else — account research, ABM scoring, territory assignment — happens later. The actor page lists the fields available to make that call.

How does this compare to the alternatives?

The realistic alternatives are a per-seat data vendor, a hand-maintained domain list, or nothing. Most teams end up on the third by accident, because the first is a procurement cycle and the second rots.

Method Commercial model Reliability Setup time Ongoing maintenance
Manual lookup by an SDR Headcount Accurate but only during working hours; breaks the SLA overnight None Grows linearly with signup volume
Hand-maintained domain-to-tier list Free Fine for your top 200 accounts, blind to everyone else Hours Constant; every new logo is a miss
Per-seat enrichment platform Annual contract, seat-based Good coverage, opaque match logic Weeks, including procurement Contract renewals and seat sprawl
Thirdwatch Actor Transparent pay-per-result Public company pages, with the match method returned per row Minutes Managed extraction and schema

The differentiator that matters for routing is the last column of row four: the actor tells you how each record was matched, not just what it matched to. A routing rule that cannot distinguish a confirmed match from a plausible guess will eventually page an account executive about a company that does not exist.

How to route inbound signups by company size in 5 steps

Which signups should reach the enrichment step at all?

Only signups with a corporate email domain that you have not already resolved. Free-mail addresses carry no firmographic signal, and re-enriching a domain you saw yesterday is pure waste — most inbound volume clusters on a surprisingly small set of employers.

FREE_MAIL = {
    "gmail.com", "googlemail.com", "outlook.com", "hotmail.com",
    "yahoo.com", "icloud.com", "proton.me", "protonmail.com", "aol.com",
}

def enrichment_candidates(signups, domain_cache):
    pending = {}
    for s in signups:
        domain = s["email"].split("@")[-1].strip().lower()
        if domain in FREE_MAIL:
            s["route"] = "self_serve"          # no enrichment, no cost
            continue
        if domain in domain_cache:
            s["company"] = domain_cache[domain]  # already known this cycle
            continue
        pending.setdefault(domain, []).append(s)
    return pending

The cache is doing most of the work here. Keep resolved domains for at least thirty days: firmographics move slowly, and a company that signed up twice this month has not changed size in between. Everything that survives both filters is a genuinely new employer.

How do I turn a batch of new domains into company records?

Pass the deduplicated domains straight into the companies input. It accepts bare website domains alongside LinkedIn URLs and slugs, so no pre-formatting is required.

import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
domains = list(pending.keys())          # e.g. ["hashicorp.com", "stripe.com"]

run = client.actor("thirdwatch/linkedin-company-enrichment-scraper").call(run_input={
    "companies": domains,
    "maxResults": len(domains),
    "concurrency": 10,
    "proxyConfiguration": {"useApifyProxy": True},
})

records = list(client.dataset(run["defaultDatasetId"]).iterate_items())
by_domain = {r["source_query"]: r for r in records}

source_query echoes the exact input string each row came from, which is what lets you join the results back to the signups that triggered them without a fuzzy match. Set maxResults to the batch length so a runaway input can never bill beyond what you queued. Leave concurrency at 10 for micro-batches; raise it only for backfills, since LinkedIn rate-limits by IP and a lower value with proxy rotation on is more stable than a high one. The Apify API reference covers the run and dataset endpoints if you are not using the Python client.

How do I separate a confirmed match from a plausible guess?

Read the resolution field before you let a record trigger anything. Domain inputs are resolved best-effort, and the actor reports which route produced each row rather than flattening them all into one score.

CONFIRMED = {"direct", "domain_guess_website_match", "typeahead_website_match"}
PROBABLE  = {"domain_guess_name_match", "typeahead_name_match"}

def confidence_tier(record):
    if record is None:
        return "unresolved"
    if record["resolution"] in CONFIRMED:
        return "confirmed"
    if record["resolution"] in PROBABLE:
        return "probable"
    return "weak"

A *_website_match means the LinkedIn page the actor landed on lists the same website domain you fed in — the company vouched for the link itself. A *_name_match means only the display name lined up, which is right most of the time and wrong for common words and heavily-branded domains. direct applies when you supplied a LinkedIn URL or slug and no guessing was needed. Treat confirmed as safe for automated sales handoff, probable as safe for scoring but worth a human glance, and never let weak or unresolved page anyone.

How do I turn a company record into a routing decision?

Score on employee_count_min and industry, then intersect with the confidence tier. Two axes, one lookup table, no machine learning.

def route(signup, record):
    tier = confidence_tier(record)
    if tier in ("unresolved", "weak"):
        signup["route"] = "self_serve"
        signup["needs_retry"] = True
        return signup

    floor = record.get("employee_count_min") or 0
    industry = (record.get("industry") or "").lower()
    strategic = any(k in industry for k in ("software", "financial services", "hospital"))

    if floor >= 1000 or (floor >= 500 and strategic):
        signup["route"] = "enterprise_ae" if tier == "confirmed" else "enterprise_review"
        signup["sla_minutes"] = 15
    elif floor >= 50:
        signup["route"] = "mid_market_queue"
        signup["sla_minutes"] = 60
    else:
        signup["route"] = "self_serve"

    signup.update({
        "company_name": record["name"],
        "company_size": record["company_size"],        # "1001-5000 employees"
        "industry": record.get("industry"),
        "hq_country": record.get("hq_country"),
        "founded_year": record.get("founded_year"),
        "linkedin_url": record["linkedin_url"],
        "match_method": record["resolution"],
    })
    return signup

Use employee_count_min rather than employees_on_linkedin as the routing input. The band is what the company itself declares; the LinkedIn member count is a proxy that skews heavily by industry. Keep the raw industry string on the lead rather than only your derived tier, so you can re-cut the taxonomy later without re-enriching anything.

How do I keep the whole loop inside the SLA?

Run a micro-batch on a short fixed cadence instead of firing one run per signup. A five-minute schedule enriches, scores and queues every record well inside a fifteen-minute enterprise SLA, and amortises the run overhead across the whole batch.

run = client.actor("thirdwatch/linkedin-company-enrichment-scraper").call(
    run_input={
        "companies": domains,
        "maxResults": len(domains),
        "concurrency": 10,
        "proxyConfiguration": {"useApifyProxy": True},
    },
    timeout_secs=240,           # fail fast rather than blowing the window
)
if run["status"] != "SUCCEEDED":
    fallback_route_all(pending)  # every signup still gets a queue, just a safe one

The failure branch is the part teams skip. If the run times out, the correct behaviour is not to retry silently while leads age — it is to route the entire batch to the self-serve path, flag it for a backfill, and move on. Wire an Apify webhook to your own endpoint if you would rather be pushed the finished dataset than poll for it.

What does a routed signup record look like?

Each enriched row carries the firmographics your rules read plus the provenance your rules trust. Two rows from a real batch, redacted:

[
  {
    "name": "HashiCorp",
    "slug": "hashicorp",
    "linkedin_url": "https://www.linkedin.com/company/hashicorp",
    "website": "https://www.hashicorp.com",
    "website_domain": "hashicorp.com",
    "industry": "Software Development",
    "company_size": "1001-5000 employees",
    "employee_count_min": 1001,
    "employee_count_max": 5000,
    "employees_on_linkedin": 2184,
    "follower_count": 233481,
    "founded_year": 2012,
    "company_type": "Public Company",
    "specialties": ["DevOps", "Cloud Infrastructure", "Security"],
    "headquarters": "San Francisco, California",
    "hq_country": "US",
    "locations_count": 4,
    "input_type": "domain",
    "resolution": "domain_guess_website_match",
    "source_query": "hashicorp.com",
    "scraped_at": "2026-07-28T09:14:02+00:00"
  },
  {
    "name": "Northfield Logistics",
    "slug": "northfield-logistics",
    "linkedin_url": "https://www.linkedin.com/company/northfield-logistics",
    "website_domain": "northfieldlog.co",
    "industry": "Truck Transportation",
    "company_size": "51-200 employees",
    "employee_count_min": 51,
    "employee_count_max": 200,
    "employees_on_linkedin": 87,
    "follower_count": 1204,
    "founded_year": null,
    "company_type": "Privately Held",
    "specialties": [],
    "hq_country": "US",
    "input_type": "domain",
    "resolution": "typeahead_name_match",
    "source_query": "northfieldlog.co",
    "scraped_at": "2026-07-28T09:14:05+00:00"
  }
]

The first row routes to an enterprise AE without a human in the loop: the website domain on the LinkedIn page matches the signup domain exactly. The second is probable, not confirmed — the domain and the display name are close but the company never listed its site — so it scores into the mid-market queue and gets a glance before anyone calls. Note the nulls on founded_year and specialties: never write a routing rule that requires an optional field to be present.

Common pitfalls

The single biggest one is assuming every domain resolves. Feeding a bare website domain lands on the right LinkedIn company roughly three times in four; abbreviated, ambiguous or heavily-branded domains miss, and the actor deliberately returns nothing rather than a wrong company. Design the unresolved path first, because it is a quarter of your volume.

Company names are fuzzier still — a common name can resolve to a larger company that shares it, which is why name inputs belong in backfills, not in a live routing path. founded_year, specialties and employees_on_linkedin are optional on LinkedIn itself and will be absent for companies that never filled them in. Subsidiaries are their own trap: a signup from a regional arm resolves to that arm's page, not the parent's, so a global enterprise can look like a 200-person business.

Rate limiting is the operational one. LinkedIn throttles by IP, so keep proxy rotation on and lower concurrency if the run log shows repeated retries. The actor handles the rotation and retry logic for you; your job is to cap maxResults at your batch size so a bad input can never turn into an unbounded run.

Related use cases

These adjacent workflows use the same actor from a different starting point. Continue with:

Frequently asked questions

How fast can a signup be routed with this approach?

Fast enough for a minutes-level SLA. Run the actor on a short micro-batch cadence rather than per signup, so a record enrolled at the top of a five-minute window is enriched, scored and queued before the window closes.

What happens to signups whose domain does not resolve?

They route on a documented fallback rather than stalling. Roughly one in four bare domains fails to confirm a LinkedIn company page, so send unresolved records to your self-serve nurture path and flag them for a later retry.

Should I enrich free-mail signups too?

No. Gmail, Outlook and Yahoo domains carry no firmographic signal and only add cost. Filter them out before the enrichment call and route them straight into your product-led onboarding flow.

Is employees_on_linkedin the same as headcount?

No. It counts members who list the company as their employer, which undercounts in non-desk industries and overcounts where alumni never updated their profiles. Use company_size and employee_count_min as your band, and treat the LinkedIn count as a corroborating signal.

Can I route on industry as well as size?

Yes. The industry label and the specialties array are both returned per record. Map industry strings to your own tier taxonomy in code, and keep the raw string on the lead so you can re-map later without re-enriching.

Related

Try it yourself

100 free credits, no credit card.

About 30 real searches. Add the MCP to Claude or Cursor in two minutes.