You do not need a review of nine enrichment tools to fill the gaps in Insightly. You need one script that reads a contact, calls an enrichment API and writes the result back with a PUT. This guide builds that script. We did not run a nine-tool bake-off for this page, so there is no ranking here, only a recipe you can test in 30 minutes.
TL;DR
- Read contacts from the Insightly API v3.1, enrich each one by LinkedIn URL, and PUT the title and work email back.
- It takes about 30 minutes. A 25-contact test spends at most 25 of the 1,000 free LeadOcean records, and a contact with no match costs nothing.
- Insightly does the storing. LeadOcean does the lookup. Nothing else is needed.
Prerequisites
- An Insightly API key and your pod URL. Both sit in User Settings, under the API section.
- A LeadOcean key from app.leadocean.io, exported as
$LEADOCEAN_API_KEY. Free needs no card. - Python 3.9 or later with
requestsinstalled. - Contacts in Insightly that already have a LinkedIn URL in
SOCIAL_LINKEDIN, a standard Contact field in the v3.1 schema (Insightly API spec, October 2026). Enrich takes exactly one oflinkedin_url,email,phoneorperson_id.
export LEADOCEAN_API_KEY="your-leadocean-key"
export INSIGHTLY_API_KEY="your-insightly-key"
export INSIGHTLY_BASE="https://api.na1.insightly.com/v3.1"The Insightly docs say your pod decides the host, so copy the API URL from User Settings rather than trusting na1 (September 2026, source).
Step 1: Read contacts from Insightly
Insightly uses HTTP Basic authentication. The API key is the username and the password stays blank. The docs say to Base64-encode it for the Authorization header (September 2026, source).
import os, requests
BASE = os.environ["INSIGHTLY_BASE"]
AUTH = (os.environ["INSIGHTLY_API_KEY"], "")
def get_contacts(top=25):
r = requests.get(f"{BASE}/Contacts", auth=AUTH, params={"top": top})
r.raise_for_status()
return r.json()
contacts = get_contacts()
print(len(contacts), "contacts")requests builds the Basic header from the tuple, so you do not encode anything by hand. Keep top small on the first run.
Step 2: Size the match for free
Before you spend records, check how big the target set is. leadocean_count_leads and the REST search count are free. Send count=true as a query parameter with limit=1.
curl -s "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["US"]}'The response carries meta.total (capped at 100,000) and meta.countOnly: true. Use it to decide whether a net-new list is worth pulling after the enrichment run.
Step 3: Enrich one contact by LinkedIn URL
POST /v1/people/enrich takes one identifier. Set reveal_email to true to get the address back. A person we hold nothing for is a 404 and costs nothing.
LO = "https://api.leadocean.io"
LO_HEADERS = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
def enrich(linkedin_url):
r = requests.post(
f"{LO}/v1/people/enrich",
headers=LO_HEADERS,
json={"linkedin_url": linkedin_url, "reveal_email": True},
)
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()
print(enrich("https://www.linkedin.com/in/jane-doe-example"))Since October 2026 the response also includes resume_data with past roles, education and certifications. Past employers carry no company_id yet, so do not join on them.
Step 4: Write the result back with a PUT
Insightly PUT requests do not need every field. Send the CONTACT_ID and only the fields you change (September 2026, source).
def pick(p):
d = p["data"]["contact_data"]
role = (d.get("contact_current_experiences") or [{}])[0]
email = (d.get("contact_emails") or [{}])[0]
return role.get("job_title"), email.get("email"), email.get("status")
def update_contact(contact_id, title, email):
body = {"CONTACT_ID": contact_id}
if title:
body["TITLE"] = title
if email:
body["EMAIL_ADDRESS"] = email
r = requests.put(f"{BASE}/Contacts", auth=AUTH, json=body)
r.raise_for_status()
for c in contacts:
li = c.get("SOCIAL_LINKEDIN") or ""
if "linkedin.com/in/" not in li:
continue
p = enrich(li)
if p:
title, email, status = pick(p)
update_contact(c["CONTACT_ID"], title, email)The current role sits in contact_data.contact_current_experiences[0] and the best address in contact_data.contact_emails[0], each with its own status.
Step 5: Check email status before you send
Do not write an address into a field your team mails from without reading its status. LeadOcean returns email_status with 13 possible values, from verified to spam_trap. There is no refund for bounces, so filter on it.
SAFE = {"verified", "catch_all_valid"}
title, email, status = pick(p)
email = email if status in SAFE else NonePass this email into update_contact in place of the raw value.
Step 6: Run it in batches
Run the loop over 25 contacts, check five by hand, then widen. Keep one request in flight at a time on Free (5 requests a second). On Agency you can run up to 200 a second per key, but Insightly sets its own API limits, so check them in your plan before you parallelize.
import time
for c in contacts:
# ... enrich and update as in Step 4 ...
time.sleep(0.25)Log every CONTACT_ID you changed. If a title looks wrong, you can restore it from the log. Re-running the script on the same contacts spends records again, so skip any contact that already has a title.
What you get
This is the response shape from the API reference, with placeholder values. It is not a real record.
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe", "profile_url": "https://www.linkedin.com/in/jane-doe-example" },
"contact_data": {
"email_status": "verified",
"contact_emails": [{ "email": "jane.doe@acme.com", "type": "work", "status": "verified" }],
"contact_current_experiences": [{ "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com" }]
},
"resume_data": { "experiences": [], "educations": [] }
}
}Free sizing call from Step 2, run on 2026-10-21 with the leadocean_count_leads MCP tool. Filters: jobLevel VP, jobFunction Sales & Business Development, country US.
{ "total": 129494, "totalIsExact": true, "totalAll": 350517,
"distinctCompanies": 58121, "credits": 0 }That is 129,494 mailable VPs of sales in the US. Mailable here means verified, catch_all_valid or catch_all email status, the tool default.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 from Insightly | API key not sent as Basic auth, or wrong pod host | Use the key as username with a blank password. Copy the API URL from User Settings |
| 401 from LeadOcean | Missing or wrong x-api-key header | Check $LEADOCEAN_API_KEY is exported in this shell |
| 429 from LeadOcean | Above the plan rate limit (5 a second on Free, 200 on Agency) | Honor Retry-After, or add a short sleep between contacts |
| 402 from LeadOcean | Records spent. Free is 1,000 for life | Move to Solo ($97 a month) or Agency ($497 a month) |
| 404 on enrich | Nobody matched that LinkedIn URL | Skip the contact. It costs nothing |
| Empty search, no rows | A bad enum value such as vp for jobLevel | Use exact strings (VP) and check leadocean_list_enum_values |
FAQ
Do I need all nine tools I found on a listicle?
No. Pick one source for data and one place to store it. Insightly stores. An enrichment API fills. Adding a third tool only adds a second bill.
Can I match on name and company instead of LinkedIn URL?
Not on enrich. There is no name plus domain lookup. Search by domain first, then enrich by person_id.
How many records does a full Insightly backfill spend?
One per contact found. Contacts that return a 404 cost nothing. Count your contacts that hold a LinkedIn URL and that is your upper bound.
Does this work on the Free plan?
Yes, inside 1,000 records for life, with search and enrich both on. Bulk CSV export is not on Free.
Related reading
- Your CRM data is a mess: 8 enrichment tools that fix it
- The only 5 Zoho CRM enrichment tools worth your time
- No budget? 9 free enrichment tools that are surprisingly good.
- LeadOcean pricing.
Enrich your first 25 Insightly contacts free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →