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

How-to guide

How to Find Your Best-Converting Verticals from Signup Data (PLG, API recipe)

Enrich the signups who activated, cut conversion by industry and company size, then size the winning vertical for free.

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

You can rank your verticals by paid conversion in an afternoon. Enrich the signups who activated, group them by industry and headcount, and divide paid by activated. Enrichment is one API call per user. The rest is a GROUP BY.

TL;DR

  • Enrich only activated users with a work email, then compute conversion per industry and per headcount band.
  • First run takes about 45 minutes and spends at most 800 records, so it fits inside the free plan's 1,000.
  • Size the winning vertical with a free count call before you move any budget towards it.

Prerequisites

  • A LeadOcean API key in $LEADOCEAN_API_KEY. The free plan has the search and enrich scopes (pricing).
  • A signups export with three columns: email, activated_at, paid_at.
  • Python 3.9 or later with requests, plus SQLite or any SQL engine.
  • A definition of "activated". Pick the first action that proves real use (first project created, first message sent), not the account creation.

Step 1: Pick who to enrich

Enrich users after activation and before payment. Tourists who never return add noise, and waiting for payment gives you a small, biased sample of survivors.

Export the activated cohort and drop webmail addresses. A work email ties the user to a company domain. A personal Gmail tells you little about the employer, so leave it out of this run.

sql
-- activated, work email only, one row per person
SELECT lower(email) AS email,
       paid_at IS NOT NULL AS paid
