Route Leads Automatically by Credit Tier With Webhooks

A working pattern for sending every consenting lead to the right person within seconds: a lookup on form submit, a signed webhook, a small router that reads the score and the phone signals, and a queue for everything that doesn't fit.

A laptop showing lines of code on a bright white desk next to a monitor, a lamp and a phone
Photo by Christopher Gower on Unsplash
On this page
  1. The shape of the system
  2. Events worth subscribing to
  3. Verify every webhook before you trust it
  4. Lead routing by credit score: fetch, then assign a tier
  5. The leads that don’t fit
  6. Consent, fairness and the phone call after
  7. Testing it before real leads hit it
  8. What it costs to run
  9. Where to go from here

Lead routing by credit score means every lead that consents to a credit check gets scored the moment they submit, and your system sends them to the right person or product automatically: strong files to your closers, middle files to the standard queue, thin or troubled files to second-look options, and anything suspicious to a human. With our API, the lookup runs when the form is submitted, a signed lookup.completed webhook tells your server it’s done, and a small router you write assigns the tier.

This post is the build. It assumes you already have a lead form, a CRM and someone who can write a little server code. If you’re starting from scratch, our credit score API guide covers the basics first.

The shape of the system

Five moving parts, in this order:

  1. The form collects the phone number and a clear consent checkbox.
  2. Your server calls POST /v1/lookups with the phone, purpose=consent and your CRM’s lead ID as reference.
  3. Our webhook posts a signed event to your endpoint when the lookup finishes.
  4. Your router fetches the full result, applies your tier rules and writes the tier to the CRM.
  5. The CRM assigns the lead to a queue or a rep based on that tier.

You might wonder why you’d bother with a webhook at all, since the lookup returns its answer in seconds. On a simple form you can route straight from the response. The webhook earns its keep when lookups start in more than one place (a web form, a rep’s dialer, a partner integration), when you want one router handling all of them, and when you want an event trail you can replay. It also means a slow CRM write never holds up the person waiting on your form.

Events worth subscribing to

You choose which events go to your URL in the API settings. For routing, these are the ones that matter:

Event When it fires What your router does
lookup.completed A lookup found someone Fetch the result, assign a tier
lookup.no_match Nobody found for that number (no charge) Send the lead to a manual queue
usage.threshold Monthly spend passed one of your alert levels Notify whoever owns the budget
key.changed A key was created, rolled or revoked Log it for your audit trail

Every event carries an id, a type, a created timestamp, a livemode flag and a data object. For lookups, data includes the lookup_id, the phone number and the reference you sent. That reference is the whole trick: it ties the event back to the exact lead in your CRM without any matching logic.

Verify every webhook before you trust it

Your webhook URL is public. Anyone can post to it. So the first thing your endpoint does is check the PNCS-Signature header, which looks like t=1762771500,v1=5d1c.... The v1 value is an HMAC-SHA256 of the timestamp, a period and the raw request body, signed with your webhook secret.

Three rules: use the raw body (not re-serialized JSON), compare in constant time, and reject anything older than five minutes so an old captured request can’t be replayed.

import hashlib, hmac, os, time

def verify(raw_body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(
        os.environ["PNCS_WEBHOOK_SECRET"].encode(),
        parts["t"].encode() + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts.get("v1", ""))

After that, answer fast. Return a 2xx as soon as the event is verified and stored, and do the actual routing in a background job. Failed deliveries are retried, which is good for reliability and means you’ll occasionally see the same event twice. Keep a table of processed event IDs and skip any you’ve already handled.

Lead routing by credit score: fetch, then assign a tier

The event tells you a lookup finished. Your worker then calls GET /v1/lookups/{id} to get the full person: name, address, phone_details (line type, carrier, caller ID name and a 0 to 100 fraud_score), the score object with the FICO Score 4 value and its key factors, the profile (total debt, credit usage, on-time payment rate, collections) and income with an estimated debt-to-income.

Then the router does something deliberately simple. Here’s an example rule set for a fictional home improvement lender:

Tier Rule Where it goes
Review Fraud score 70 or higher, or a VoIP line whose name doesn’t match the lead A person checks it before anyone calls
A FICO 4 of 720 or higher, debt-to-income under 36% Senior closers, called first
B FICO 4 from 660 to 719 Standard sales queue
C FICO 4 from 580 to 659 Second-look lender program or a smaller project option
D Below 580 Nurture track with credit-building resources
Manual No match, frozen file or too new to score A rep calls and collects details

The thresholds are yours to set. What matters is the order: check for fraud first, then score, then the edge cases. A 790 on a disposable line with a mismatched name is not a tier A lead.

And here’s the core of the router, stripped to the logic:

# r is the lookup result; lead_name is what the person typed on your form
def tier(r, lead_name):
    phone, score = r["phone_details"], r.get("score") or {}
    name_ok = r["name"].split()[-1].lower() in lead_name.lower()
    if phone["fraud_score"] >= 70 or (phone["line_type"] == "voip" and not name_ok):
        return "review"
    if not score.get("value"):
        return "manual"
    v, dti = score["value"], (r.get("income") or {}).get("debt_to_income", 1)
    if v >= 720 and dti < 0.36: return "A"
    if v >= 660: return "B"
    if v >= 580: return "C"
    return "D"

Write the tier, the score, the top factor and the lookup ID onto the CRM record. Your reps should see why a lead is tier C (“balances on revolving accounts are too high”) so the first call is useful.

Two people sketching a process on a whiteboard with markers, boxes and arrows drawn across it
Photo by Kaleidico on Unsplash

The leads that don’t fit

A routing system is judged by what happens to the awkward cases, not the easy ones.

No match. Sometimes a number doesn’t lead to anyone, often because it’s new, prepaid or shared on a family plan. You aren’t charged. Route these to a person, and if the lead agrees, run the lookup again with their name and address sent alongside the phone.

Frozen files. The response flags it. That’s a real person who protected their credit. A rep can explain that they’ll need to lift the freeze with TransUnion before a check can go through.

Too new to score. Thin files are often young or new-to-the-country customers, not bad ones. A manual review with income documents can turn them into good loans.

The tier B lead who’s one paydown from tier A. If the top factor is card balances, tell the rep. It’s a different conversation.

Speed is pointless if the process doesn’t hold up. Four things to bake in from day one, and since none of this is legal advice, have your counsel read the consent wording and tier rules before launch.

A permissible purpose on every lookup. Under the FCRA (15 U.S.C. § 1681b), you need one, and for lead forms that’s the person’s written consent. A checkbox they actively tick, in plain words, before you call the API. Store who agreed, when, and the exact text they saw. Never run lookups on purchased lists or cold leads who didn’t ask; that isn’t allowed, and we turn off keys that do it. Our post on credit check consent forms has wording that works.

Tiers built on creditworthiness, applied the same way to everyone. If you’re a creditor, the Equal Credit Opportunity Act’s Regulation B (12 CFR 1002.4) bars discrimination on a prohibited basis and discouraging applicants on one. Route on score, income and fraud signals only. Never on zip code as a stand-in for anything, never on name, never on anything the lead told you about age or family.

Adverse action when a report changes the answer. If a lead is told they don’t qualify, or gets worse terms, because of what the report said, an adverse action notice is required under 15 U.S.C. § 1681m. The response includes the score, its key factors, the bureau (TransUnion) and a timestamp, so you have what the notice needs.

Separate permission to call or text. Consent to pull credit is not consent to market. Automated calls and texts have their own rules under the TCPA (47 U.S.C. § 227). Collect that consent separately on the same form. Our TCPA guide for calling and texting leads walks through it.

Testing it before real leads hit it

Keys that start with pncs_test_ return example people for any number and are never charged. Point a test key at a staging copy of your router, send yourself the test lookup.completed event from the API settings, and check that:

  • a bad signature gets a 401 and nothing is processed,
  • the same event delivered twice is processed once,
  • each tier lands in the right CRM queue,
  • a no-match lead shows up in the manual queue with the reason attached.

Send an Idempotency-Key header on every POST /v1/lookups too, so a retried form submission never runs, or charges for, the same lookup twice.

What it costs to run

Pricing is simple: $49 a month or $149 a year for the API plan, plus $7.50 for each lookup that finds a match, with no monthly limit. No-matches are free. So a team routing 300 consenting leads a month pays $49 plus $2,250 (300 × $7.50), and the cost of each lookup is easy to put next to the value of one closed deal. At 1,000 or more lookups a month, you qualify for volume rates; email [email protected] to get them. Our breakdown of credit API pricing per report has more worked examples.

Set a spending limit on the account and subscribe to usage.threshold, so a form spam attack or a bug in a loop can’t run up a surprise bill.

Where to go from here

Once the router works, the interesting part is tuning it. Pull a month of tier assignments, compare them against what actually closed, and move your thresholds. Most teams find their tier B and C lines aren’t where they guessed.

If your CRM has a native integration path, connecting credit lookups to your CRM covers the patterns. For brokers scoring leads across several lenders, see loan lead scoring, and for the front end of all this, pre-qualifying leads with a phone number. The full endpoint list and webhook settings live on our API page.

Share this article LinkedIn X Facebook Email

Written by

Jordan AveryHead of Content

Jordan Avery runs the blog at Phone Number Credit Score and writes about phone data, credit scores, tenant and lead screening, and the rules that come with all of it. Most posts start as a question a customer asked us.

Try it now

Have a phone number? Find their credit score.

Name, current address and real FICO® Score 4 from TransUnion data. Results in seconds. As low as $9.99.

Have a phone number? Find their score.

Type in any U.S. phone number. We’ll tell you who it belongs to, where they live and their real credit score.

  • Soft pull, they’re never notified
  • Results in seconds
  • As low as $9.99

Use it for a permissible purpose, like the person’s OK. The rules