You get a HubSpot workflow that fires on a new contact and sends the email to LeadOcean. It writes job title, company domain, phone and email status back. It uses one custom code action, not a marketplace app. LeadOcean has no HubSpot listing, so the call is a plain HTTP request to https://api.leadocean.io.
How it works
The workflow has four parts: an enrollment trigger, a custom code action, property updates and a guard so a record is never enriched twice. HubSpot says custom code actions need a Data Hub Professional or Enterprise account (HubSpot docs, October 2026). No Data Hub? Use the script route at the end of this section.
- Create a LeadOcean key. Sign up at app.leadocean.io/register. The free plan needs no card and gives 1,000 records.
- Store the key as a secret. In the custom code action, click Add secret and paste the value of
$LEADOCEAN_API_KEY. HubSpot exposes it to the code asprocess.env.<secret name>(HubSpot docs, October 2026). - Create a contact-based workflow. Enroll contacts when Email is known and a custom property
leadocean_enriched_atis unknown. This is the dedupe guard. - Add a Custom code action. Language Node.js. Add
emailas an input property. Paste the code from the next section. - Add Edit records actions. Map each code output to a contact property (see the table below). HubSpot documents code outputs as inputs to Edit records. Set
leadocean_enriched_atlast, to today's date. - Test on a dedicated record, then turn the workflow on. HubSpot applies a test to the selected record, so make a throwaway contact first (HubSpot docs, October 2026). The test spends one LeadOcean record.
The script route skips workflows. Search contacts with no job title, enrich each email, then call POST /crm/v3/objects/contacts/batch/update with up to 100 contacts per request (HubSpot docs, October 2026). It costs no Data Hub seat. You run it yourself from cron or a CI job.
For a one-off list, use the Exports page in the LeadOcean app. Build filters, pick columns, see the record price, download a CSV and import it into HubSpot.
Configuration
The code below runs inside the custom code action. HubSpot lists axios as a supported Node.js library (HubSpot docs, October 2026). Replace LEADOCEAN_API_KEY with your secret name.
const axios = require('axios');
exports.main = async (event, callback) => {
const email = event.inputFields['email'];
let out = { leadocean_status: 'not_found' };
try {
const res = await axios.post(
'https://api.leadocean.io/v1/people/enrich',
{ email: email, reveal_email: true },
{ headers: { 'x-api-key': process.env.LEADOCEAN_API_KEY } }
);
const c = res.data.data.contact_data || {};
const job = (c.contact_current_experiences || [])[0] || {};
const phone = (c.contact_phones || [])[0] || {};
const mail = (c.contact_emails || [])[0] || {};
out = {
leadocean_status: 'found',
job_title: job.job_title || '',
company_domain: job.company_domain || '',
phone: phone.phone || '',
email_status: mail.status || ''
};
} catch (e) {
if (!e.response || e.response.status !== 404) throw e;
}
callback({ outputFields: out });
};Test the same call outside HubSpot first:
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@example.com","reveal_email":true}'Send exactly one identifier per call: email, linkedin_url, phone or person_id (LeadOcean docs, September 2026). A person LeadOcean holds nothing for is a 404 and costs nothing, which is why the code catches it and moves on. reveal_email costs no extra record.
Field mapping
The response is one leadocean.person.v1 object under a data key. The fields below sit under data.contact_data (LeadOcean docs, October 2026).
| LeadOcean field | HubSpot property | Note |
|---|---|---|
contact_data.contact_current_experiences[].job_title | jobtitle | Current role. Overwrite only when empty. |
contact_data.contact_current_experiences[].company_domain | website | Overwrite only when empty. |
contact_data.contact_phones[].phone | phone | Only returned when reveal_email is true. |
contact_data.contact_emails[].status | custom leadocean_email_status | Values include verified, catch_all_valid and risky. Branch on it before any send. |
| run date | custom leadocean_enriched_at | Dedupe guard. Create it as a date property first. |
HubSpot treats email as the primary unique identifier for contacts, so match on email and never create contacts from this workflow (HubSpot docs, October 2026). Use Update, not create. Run the enrich step once per record and let the date property block repeats, so a re-saved contact never spends a second LeadOcean record. LeadOcean does not refund bounced emails, so filter on leadocean_email_status first.
Cost at 10,000 rows
10,000 rows cost $97 on LeadOcean Solo, plus HubSpot Data Hub Professional if you use the workflow route. Solo allows 10k enrichments every 5 hours and 50k a week, so 10,000 fits one window. The first 1,000 run free on the Free plan.
| LeadOcean | HubSpot | |
|---|---|---|
| Plan | Solo, $97 a month, flat | Data Hub Professional, starts at $800 a month, 1 seat included |
| Per-record price | None. A fair-use pace, no overage invoice | Not applicable |
| Workflow route total | $97 | $800 |
| Script route total | $97 | $0 extra, but API limits apply |
HubSpot's list price comes from its Data Hub page, which shows one "starts at" figure and no separate yearly rate (HubSpot, October 2026). The workflow route totals $897 a month. The script route drops the $800. HubSpot API limits are 100 requests per 10 seconds per app on Free and Starter, and 190 on Professional. A daily cap applies per account (HubSpot docs, October 2026). Batching 100 contacts per update keeps 10,000 rows near 100 write calls.
Teams enriching more than 50k rows a week, Solo's weekly pace, should look at Agency at $497 a month. See pricing.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 401 in the action log | Key missing or wrong secret name | Check the secret is added to this action and the name matches process.env. |
| 400 from LeadOcean | Two identifiers sent, or empty email | Send only email. Add a trigger filter that requires Email is known. |
| 402 from LeadOcean | The free 1,000 records are spent | Move to Solo. Free records do not renew. |
| 429 from LeadOcean | Over the rate limit | 5 requests a second on Free, 50 on Solo (LeadOcean docs, October 2026). Turn on the action's rate limit in the workflow. |
| Workflow pauses mid-run | Action rate limit reached | HubSpot logs that the action resumes at a set time. Raise the limit or wait (HubSpot docs, October 2026). |
| HubSpot 429 on the script route | Hit the 10-second or daily cap | Throttle requests and batch 100 contacts per update (HubSpot docs, October 2026). |
| Output not available in later actions | Output field not declared | Add each output in the Data outputs section of the action (HubSpot docs, October 2026). |
FAQ
Does LeadOcean have a native HubSpot app?
No. Use a custom code action or a script that calls the REST API. You get every endpoint, including GET /v2/people/phone for a direct phone lookup.
Do I need a paid HubSpot plan?
For the workflow route, yes: custom code actions need Data Hub Professional or Enterprise. The script route works on any plan with a private app token and API access.
Will it overwrite data my reps typed in?
Not if you map with care. Use a branch that checks the property is empty before each Edit records action, and keep leadocean_enriched_at so a record is enriched once.
How do I get phone numbers?
Set reveal_email to true and read contact_data.contact_phones[].phone. A second call to GET /v2/people/phone costs one record per call, including a call that finds nothing.
See also Retool, Notion and Folk, or go back to the integrations hub.
Enrich your HubSpot contacts with a free LeadOcean key.
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →