Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Enrich B2B Product Signups (Identify, Qualify, Route)

One API call per work-email signup tells you who they are, whether they fit, and who should follow up.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

Enrichment at signup means one call. Call POST /v1/people/enrich with the signup's work email, read title, level and company size from the response, and route on three rules. You do not need a data team for this. You need one webhook handler.

TL;DR

  • Identify: enrich the signup email. A hit returns the person and their current role. A miss is a 404 and costs nothing.
  • Qualify: size your target market first with a free count, then score each signup against it. Takes about 1 hour to wire up.
  • Route: send fits to a human, send the rest to self-serve onboarding. It spends 1 record per signup we hold, so the free 1,000 covers your first 1,000 signups.

Prerequisites

You need roughly an hour and one engineer. Nothing here needs a warehouse or a CRM, though you can send the result to both later.

  • A LeadOcean API key in $LEADOCEAN_API_KEY. The free plan gives 1,000 records for life, no card, 5 requests a second.
  • A signup event with an email address (webhook, queue or a nightly export).
  • Any HTTP client. The examples use curl and Python.

Step 1: Skip addresses that cannot match

Drop free-mail domains before you spend anything. Personal addresses rarely resolve to a company, and a plain string check is free.

python
FREE_MAIL = {"gmail.com", "yahoo.com", "outlook.com", "hotmail.com", "icloud.com", "proton.me"}

def is_work_email(email: str) -> bool:
    return email.split("@")[-1].lower() not in FREE_MAIL

Route skipped signups to self-serve. Do not guess their employer from the name.

Step 2: Identify the signup

Do this on the server, in the handler that receives the signup event. Never call the API from the browser, because the key would ship to every visitor.

Enrich takes exactly one of linkedin_url, email, phone or person_id. Send the email.

bash
curl -s -X POST https://api.leadocean.io/v1/people/enrich \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "jane.doe@acme.com"}'

A 200 returns the person. A 404 means we hold nothing, and nothing is billed. Treat a 404 as "unknown" and route to self-serve.

Contact points (the verified address, phones) come back only when you send reveal_email: true. For routing you rarely need them. You already have the email.

Step 3: Size the market you are scoring against

Decide what a fit is before you score. Count the audience for free, as many times as you like. This count is for engineering VPs and C-level at 51-200 person US companies. It is mailable (verified, catch-all valid or catch-all email), not everyone matching.

python
import os, requests

r = requests.post(
    "https://api.leadocean.io/v1/people/search?count=true",
    headers={"x-api-key": os.environ["LEADOCEAN_API_KEY"]},
    json={
        "jobLevel": ["C-Team", "VP"],
        "jobFunction": ["Engineering"],
        "employeeRange": ["51-200"],
        "country": ["US"],
        "limit": 1,
    },
)
print(r.json()["meta"]["total"])

If the number is tiny, your ideal customer profile is narrow and a human should see most fits. If it is huge, route by a stricter rule.

Step 4: Qualify with three rules

Most teams over-build this. A signup form already tells you the email and maybe a company name. Enrichment adds the three things a human would look up by hand: seniority, function and company size. Sales wants senior buyers at companies big enough to pay. Everyone else gets the product-led path, which is cheaper to run.

Cache misses too, so you do not re-ask for people we do not hold.

Keep scoring boring. Three fields decide most routes: job level, job function and company headcount. They sit on the current role, data.contact_data.contact_current_experiences[0]. Per the enrich docs, a role carries job_seniority, job_functions[] and company_employees.number_of_employees.

python
def route(data: dict) -> str:
    roles = data["contact_data"].get("contact_current_experiences") or [{}]
    role = roles[0]
    level = (role.get("job_seniority") or {}).get("job_level")
    functions = role.get("job_functions") or []
    size = (role.get("company_employees") or {}).get("number_of_employees") or 0
    if level in {"C-Team", "VP", "Director"} and 51 <= size <= 1000:
        return "sales"
    if "Engineering" in functions:
        return "dev-nurture"
    return "self-serve"

Print one real response before you ship it, and adjust if a value comes back as an object rather than a string.

Step 5: Route in your webhook

Put the three calls together. Keep the handler under your plan's rate limit (5 a second on Free, 200 on Agency) and return fast.

python
def on_signup(event):
    email = event["email"]
    if not is_work_email(email):
        return enqueue("self-serve", email)
    r = requests.post(
        "https://api.leadocean.io/v1/people/enrich",
        headers={"x-api-key": os.environ["LEADOCEAN_API_KEY"]},
        json={"email": email},
        timeout=10,
    )
    if r.status_code == 404:
        return enqueue("self-serve", email)
    r.raise_for_status()
    data = r.json()["data"]
    enqueue(route(data), email, data)

Enqueue to whatever you already run: a Slack alert, a CRM task, an n8n flow. Our integration pages cover Clay, n8n, Make and Zapier.

Step 6: Make it safe to retry

Signup webhooks fire twice. Store the result keyed by email and check your store before you call. A repeat enrich of the same email spends a second record, so the cache pays for itself on the first duplicate.

python
def enrich_once(email: str):
    cached = store.get(email)
    if cached is not None:
        return cached
    r = requests.post(
        "https://api.leadocean.io/v1/people/enrich",
        headers={"x-api-key": os.environ["LEADOCEAN_API_KEY"]},
        json={"email": email},
        timeout=10,
    )
    result = None if r.status_code == 404 else r.json()["data"]
    store.set(email, result)
    return result

Handle 429 by sleeping for the Retry-After value and retrying once. Do not retry a 404.

Step 7: Add career history for the borderline cases

Since October 2026 person enrich also returns resume_data: past roles, education, certifications and awards, each when held. Use it as a tiebreaker. A new signup whose past role was a founder or a VP at a larger company is worth a human look even at a small employer. Past employers carry no company_id yet, so match them by name.

Step 8: Check the funnel after a week

Count how many signups came back as sales, dev-nurture and self-serve. Then check which route converts. If sales fits convert no better than the rest, your rules are wrong, not the data. Tighten the level or size band and rerun the free count from Step 3 to see how big the new target is.

Keep the rules in one function so you change them in one place. Resist adding a fourth rule until the first three have a week of traffic behind them.

Step 9: Decide what to store

Store the route, the reason and the date, not the whole response. A one-line reason such as "VP, 51-200, Engineering" lets a rep see why they got the lead. Keep the raw profile_data only if you plan to rescore later, and delete it when you stop needing it.

If sales asks for a phone number on the hottest fits, call GET /v2/people/phone for those people only. Do not reveal contact data for every signup. You already hold their email, and the extra lookup spends records on people nobody will call.

Rescore on a schedule, not on every page view. People change jobs, and the dataset refreshes monthly, so a monthly rerun over your open signups is enough.

What you get

The free count from Step 3, run on 21 October 2026:

code
leadocean_count_leads
  jobLevel:      C-Team, VP
  jobFunction:   Engineering
  employeeRange: 51-200
  country:       US

total:              10,149   (mailable: verified, catch_all_valid, catch_all)
totalAll:           28,362   (same filters, no email condition)
distinctCompanies:   7,262
credits:                 0

So 10,149 mailable buyers at 7,262 companies match that profile. A signup that lands in that set is a fit.

Enrich response shape from the API reference, with placeholder values (abbreviated):

json
{
  "success": true,
  "data": {
    "profile_data": { "profile_full_name": "Jane Doe", "profile_url": "https://www.linkedin.com/in/jane-doe" },
    "contact_data": {
      "has_email": true,
      "has_phone": false,
      "email_status": "verified",
      "email_type": "work",
      "contact_current_experiences": [{
        "company_name": "Acme",
        "company_domain": "acme.com",
        "job_title": "VP Engineering",
        "company_employees": { "number_of_employees": 120 }
      }]
    },
    "resume_data": { "...": "past roles, educations, certifications" }
  },
  "meta": { "key_hash": "..." }
}

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerCheck $LEADOCEAN_API_KEY is exported and sent as x-api-key.
404 on enrichWe hold nothing for that identifier, or no identifier we can enrich byRoute to self-serve. It is not billed. Retrying will not change it.
429Above your plan's rate limit (5 a second on Free, 200 on Agency)Honor the Retry-After header and queue signups instead of calling inline.
402Records spent (1,000 on Free, for life)Check GET /v1/usage, then move to Solo ($97 a month) or Agency ($497 a month).
Empty search resultsA bad enum value, such as VP spelled Vice PresidentList valid values with GET /v1/enums or leadocean_list_enum_values.
Export refusedOver 50,000 rows a request or 500,000 a daySplit the export into smaller filtered requests.

FAQ

How many records does signup enrichment use?

One per signup we find and return. A signup we do not hold is a 404 and costs nothing. Counts and filter lookups are free too.

Should I enrich every signup?

No. Skip free-mail addresses and obvious test accounts first. Enrich the rest, then route on the result. See the sign-up enrichment workflow for the full flow, and how PLG products use enriched signup data for what to do with it. More guides are on the blog.

Can I enrich by name and domain?

Not directly. There is no name plus domain lookup. Search by domain first, then enrich by person_id.

What does it cost at volume?

The free plan covers your first 1,000 records. After that, Solo is $97 a month for one person and Agency is $497 a month for teams, with a fair-use pace rather than credits. See pricing.

How fresh is the data?

The dataset is refreshed monthly, and each record carries its own fetched_at date. A signup who changed jobs last week may still show the old role.

Enrich your next signup in one call

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →