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

How-to guide

How to Enrich Insightly Contacts (API and Python)

Skip the shortlist of nine tools. Here is the one recipe that fills titles and work emails in Insightly from a LinkedIn URL, with the queries and output.

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

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 requests installed.
  • 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 of linkedin_url, email, phone or person_id.
bash
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).

python
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.

bash
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.

python
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).

python
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.

python
SAFE = {"verified", "catch_all_valid"}
title, email, status = pick(p)
email = email if status in SAFE else None

Pass 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.

python
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.

json
{
  "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.

json
{ "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

ErrorCauseFix
401 from InsightlyAPI key not sent as Basic auth, or wrong pod hostUse the key as username with a blank password. Copy the API URL from User Settings
401 from LeadOceanMissing or wrong x-api-key headerCheck $LEADOCEAN_API_KEY is exported in this shell
429 from LeadOceanAbove the plan rate limit (5 a second on Free, 200 on Agency)Honor Retry-After, or add a short sleep between contacts
402 from LeadOceanRecords spent. Free is 1,000 for lifeMove to Solo ($97 a month) or Agency ($497 a month)
404 on enrichNobody matched that LinkedIn URLSkip the contact. It costs nothing
Empty search, no rowsA bad enum value such as vp for jobLevelUse 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.

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 →