You can fill social and company data on Nimble contacts with one Node script. It reads people from Nimble's REST API, calls LeadOcean once per person and once per company, and writes the result back. No automation vendor sits in the middle.
Searches for Nimble enrichment tools return roundups of nine products. This is the version a technical buyer can bookmark: one recipe, real requests, real output. For the tool comparison, read best CRM data enrichment tools.
TL;DR
- Time: about 40 minutes to build and test. A 50-contact run takes under a minute.
- Cost: 1 LeadOcean record per person found and 1 per company looked up. A person or company we hold nothing for is a 404 and costs nothing. Free gives 1,000 records for life, no card.
- Limit to watch: LeadOcean allows 5 requests a second on Free and 200 on Agency. The script calls one person at a time, so it stays under both.
Prerequisites
- A Nimble account and an API access token. Nimble uses OAuth 2.0 bearer tokens, sent as
Authorization: Bearer <token>(Nimble auth docs, October 2026). Export it as$NIMBLE_TOKEN. - A LeadOcean key from app.leadocean.io/register. Export it as
$LEADOCEAN_API_KEY. - Nimble person contacts that carry a LinkedIn URL.
- Node 18 or later, for the built-in
fetch.
Step 1: List the Nimble people you want to fix
Nimble lists contacts at GET https://api.nimble.com/api/v1/contacts/. The list takes record_type (person, company or all), per_page (default 30), page and a fields list (Nimble contacts list docs, September 2026). Sorting only works with per_page at 100 or less.
curl -G "https://api.nimble.com/api/v1/contacts/" \
-H "Authorization: Bearer $NIMBLE_TOKEN" \
--data-urlencode "record_type=person" \
--data-urlencode "per_page=50" \
--data-urlencode "fields=first name,last name,email,phone"Each contact comes back with a fields object. Every field is a list of {value, modifier} pairs (Nimble update docs, September 2026).
Step 2: Test the LeadOcean key
Enrich one person by LinkedIn URL. Send exactly one identifier per call. Add reveal_email to get the addresses and numbers.
curl -X POST https://api.leadocean.io/v1/people/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{"linkedin_url": "https://www.linkedin.com/in/jane-doe-example", "reveal_email": true}'A 200 returns the person and spends one record. A 404 means we hold nothing for that person and costs nothing. A 401 means the key is wrong.
Step 3: Add the company data
The person response carries the current employer in contact_current_experiences. Take its company_domain and call company enrich for the firmographics.
curl -X POST https://api.leadocean.io/v1/companies/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{"domain": "acme.com"}'The response holds company_data, company_metrics and company_detected_technologies. Look up each domain once and cache it, because ten Nimble contacts at one company should cost one company record, not ten.
Step 4: Write it back to Nimble
Nimble updates a contact with PUT /api/v1/contact/<id>. The body is {"fields": {...}}. By default a value only replaces the field entry with the same modifier. Add ?replace=1 to replace every entry of that field. Passing null as a value removes it (Nimble update docs, September 2026).
Leave replace off. You then add a work email next to the personal one instead of deleting it.
Here is what each LeadOcean field maps to in Nimble. Pick the Nimble fields you want filled before you run it on the whole account.
| LeadOcean field | Kind | Nimble target |
|---|---|---|
contact_data.contact_emails[0].email | Contact | email with modifier work |
contact_data.contact_phones[0].phone | Contact | phone |
profile_data.profile_headline | Social | A custom field such as "headline" |
contact_data.contact_current_experiences[0] | Firmographic | Job title and company name |
company_data | Firmographic | Custom fields for size, industry and location |
company_detected_technologies | Firmographic | A custom tag or note |
Social data here means the LinkedIn headline and profile you already match on, plus current role. Firmographic data means the company facts: size, industry, location and detected technologies. Both come from the same two calls.
Step 5: The full script
Nimble's default linkedin field is a social field that holds the profile URL (Nimble default fields, October 2026). The script reads it.
This script enriches up to LIMIT people, caches companies by domain, and writes the work email, phone and a company line into Nimble. Save it as enrich-nimble.mjs.
const NIMBLE = "https://api.nimble.com/api/v1";
const LO = "https://api.leadocean.io";
const LIMIT = 50;
// Name of the Nimble field that holds the LinkedIn URL.
const LINKEDIN_FIELD = "linkedin"; // Nimble default social field, value is the profile URL
const nimble = (path, init = {}) =>
fetch(NIMBLE + path, {
...init,
headers: {
Authorization: `Bearer ${process.env.NIMBLE_TOKEN}`,
"content-type": "application/json",
},
});
async function lo(path, body) {
for (;;) {
const r = await fetch(LO + path, {
method: "POST",
headers: { "x-api-key": process.env.LEADOCEAN_API_KEY, "content-type": "application/json" },
body: JSON.stringify(body),
});
if (r.status === 429) {
await new Promise((s) => setTimeout(s, Number(r.headers.get("retry-after") ?? 1) * 1000));
continue;
}
if (r.status === 404) return null; // nobody matched, no charge
if (!r.ok) throw new Error(`LeadOcean ${r.status}`);
return (await r.json()).data;
}
}
const list = await (await nimble(`/contacts/?record_type=person&per_page=${LIMIT}`)).json();
const companies = new Map();
for (const c of list.resources) {
const url = c.fields?.[LINKEDIN_FIELD]?.[0]?.value;
if (!url) continue;
const p = await lo("/v1/people/enrich", { linkedin_url: url, reveal_email: true });
if (!p) { console.log(c.id, "skipped"); continue; }
const email = p.contact_data?.contact_emails?.[0];
const phone = p.contact_data?.contact_phones?.[0];
const domain = p.contact_data?.contact_current_experiences?.[0]?.company_domain;
if (domain && !companies.has(domain)) {
companies.set(domain, await lo("/v1/companies/enrich", { domain }));
}
const co = companies.get(domain)?.company_data;
const fields = {};
if (email?.email) fields.email = [{ value: email.email, modifier: "work" }];
if (phone?.phone) fields.phone = [{ value: phone.phone, modifier: "work" }];
if (co) fields["description"] = [{ value: JSON.stringify(co).slice(0, 500), modifier: "" }];
const r = await nimble(`/contact/${c.id}`, { method: "PUT", body: JSON.stringify({ fields }) });
console.log(c.id, r.status, email?.status ?? "no email");
}Two cautions. The company line is a raw JSON slice so you can see which keys exist before you map them to custom fields. Nimble lets you create custom fields through its metadata API (Nimble contacts docs, September 2026), so map firmographic keys to real fields once you have chosen them. And contact_emails[0] is the best address, so read its status before anyone sends to it.
Step 6: Size a list of new contacts
Enrichment fixes contacts you already hold. To add new companies, size them first. GET /v1/companies/count and the MCP tool leadocean_count_companies are free and return an exact total with breakdowns. On 21 October 2026 this count ran against the live index:
industry: Software Development
hqCountry: US
total: 201,057 companies (exact)
employee_range: 1-10: 148,099 | 11-50: 38,048 | 51-200: 9,261 | 201-500: 2,316
hq_state: California 44,279 | Texas 15,040 | New York 14,458
credits: 0This counts companies, not mailable people. To load people into Nimble, export a CSV and use Nimble's own import. Use the Exports page in the LeadOcean app: a filter builder, a column picker, a price preview and a download button. Or call POST /v1/exports, which takes up to 50,000 rows a request and 500,000 a day. Bulk CSV export is not on Free.
Step 7: Check before you widen the run
Run 50 contacts, then open them in Nimble. Three checks catch most problems.
First, confirm the email landed on the right contact. A stale LinkedIn URL can point at someone who left. Second, count the skipped rows. A high rate means the URLs in Nimble are old. Third, read email_status on the sample. Only verified and catch_all_valid are safe to send to without a second check.
Then raise LIMIT. Each run is repeatable, but it re-spends a record on people you already filled, so add a custom Nimble field such as "enriched" and skip contacts that have it.
What you get
A run on three placeholder contacts prints one line each. Your IDs differ:
4f69fb852ab3740c5e000004 200 verified
5e69fb852ab3f40d5e050017 200 catch_all_valid
6a69fb852ab3f40d5e050099 skippedThe script reads this LeadOcean person response. It is the response shape from the API reference, with placeholder values:
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
"contact_data": {
"has_email": true,
"has_phone": true,
"email_status": "verified",
"contact_emails": [{ "email": "jane@acme.com", "type": "work", "status": "verified" }],
"contact_phones": [{ "phone": "+14155550133", "type": "mobile" }],
"contact_current_experiences": [{ "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com" }]
}
}
}Since 17 October 2026 the same call also returns career history in resume_data: past roles, education and certifications. Past employers carry no company_id yet.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 from LeadOcean | Key missing or wrong | Check $LEADOCEAN_API_KEY and the x-api-key header name. |
| 429 from LeadOcean | Over the plan's rate limit, 5 requests a second on Free, 200 on Agency | Process contacts one at a time. Honour Retry-After. |
| 402 from LeadOcean | The 1,000 free records are spent | Move to Solo at $97 a month. See pricing. |
| 404 from LeadOcean | No record for that person or company | Expected. No charge. The script skips it. |
| Empty results from a LeadOcean search | A filter value is not an exact enum string, such as vp for jobLevel | Resolve values with the free leadocean_list_enum_values tool or GET /v1/enums. |
| Validation error from Nimble | A parameter value Nimble does not recognise (Nimble list docs, September 2026) | Unknown parameters are ignored but unknown values return an error. Check record_type and fields. |
Run it on a schedule
A cron line on any machine with Node is enough. Hourly suits contacts that arrive through forms or imports.
0 * * * * NIMBLE_TOKEN=... LEADOCEAN_API_KEY=... node /opt/enrich/enrich-nimble.mjs >> /var/log/enrich.log 2>&1Keep the keys in a secrets store, not in the crontab. A contact LeadOcean cannot match is retried each run as a free 404, so the cost of a miss is a few milliseconds.
FAQ
Does LeadOcean have a native Nimble integration?
Does this replace Nimble's own social features?
No. Nimble keeps the contact record and its CRM workflow. LeadOcean only supplies data. LeadOcean has no rep workspace, sequencer, dialer, CRM or Chrome extension.
Can I enrich by email instead of LinkedIn URL?
Yes. Send email in place of linkedin_url. Send exactly one identifier per call. There is no name plus domain lookup: search by domain first, then enrich by person_id.
How many records does a 1,000-contact account cost?
At most 2,000 in the worst case, one per person and one per distinct company. Misses are free, and cached companies are looked up once. The Free plan covers 1,000 records for life, then you need a paid plan from $97 a month. See what CRM enrichment adds to your sales data and fixing a messy CRM.
Fill the social and company fields in your Nimble 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 →