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

How-to guide

How to Enrich Freshsales Contacts (Freshworks CRM API and LeadOcean)

One loop you can read: list contacts, enrich by email, bulk upsert in batches of 100.

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

TL;DR

  • List Freshsales contacts that have an email but no job title, enrich each email with one LeadOcean call, and write results back with bulk_upsert, 100 contacts a request.
  • A first run over 100 contacts takes about 45 minutes and spends at most 100 of your 1,000 free records. A miss is a 404 and costs nothing.
  • You do not need eight tools. You need one data source behind Freshsales' API and a script you can read.

Searches for "Freshsales enrichment tools" return roundups of eight or ten logos. We have not run a head-to-head test of eight tools, so this page does not rank them. It shows the one loop a technical buyer can verify.

Freshworks' CRM API takes Authorization: Token token=... and caps an account at 1000 API requests per hour (October 2026). Bulk upsert takes 100 contacts a call. That makes a small loop cheap.

LeadOcean has no Freshsales app and no Chrome extension. This is a script against two APIs. For the general idea, read what enrichment is. For the same recipe on another CRM, see Zoho CRM.

Prerequisites

  • A LeadOcean API key from app.leadocean.io. The free plan has no card and 1,000 records, one-off. Free keys can enrich.
  • A Freshworks CRM API key (in your profile settings) and your bundle alias, which gives the domain https://YOURALIAS.myfreshworks.com (October 2026).
  • Python 3 with requests.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"
export FRESH_KEY="paste-your-freshworks-api-key-here"
export FRESH_BASE="https://yourbundle.myfreshworks.com/crm/sales/api"
pip install requests

Step 1: List contacts that need a title

Freshsales lists contacts through a saved view. Fetch the filter ids first, then page through a view.

bash
curl -s -H "Authorization: Token token=$FRESH_KEY" \
  -H "Content-Type: application/json" \
  "$FRESH_BASE/contacts/filters"

Pick a view id (for example "All Contacts") from the response. Pages hold 25 contacts by default and you add page=2 and so on (October 2026).

python
import os, requests

BASE = os.environ["FRESH_BASE"]
FH = {"Authorization": f"Token token={os.environ['FRESH_KEY']}",
      "Content-Type": "application/json"}

def missing_titles(view_id, max_pages=4):
    out = []
    for page in range(1, max_pages + 1):
        r = requests.get(f"{BASE}/contacts/view/{view_id}",
                         headers=FH, params={"page": page}, timeout=30)
        r.raise_for_status()
        rows = r.json().get("contacts", [])
        if not rows:
            break
        out += [c for c in rows if c.get("email") and not c.get("job_title")]
    return out

Each page is one of your 1000 hourly requests. Four pages cover 100 contacts.

Step 2: Enrich each email

Enrich takes exactly one of linkedin_url, email, phone or person_id. Send the Freshsales email with reveal_email set to true, since phone numbers come back only with it. Revealing costs no extra record.

bash
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@acme.com", "reveal_email": true}'

A 404 means we hold nothing for that person. It is not billed. Skip the row.

python
LO = {"x-api-key": os.environ["LEADOCEAN_API_KEY"], "content-type": "application/json"}

def enrich(email):
    r = requests.post("https://api.leadocean.io/v1/people/enrich",
                      headers=LO, json={"email": email, "reveal_email": True}, timeout=30)
    if r.status_code == 404:
        return None
    if r.status_code in (401, 402):
        raise SystemExit(f"stop: {r.status_code} {r.text[:120]}")
    r.raise_for_status()
    return r.json()["data"]["contact_data"]

Step 3: Bulk upsert the results

Freshsales' bulk_upsert takes up to 100 contacts a request. Each item names one unique identifier and a data hash. Use the contact id, which the docs allow (October 2026). Up to 10 bulk calls can run at once per account.

python
def build_row(contact, cd):
    exp = (cd.get("contact_current_experiences") or [{}])[0]
    data = {}
    if exp.get("job_title"):
        data["job_title"] = exp["job_title"]
    phones = [p["phone"] for p in cd.get("contact_phones") or []]
    if phones and not contact.get("mobile_number"):
        data["mobile_number"] = phones[0]
    return {"id": str(contact["id"]), "data": data} if data else None

def push(rows):
    r = requests.post(f"{BASE}/contacts/bulk_upsert", headers=FH,
                      json={"contacts": rows}, timeout=60)
    r.raise_for_status()
    return r.json()

Freshsales tracks bulk calls through its Job Status API. Check the job before you trust the write.

Step 4: Dry run on 20 contacts

Print what would change and write nothing. Then switch writes on.

python
import time

DRY = True
rows, spent = [], 0
for c in missing_titles(VIEW_ID)[:20]:
    cd = enrich(c["email"])
    time.sleep(0.3)
    if cd is None:
        continue
    spent += 1
    row = build_row(c, cd)
    print(c["id"], "->", row)
    if row:
        rows.append(row)
if rows and not DRY:
    push(rows)
print(f"records spent: {spent}  rows to write: {len(rows)}")

Set VIEW_ID to the id from Step 1. On Free the LeadOcean limit is 5 requests a second, so a sequential loop with a pause stays under it.

Step 5: Find contacts Freshsales does not have yet

Enrichment fixes rows you hold. To add rows, size the list first. The count is free.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{"jobLevel": ["VP"], "jobFunction": ["Sales & Business Development"], "country": ["DE"], "limit": 1}'

Run the same body without count, with limit up to 100 and the cursor from meta for the next page. Create the results in Freshsales through its contacts API. Paid plans can also use POST /v1/exports for up to 50,000 rows a request and 500,000 a day, then import the CSV.

What you get

The dry run prints one line per contact. This is the format with placeholder values.

text
1001 -> {'id': '1001', 'data': {'job_title': 'Head of Sales', 'mobile_number': '+1 415 555 0133'}}
1002 -> {'id': '1002', 'data': {'job_title': 'VP Marketing'}}
records spent: 2  rows to write: 2

The enrich call returns this shape. It is the response shape from the API reference, with placeholder values.

json
{
  "success": true,
  "data": {
    "contact_data": {
      "email_status": "verified",
      "contact_phones": [{ "phone": "+14155550133", "type": "mobile" }],
      "contact_current_experiences": [
        { "company_name": "Acme", "company_domain": "acme.com", "job_title": "Head of Sales" }
      ]
    }
  }
}

Sizing run on 2026-10-21 with leadocean_count_leads: jobLevel VP, jobFunction Sales & Business Development, country DE. It returned 1,395 people across 905 employers. The tool's default counts only verified, catch_all_valid and catch_all addresses, so that is a mailable count. The same filters with no email condition match 3,893.

Troubleshooting

ErrorCauseFix
401 from LeadOceanMissing or wrong x-api-key.Re-export $LEADOCEAN_API_KEY from app.leadocean.io.
402Out of records. On Free that is after your 1,000.Read GET /v1/account, then move to Solo at $97 a month.
429 from LeadOceanAbove the plan's rate limit: 5 a second on Free, 200 on Agency.Sleep and retry. A sequential loop will not hit it.
404 on enrichWe hold nothing for that email.Not billed. Skip the row.
429 from FreshsalesOver 1000 API requests per hour per account.Batch writes at 100 and cap pages per run.
Empty search resultsA bad enum value, such as vp for jobLevel.Use exact strings: VP, Director, C-Team. Check leadocean_list_enum_values.

FAQ

Does LeadOcean have a native Freshsales integration?

No. There is no Freshsales app, and no Chrome extension. You call both APIs from a script, a scheduled job or a workflow tool.

Did you test eight Freshsales enrichment tools?

No. We have not run that test, so we do not rank eight tools. For how stacking sources works, read what is waterfall enrichment.

Will this overwrite titles my team typed in?

Not as written. Step 1 keeps only contacts with no job_title, and Step 3 sends a phone only when Freshsales holds none.

What does it cost after the free 1,000?

Solo is $97 a month for one person. Agency is $497 a month for teams, with 200 requests a second. Paid plans have a fair-use pace, not credits. See pricing.

Fill your Freshsales contacts with titles and phones

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →