A HubSpot fit score needs filled properties. Most CRMs have a name and an email and little else, so the score returns zero for half the list. This guide fills job title, seniority, company size and industry first, then builds the score on top.
TL;DR
- Time: about 45 minutes for the first score.
- Cost: up to 100 LeadOcean records on a test batch. The Free plan has 1,000 records for life and needs no card.
- Result: a contact fit score with a threshold label (A, B or C) that you can use in segments and workflows.
Prerequisites
- HubSpot Marketing Hub Professional or Enterprise for contact scores. Company scores work with Marketing Hub or Sales Hub (HubSpot docs, October 2026).
- Edit permission for Lead Scoring in HubSpot.
- A HubSpot private app token with contact read and write scopes, in
$HUBSPOT_TOKEN. - A LeadOcean key in
$LEADOCEAN_API_KEY. Get one at app.leadocean.io/register. curlandjq.
Step 1: Pick three fit signals and size them
Start with three signals you can fill from data: seniority, company size and industry. A score with three clear rules beats one with thirty guesses.
Size your target list before you build anything. A count is free and spends no records. Send count=true as a query parameter with limit 1 (LeadOcean docs, October 2026).
curl -X POST "https://api.leadocean.io/v1/people/search?count=true&facets=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{
"jobLevel": ["C-Team", "VP", "Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"industry": ["Software Development"],
"limit": 1
}'meta.total is capped at 100,000. The data array comes back empty and meta.countOnly is true. With facets=true, meta.facets breaks the total down by seniority, country, industry and company size.
Read the facets in the response before you pick point values. If 70 percent of your target list is at directors, a rule that gives directors the top score tells you nothing. Spread the points where the list varies.
Step 2: Create the score properties in HubSpot
HubSpot stores fit data in contact properties. Create three custom ones through the properties API (HubSpot docs, October 2026). Each needs a groupName, type and fieldType.
for P in "lo_seniority:Seniority (LeadOcean):string:text" \
"lo_company_employees:Company employees (LeadOcean):number:number" \
"lo_industry:Industry (LeadOcean):string:text"; do
IFS=: read NAME LABEL TYPE FIELD <<< "$P"
curl -s -X POST "https://api.hubapi.com/crm/v3/properties/contacts" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "content-type: application/json" \
-d "{\"name\":\"$NAME\",\"label\":\"$LABEL\",\"groupName\":\"contactinformation\",\"type\":\"$TYPE\",\"fieldType\":\"$FIELD\"}"
doneYou can also create them by hand in Settings, Properties. The API route is faster when you repeat this for several portals.
Why custom properties and not the built-in ones? Built-in fields such as job title are often typed by the contact on a form. They are free text, so "VP Sales", "Vice President, Sales" and "vp of sales" all differ. A normalised seniority value gives every rule one exact string to match.
Step 3: Enrich a test batch from LeadOcean
Enrich by email. POST /v1/people/enrich takes exactly one of linkedin_url, email, phone or person_id. A person LeadOcean holds nothing for is a 404 and costs nothing (LeadOcean docs, October 2026).
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"}' \
| jq '.data.contact_data.contact_current_experiences[0]
| {seniority: .job_seniority.job_level,
employees: .company_employees.number_of_employees,
industry: .company_industry}'You do not need reveal_email here. The fit score needs role and company data, not the address. Each hit is one record.
Step 4: Write the values back to HubSpot
Update each contact by email with PATCH /crm/v3/objects/contacts/{email}?idProperty=email (HubSpot docs, October 2026).
#!/usr/bin/env bash
: "emails.txt holds one email per line. Start with 100 rows."
while read -r EMAIL; do
OUT=$(curl -s -f -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d "{\"email\":\"$EMAIL\"}" \
| jq -c '.data.contact_data.contact_current_experiences[0]
| {lo_seniority: .job_seniority.job_level,
lo_company_employees: (.company_employees.number_of_employees | tostring),
lo_industry: .company_industry}') || { echo "miss: $EMAIL"; continue; }
curl -s -X PATCH "https://api.hubapi.com/crm/v3/objects/contacts/$EMAIL?idProperty=email" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "content-type: application/json" \
-d "{\"properties\": $OUT}" > /dev/null
sleep 0.25 # stays under 5 requests a second on Free
done < emails.txtMisses print miss: and are skipped. They cost nothing. Check the first ten contacts in HubSpot before you run the rest.
Run a hundred contacts first, not the whole database. Open ten of them in HubSpot and compare the values with the contact's LinkedIn page. If the three fields look right, run the rest in batches of a few thousand.
Step 5: Build the fit score
Open Marketing, Lead Scoring, then Create score. Choose Contacts, then Contact fit score (HubSpot docs, October 2026).
Add one property group and these rules. The points are a starting point. Change them after you see the distribution.
| Property | Condition | Points |
|---|---|---|
| Seniority (LeadOcean) | is any of C-Team, VP | +25 |
| Seniority (LeadOcean) | is Director | +15 |
| Company employees (LeadOcean) | between 51 and 1000 | +25 |
| Industry (LeadOcean) | is any of Software Development, IT Services and IT Consulting | +20 |
| Seniority (LeadOcean) | is unknown | -10 |
The default maximum is 100 points, and the Score limit dropdown changes it. Criteria inside one rule use AND logic, so keep each row above as its own rule.
Negative points matter as much as positive ones. A contact with no seniority at all is usually a weak record or a personal address. The minus 10 pushes it down the list instead of leaving it in the middle. Keep the group at three to five rules for now. You can add a second group for engagement once you have a month of event history.
Under the Contacts tab, choose Score specific contacts and pick a segment for your target region. This keeps students and personal addresses out of the score.
Step 6: Set thresholds, test and turn on
Thresholds decide who sales sees, so set them against your own distribution. A threshold copied from a blog post fits someone else's list.
Click Score thresholds in the left sidebar of the score editor. HubSpot creates a label property that runs from A (high fit) to C (low fit). Set the cut points from your data, not from a template.
- Click Test a contact and enter three contacts you know. Check each rule fires.
- Click Preview distribution. If almost everyone lands in C, the rules are too strict.
- Set A at 60 or above and B at 35 or above as a first pass.
- Click Review and turn on.
The score value and the label then sit in contact properties. Use them in segments, workflows and reports (HubSpot docs, October 2026). For what happens after a lead scores A, see lead routing in your CRM.
What you get
The size check from Step 1, run through the free leadocean_count_leads tool on 2026-10-21. It counts mailable people (verified, catch_all_valid or catch_all addresses), not every match.
| Filters | Mailable people | Distinct companies |
|---|---|---|
| jobLevel C-Team, VP or Director; jobFunction Sales & Business Development; country US | 601,282 | 180,014 (approximate) |
| Same, plus industry Software Development | 39,679 | 9,557 |
The facets of the second count, by seniority band: 26,488 director, 10,144 vp and 2,246 c_suite. By company size, 8,642 sit at companies of 1001-5000 people, the largest band.
The enrich response shape from the API reference, with placeholder values, shows where the three fields live:
{
"success": true,
"data": {
"contact_data": {
"has_email": true,
"email_status": "verified",
"contact_current_experiences": [{
"company_name": "Acme",
"company_domain": "acme.com",
"company_industry": "Software Development",
"company_employees": { "number_of_employees": 120 },
"job_title": "VP Sales",
"job_seniority": { "job_level": "VP" }
}]
}
},
"meta": { "credits": 1 }
}This is the response shape from the API reference. The values are placeholders.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 from LeadOcean | Missing or wrong x-api-key header | Check $LEADOCEAN_API_KEY is exported and sent as x-api-key. |
| 429 from LeadOcean | Above the plan's rate limit: 5 requests a second on Free, 200 on Agency | Keep the sleep in the loop, or move to Agency for bulk runs. Honour Retry-After. |
| 402 from LeadOcean | The 1,000 free records are spent | Upgrade at /pricing. Solo is $97 a month. |
| Count is 0 or the data is empty | A filter value is not an exact string, such as Director or vp for jobLevel | Run leadocean_list_enum_values or GET /v1/enums/job_level. jobLevel takes C-Team, VP, Director, Manager, Staff, Other. |
| Export stops short | Over 50,000 rows a request or 500,000 rows a day | Split the export by country or industry and run it over several days. |
FAQ
Do I need Marketing Hub Professional to score contacts?
Yes. Contact scores are Marketing Hub only, on the Professional or Enterprise tier. Company scores work with Marketing Hub or Sales Hub (HubSpot docs, October 2026). Check your plan before you build.
Should I start with fit, engagement or a combined score?
Start with fit. It works the day you turn it on, because it reads properties you already hold. Engagement scores need weeks of event history before the numbers mean anything. Add engagement later and switch to a combined score.
How many records does this cost?
One per contact LeadOcean finds. Misses are 404s and cost nothing. A counts call is free. The Free plan covers 1,000 contacts once. Paid plans run on a fair-use pace, not credits (LeadOcean pricing).
Can I run this without code?
Yes. The LeadOcean app at app.leadocean.io has an Exports page with a filter builder, a column picker and a price preview. Export a CSV, then import it into HubSpot and map the columns to the three properties. Bulk CSV export is not on Free.
How often should I refresh the data?
The dataset is refreshed monthly, and each record carries its own fetched_at date. Re-enrich your A and B contacts once a quarter. Job changes are the main reason a fit score goes stale. For model design, see how to build a lead scoring model and what lead scoring is.
Fill your HubSpot fit score with real job and company data
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →