How to build a product qualified leads API with company data

How to qualify product-qualified leads using product-usage and firmographic data via the Crustdata API – pull, score, and route without a scoring platform.

Published

Written by

Chris P.

Reviewed by

Nithish A.

Read time

7

minutes

Most product-qualified lead pipelines score what a user did inside your product and never ask who the user works for. This guide builds one that does, using the Crustdata company API for the lookup.

A product qualified lead, or PQL, is a free or trial user whose activity inside your product suggests they are ready to pay. Most teams find them with a simple rule: once an account passes a usage threshold, say five active seats or a first completed project, the account is flagged and pushed into the CRM so a salesperson can follow up. Every published version of that pipeline works the same way, and every one scores the same kind of input: the events your product already records, such as seats filled, API calls made, and features tried.

That is also what separates a PQL from an MQL: a marketing-qualified lead is scored on marketing behaviour – a whitepaper download, a webinar signup, a demo request – before the person has ever used the product, while a product-qualified lead is earned inside the product itself, from what the account actually did with it.

That rule has a blind spot. A usage threshold only sees what happened inside your product, so it cannot tell whether five active seats belong to a two-person agency or to a 4,000-person enterprise trialling you with one team. The threshold scores both accounts the same and sends both to sales as equals. The fix is to look up the company behind the account, its headcount and funding stage, before the rule runs, so the score weighs who the customer is as well as what they did. Below you get the ideal pipeline flow, the single API call that fetches the company data, and the scoring function that uses it.

What does a product qualified leads API mean?

There is no product called a product qualified leads API. When people search for one, they want one of four ways to get "this account is ready for sales" out of a product and into a CRM, and each one is built differently:

  • An endpoint in your own app. Your application receives usage events (a login, a seat added, a project created), applies your qualification rule, and when an account passes, sends it to the CRM. You write and host all of it.

  • A reverse ETL sync. Your product data already lands in a warehouse such as Snowflake or BigQuery. You write a SQL query that decides who qualifies, and a sync tool such as Hightouch or Census copies the result into a CRM field on a schedule.

  • A CDP broadcast. A customer data platform such as Segment or RudderStack collects in-app events and forwards them to every connected tool, the CRM included. The platform moves the events. The scoring happens in whichever tool you configure to do it.

  • A scoring platform. A packaged product ingests your usage data, applies its own scoring model, and shows reps a ranked list of accounts.

It’s easy to mix these up. GitLab's version is a "talk to sales" button inside the product that posts to an internal GitLab endpoint, which then hands the lead to Marketo for scoring and routing. That is one name covering two systems, with no single API doing the whole job.

This guide builds the first option, the endpoint in your own app, and adds a step that the other three skip: a company lookup before the score is calculated. If you want the wider family of lead APIs mapped first, the four types of lead generation API lays them out side by side.

Why does a usage threshold misread two identical accounts?

Every published scoring rule shares the blind spot. Hightouch's SQL example adds a point each for viewing the pricing page, inviting a teammate, and being active daily, then subtracts a point if the signup email is a Gmail or similar personal address. Moesif defines PQLs by feature usage, engagement metrics, and other behavioral data. Your own rule probably uses the same handful of signals: seats used against seats allowed, monthly API calls, feature milestones passed, and pricing page views. All of them describe behaviour. None describes the company.

The personal-email penalty is the closest a published rule gets, and all it tells you is whether there is a business behind the signup, not which business or how big. DealHub's glossary suggests +25 points for a company size of 100 to 500, but it defines a PQL by in-product actions and says nothing about where the company size comes from or how it reaches the score.

So eight active seats at a two-person consultancy that will never need a ninth, and eight at a 4,000-person enterprise trialling you with one team, produce the same events and the same score. One is a starter plan. The other could be a six-figure rollout. No amount of tightening the usage rule fixes that, because company size and stage are not in your usage data. They have to come from outside, before the rule runs.

What does the pipeline look like with company data?

With company data, the pipeline runs in the same order. A usage event fires, your app looks up the company, the rule scores usage and company together, and the qualified account is sent to the CRM. Published pipelines use the same four steps in a different order. They score first and enrich afterwards: app, threshold, enrich, CRM. "Enrich" means calling a data API to attach company details, such as headcount and funding, to the record. Because that call happens after the threshold has already decided who qualifies, the company data never influences the decision. It only adds context to a lead that was picked on usage alone.

Published order:   [ app ] -> [ threshold ] -> [ enrich ] -> [ CRM ]

                                    ^ scores blind to the company



This build:        [ app ] -> [ enrich ] -> [ threshold ] -> [ CRM ]

                                    ^ score now reads both

Nothing is added or removed. The enrichment step moves ahead of the threshold, and the threshold now has two inputs instead of one.

The second decision is when to trigger the lookup. The obvious moment is sign-up, and it is the expensive one. Each company lookup costs 2 credits, and at sign-up you would pay that for every disposable email address and every account that never logs in a second time. Trigger the lookup on the first meaningful usage event instead, such as completing onboarding or creating a first project. By then, the account has come back at least once, so you only pay for accounts showing some intent, and the company record that you fetch is one the scoring rule will use straight away. 

Because the lookup happens before scoring, everything the rule needs arrives in one object. The usage signals from your app and the company block from the lookup travel in the same payload:

{
  "event": "first_real_action",
  "account_id": "acc_4821",
  "domain": "enterprise-prospect.com",
  "usage_signals": {
    "seats_active": 8,
    "monthly_api_calls": 4500,
    "feature_milestone_passed": true
  },
  "company_data": {
    "headcount": { "total": 4100 },
    "funding": { "last_round_type": "series_d" }
  }
}

This payload assumes you already have the account's domain. Often, you will – a work email signup hands it to you for free. But a large share of PLG signups arrive on a personal Gmail with no company domain attached, and skipping those means skipping some of your best accounts. The next section covers that case: you resolve the person to their company from their email first, then run the same lookup. Crustdata's company enrichment API fills the company block, drawing on 200+ million companies, and the section after this is the function that reads the object and makes the call.

How do you fetch company data before scoring?

You send POST /company/enrich with a domains array, and you get back an array with one entry per domain. Each entry holds a matches list, and each match carries a confidence_score and a company_data object.

Send the domain alone, and the response holds basic_info and little else. The score needs headcount and funding, so request those sections by name. Add exact_match so an ambiguous domain returns only companies whose primary domain is the one you sent.

curl -X POST https://api.crustdata.com/company/enrich \

  -H "Authorization: Bearer $CRUSTDATA_API_KEY" \

  -H "x-api-version: 2025-11-01" \

  -H "Content-Type: application/json" \

  -d '{

        "domains": ["enterprise-prospect.com"],

        "fields": ["headcount", "funding"],

        "exact_match": true

      }'

A hit returns the record you can score.

[

  {

    "matched_on": "enterprise-prospect.com",

    "match_type": "domain",

    "matches": [

      {

        "confidence_score": 1.0,

        "company_data": {

          "headcount": { "total": 4100 },

          "funding": { "last_round_type": "series_d" }

        }

      }

    ]

  }

]


A few rules keep this stable in production.

  • Enrichment costs 2 credits per record as a base, so you pay per account you look up.

  • The endpoint's default rate limit is 15 requests per minute, which is why you cache the record per account, not per event – a per-event call is the first thing to fall over under load. That 15 is a default, not a hard ceiling: it can be raised for higher-volume workloads, so treat it as a reason to cache rather than a cap on how many accounts you can enrich.

  • A domain that does not resolve comes back as a 200 with an empty matches array. The OpenAPI spec also allows a 404 for the same case, so handle both.

Bear in mind that there is no async retry and no auto-enrichment on this endpoint. A miss is synchronous and final, so handle the empty response in code rather than waiting for a backfill. If you have used enrichment for the downstream contact lookup that runs after a rep is already working on an account, this is the same call, moved to the front of the pipeline where the score can use it.

What if the signup has no company domain?

A work email gives you the domain directly – strip everything before the @ and you have it. Personal addresses don't: a gmail.com or outlook.com domain tells the enrich call nothing about the company, and sending it wastes a credit on a guaranteed miss. Rather than drop these accounts, resolve the person to their company first, then run the same company lookup you already have.

The move is two calls instead of one. Send the email to Crustdata's person enrichment endpoint, which returns the company the person works at, including its domain. Take that domain and feed it into the /company/enrich call above – everything downstream, the payload shape and the scoring function, stays exactly the same.

personal email ─▶ [ person enrich ] ─▶ company domain ─▶ [ company enrich ] ─▶ score

Gate this on the personal-domain case so you only pay for the extra resolution step when you have to: if the signup email already carries a business domain, skip straight to the company lookup.

How do you score usage and company fit together?

To use the company data, you extend the qualification rule rather than replace it. The rule now checks two things and qualifies an account only when both pass. Usage has to clear the threshold, and the company has to clear the gate. An account that is busy but belongs to the wrong kind of company fails the gate. A perfect-fit company that has barely logged in fails the threshold. Either failure means no handoff to sales. 

The gate reads two fields the enrich call already returned, headcount.total and funding.last_round_type. Both sit in the company block, so scoring costs you nothing extra. If you want a growth signal as well, headcount.growth_percent is there too, keyed on mom, qoq, six_months, yoy and two_years

The enrich endpoint names growth on mom, qoq, six_months and yoy , while the search endpoint keys the same idea on 1m, 3m, 6m and 12m.

It takes the usage signals and the first match's company block, and it returns whether the account qualified, plus the reason it did or did not. A domain with no match, or a company with no funding record, fails the gate rather than crashing the rule.

def qualify(usage, company):

    usage_ok = (

        usage["seats_active"] >= 5

        and usage["feature_milestone_passed"]

    )



    headcount = (company or {}).get("headcount") or {}

    funding = (company or {}).get("funding") or {}

    company_ok = (

        (headcount.get("total") or 0) >= 250

        and funding.get("last_round_type") in {

            "series_b", "series_c", "series_d",

        }

    )



    if usage_ok and company_ok:

        return {"qualified": True, "reason": "usage + company fit"}

    if usage_ok and not company_ok:

        return {"qualified": False, "reason": "usage only, company gate failed"}

    return {"qualified": False, "reason": "usage below threshold"}


Pass company as response[0]["matches"][0]["company_data"] when matches is non-empty, and None when it is.

Each match also carries a confidence_score, and it is worth reading before you trust the record. A 1.0 is an exact domain match you can score as-is; a lower score means the match is looser, which matters more when you have resolved the company from a personal email than when you sent a clean domain. Set a floor – treat anything above, say, 0.8 as reliable, and route lower-confidence matches to a rep for a manual check instead of auto-qualifying on data you are not sure about.

Run the two accounts from earlier through it. The two-person consultancy has its eight seats, so usage_ok is true, but headcount fails the gate, and it returns False with "company gate failed". The 4,000-person enterprise pilot clears both and returns True. Same event stream, opposite outcomes, which is the entire point.

The numbers we have used here are placeholders, so set your real boundary from your own closed-won accounts and find the band that actually converts, rather than borrowing point values from a glossary page. And if you would rather not reject on company fit, the same block can rank routing priority instead of deciding eligibility, so weaker-fit accounts still reach a rep, just lower in the queue. The firmographic attributes behind the gate are worth a look if you want the full field list.

Where do qualified accounts go next?

A True from the scoring function is only useful if it reaches a person. The qualified account posts to your CRM by webhook, and it brings both dimensions with it. The usage trigger that fired and the company attributes that cleared the gate ride along in the same record, so the rep opens it already knowing why it landed on their desk. No digging, no separate lookup, no guessing whether an eight-seat account is the small one or the big one.

This is the step where reverse ETL tools such as Hightouch and Census, and customer data platforms such as Segment and RudderStack, come in, and it helps to be clear about what they do. They move a record from one system to another, on a schedule or on an event. In this pipeline, that is their entire role. Your function has already decided who qualifies and delivers the result. If you let the sync tool or the CDP hold the qualification logic instead, the rule you just wrote ends up split between your code and a vendor's configuration screen, where nobody can read it end to end.

Two more pieces make this hold up over time.

  • Accounts change after they qualify, so re-scoring matters. Crustdata's Watcher API tracks fields like funding.last_round_type and hiring.openings_count, and fires only when one actually transitions. A company watch costs 5 credits per changed entity, so you pay for real movement rather than for polling.

  • Your existing users need this too, not only new signups. Enrichment runs in two modes. It runs in real time on the trigger event for anyone new, and as a scheduled job against Crustdata's batch enrich endpoint, which takes up to 10,000 domains in one request at the same 2 credits per record, to backfill the user base you already have.

If the account is being handed to an automated system rather than a human rep, our article on APIs for AI agents covers what changes when the reader on the other end is a machine.

Should you build this or buy a scoring platform?

The build-or-buy question is usually framed as two choices. Build the whole pipeline yourself, including sourcing the company data, or buy a lead-scoring platform that handles everything and shows your reps a ranked list. There is a third option, which is to write and own the scoring rule yourself, and call a data API for the one input you cannot generate in-house, i.e. the company data.

A scoring platform gives you a working model on day one, and a screen reps already know how to use. The trade is that the model encodes the vendor's idea of a good lead, and you usually cannot see or change how it weighs things. The third option costs you more up front, because you write the function and are responsible for it. In return, the rule stays in your code, and you can tune it to the accounts you have closed instead of to the vendor's average customer.

Route

What you get

What you own

What it costs

Build the whole pipeline

Full control of every stage.

Event tracking, scoring, and the data problem.

Engineering time across the board.

Buy a scoring platform like Pocus AI, or HeadsUp (acquired by Hightouch)

A ready model and a rep-facing surface.

Very little, and the vendor's scoring assumptions come with it.

A platform contract and sales-team adoption.

Keep your rules, call a data API (Crustdata)

The company dimension your score was missing, with your logic intact. Best for product-led teams that want to own the scoring and skip building a data layer.

Your scoring rules and the request that fills the company block.

Credit cost per call, quoted on volume.

Buying is fast and rents you someone else's assumptions. Building everything gives you full control and leaves you sourcing and maintaining company data yourself. The middle option keeps the rule in your code and outsources only the data. Crustdata charges for that data in credits per call, with dollar pricing quoted on volume. If you are weighing the data layer itself, our article on B2B data API providers lines the options up side by side.

Putting the company dimension in your pipeline

Start with a single company attribute in the gate. Headcount is the easiest to reason about and the fastest way to separate a small account from a large one, so add one headcount.total check to your existing usage threshold, ship it, and watch how the list of qualified accounts changes over the next few weeks.

Your first threshold will be a guess. To correct it, pull the accounts that went on to buy, check the headcount range they fall in, and move the boundary to match. Add a second attribute, funding stage or headcount growth, once you can see the first one improving the list your reps receive.

Crustdata's signup enrichment runs the same company lookup at account creation instead of at first usage. Product-led growth teams use it to route and qualify leads on day one. For this build, it is also a low-cost way to check the company data you get back for your real signups before you make the scoring rule depend on it.

Frequently asked questions

What's the difference between a PQL and an SQL?

A PQL is flagged automatically when an account's product usage crosses your thresholds. An SQL is a lead a sales rep has looked at and accepted as worth pursuing. In this setup, the account that lands in your CRM is a PQL – it becomes an SQL the moment a rep picks it up, which they can do fast because the usage and company signals that qualified it are right there in the record.

Won't adding a company lookup slow down my product?

No – the lookup runs on the first real usage event, not during signup or page loads, so users never wait on it. And because the result is cached per account, it only runs once no matter how many events follow. If you'd rather not block at all, run it in the background and let the score catch up on the next event.

How accurate is the company data behind the score?

Each match comes back with a confidence score, so you can auto-qualify only the strong matches and send the rest to a rep to eyeball. Company details also go stale – funding rounds close, headcount grows – so it's worth re-checking accounts that qualified a while ago rather than trusting a number indefinitely. Start with one attribute and watch how your qualified list changes before you lean on it.

Do I need a CDP or reverse ETL tool to build a PQL pipeline?

No. Tools like Segment and Hightouch move records between systems; they don't decide who qualifies. All you actually need is your app to catch the usage event, one call to fetch the company data, and a webhook into your CRM. If you already run a CDP, it's fine for the delivery step – just keep the scoring logic in your own code.

Is company enrichment a privacy risk?

It's lower-risk than it sounds, because you're pulling facts about the company – headcount, funding, growth – not personal details about the individual user. Even when you start from a personal email, the goal is to identify the employer, not to build a profile of the person.



Data

Delivery Methods

Use Cases

Solutions

Resources

Sign In

Sign In