FROM signups
WHERE activated_at IS NOT NULL
  AND signup_date >= date('now', '-12 months')
  AND substr(email, instr(email, '@') + 1) NOT IN
      ('gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'icloud.com', 'proton.me')
LIMIT 800;

Save the result as activated.csv. The 800 cap keeps the first run inside the free plan. Misses are free, so the real spend is usually lower.

Step 2: Enrich each email

POST /v1/people/enrich takes exactly one of linkedin_url, email, phone or person_id. It costs one record. A person we hold nothing for returns 404 and costs nothing.

bash
curl -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"}'

Leave reveal_email off. You want the company, not the contact points, and the call costs the same either way.

Read the current role from contact_data.contact_current_experiences[]. resume_data.experiences[] holds past roles only, so do not read the current employer from it. The fields you need are company_domain, company_industry, company_employees.number_of_employees, job_title and job_seniority.

Step 3: Run the batch

This script reads the CSV, enriches each email once, and writes enriched.csv. The time.sleep(0.25) keeps it at about 4 requests a second, under the Free plan's 5 a second per account. On Solo (50 a second) or Agency (200) you can shorten it.

python
import csv, os, time, requests

URL = "https://api.leadocean.io/v1/people/enrich"
HEADERS = {"x-api-key": os.environ["LEADOCEAN_API_KEY"], "content-type": "application/json"}

def current_role(person):
    contact = person.get("contact_data") or {}
    exps = contact.get("contact_current_experiences") or []
    return exps[0] if exps else {}

def size_band(n):
    if n is None: return "unknown"
    for cap, label in [(10, "1-10"), (50, "11-50"), (200, "51-200"), (500, "201-500"), (1000, "501-1000")]:
        if n <= cap: return label
    return "1001+"

rows = list(csv.DictReader(open("activated.csv")))
out = []
for r in rows:
    for _ in range(5):
        resp = requests.post(URL, headers=HEADERS, json={"email": r["email"]}, timeout=30)
        if resp.status_code != 429:      # 429: over your plan's rate, wait and retry
            break
        time.sleep(int(resp.headers.get("Retry-After", "1")))
    if resp.status_code == 404:          # nothing held, nothing charged
        continue
    if resp.status_code != 200:
        print(r["email"], resp.status_code, resp.text[:120]); break
    role = current_role(resp.json()["data"])
    headcount = (role.get("company_employees") or {}).get("number_of_employees")
    out.append({"email": r["email"], "paid": r["paid"],
                "industry": role.get("company_industry") or "unknown",
                "size": size_band(headcount),
                "seniority": role.get("job_seniority") or "unknown"})
    time.sleep(0.25)

with open("enriched.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=["email", "paid", "industry", "size", "seniority"])
    w.writeheader(); w.writerows(out)
print(f"{len(out)} enriched of {len(rows)}")

The break on any other status is deliberate. A 402 means the free records are spent, and retrying into it gets you nothing.

Step 4: Rank the verticals

Load enriched.csv into SQLite (.import --csv enriched.csv enriched) and group. Conversion here means paid divided by activated, so it is a ratio of two counts you already trust.

sql
SELECT industry,
       COUNT(*)                                   AS activated,
       SUM(paid)                                  AS paid,
       ROUND(100.0 * SUM(paid) / COUNT(*), 1)     AS paid_pct
FROM enriched
WHERE industry <> 'unknown'
GROUP BY industry
HAVING COUNT(*) >= 30
ORDER BY paid_pct DESC;

Change industry to size for the headcount cut, or to industry, size for the cross.

Two rules keep you honest. Drop segments under 30 activated users, because three paid out of ten is not a rate. Then rerun the same query on retention or expansion if you have them, since a 12 percent vertical that stays beats a 20 percent one that churns.

On the free plan the 30-user floor leaves you three or four verticals. That is enough to find a lead. A paid plan is worth it when you want to enrich the whole base and rerun this every quarter. Solo is $97 a month for one person, Agency $497 a month for a team.

Step 5: Size the winner before you act

A vertical that converts well but has few buyers is a trap. Check the market with a free count call. Add count=true and limit=1 as query parameters, and put the filters in a flat JSON body. Arrays hold exact enum strings.

bash
curl -X POST "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "industry": ["Insurance", "Insurance Carriers", "Insurance Agencies and Brokerages"],
    "employeeRange": ["51-200"],
    "jobLevel": ["Director", "VP"],
    "country": ["US"]
  }'

meta.total is the audience size, capped at 100,000, and it spends no records. Agents get the same call from the free leadocean_count_leads tool on the MCP server.

Industry is a closed list of 534 values, and one real-world vertical often maps to several. Look them up before you filter, as shown next.

What you get

The industry lookup is free. This is the real result of leadocean_list_enum_values with name: industry and q: insurance, run on 2026-10-01:

json
{
  "name": "industry",
  "description": "Company industry. One of 534 normalised values.",
  "total": 534,
  "matched": 4,
  "values": [
    "Insurance",
    "Insurance Agencies and Brokerages",
    "Insurance Carriers",
    "Insurance and Employee Benefit Funds"
  ]
}

That is why Step 5 passes several insurance values. Your GROUP BY industry in Step 4 will also split one vertical across these labels, so merge them with a small mapping table before you read the ranking.

The enrich call returns this shape (response shape from the API reference, placeholder values).

json
{
  "success": true,
  "data": {
    "profile_data": { "profile_full_name": "Jane Doe" },
    "contact_data": {
      "has_email": true,
      "email_status": "verified",
      "contact_current_experiences": [{
        "company_name": "Acme",
        "company_domain": "acme.com",
        "job_title": "Director of Operations",
        "company_employees": { "number_of_employees": 120 }
      }]
    }
  },
  "meta": { "credits": 1 }
}

Step 4 prints one row per industry in this form.

industryactivatedpaidpaid_pct
(your top industry)nn%

We have not run this on your signups, so there is no ranking to show. The table is the format, not a result.

Troubleshooting

ErrorCauseFix
401Missing, wrong or revoked keyCheck x-api-key and that $LEADOCEAN_API_KEY is exported in the shell running the script
402The free plan's 1,000 records are spent. They do not resetMove to Solo ($97 a month) or Agency ($497 a month), or analyse what you already enriched
404 on enrichWe hold nothing for that emailCosts nothing. Skip it and report the match rate
429Faster than your plan's rate per account: 5 a second on Free, 50 on Solo, 200 on AgencyWait the Retry-After seconds, then retry
400Two identifiers in one enrich call, or a bad fieldSend exactly one of linkedin_url, email, phone or person_id and read details
503 with no_sourceThe store behind that call is briefly unreachableBack off and retry. It is never billed
Count returns 0An industry or level string that is not an exact enum valueLook it up with leadocean_list_enum_values and copy the string as returned

FAQ

How many signups do I need before the ranking means anything?

Aim for at least 30 activated users per segment on a first pass and 100 before you reallocate budget. Smaller segments swing on one or two accounts. Combine adjacent industries or wait a quarter.

Should I enrich at signup, after activation or after payment?

After activation, before payment. Signup enrichment spends records on users who never return. Post-payment gives you a thin sample that arrives months late. Activation is the point where intent is real and the insight is still useful.

What about signups with personal emails?

This recipe drops them, because a personal address says little about the employer. Ask for the company at onboarding, or enrich by linkedin_url where you hold one. For the rest, treat the segment as unknown rather than guessing.

Does this work for technographic segments too?

Yes. Industry is one cut. You can also size a segment by detected technology, as in the technographic prompt, and compare it with the same conversion query.

Where do I find the people in the winning vertical?

Search with the same filters, then enrich or export. Product Hunt launchers are one source, and the marketing database examples show others. Browse more in the blog.

Find out which vertical your product already wins

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

Get your free API key →