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

How-to guide

How to Enrich Zoho CRM Contacts (COQL, Python and the LeadOcean API)

Most lists of Zoho CRM enrichment tools are five logos. This is one working loop: query Zoho, enrich by email, write back 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

  • Query Zoho for contacts with no title, enrich each work email with one LeadOcean call, and write the result back with one Zoho PUT per 100 contacts. No middleware.
  • A first run over 100 contacts takes about 40 minutes and spends at most 100 of your 1,000 free records. A miss is a 404 and costs nothing.
  • You do not need five tools. You need one data source behind Zoho's API and a script you can read.

Search results for "Zoho CRM enrichment tools" are five-logo roundups. A technical buyer needs less: which call finds the empty fields, and where do the answers land? Zoho's Records API takes up to 100 records per update call, so a clean loop is small (September 2026).

LeadOcean has no Zoho app and no Chrome extension. This is a script you run against both APIs. For a ranked list of tools, read enrichment tools for 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 Zoho CRM OAuth access token with the COQL read scope and write access to Contacts. Zoho's docs list ZohoCRM.coql.READ and ZohoCRM.modules.contacts.ALL (September 2026).
  • Your Zoho API domain. Zoho's examples use https://www.zohoapis.com. Your data centre may differ.
  • Python 3 with requests.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"
export ZOHO_TOKEN="paste-your-zoho-access-token-here"
export ZOHO_API="https://www.zohoapis.com"
pip install requests

Step 1: Find contacts with a missing title

Zoho's COQL endpoint takes a SQL-like query by POST. It supports is null on text fields, and a limit of 1 to 200 costs 1 API credit (COQL docs, September 2026).

bash
curl -s -X POST "$ZOHO_API/crm/v8/coql" \
  -H "Authorization: Zoho-oauthtoken $ZOHO_TOKEN" \
  -d '{"select_query": "select id, Last_Name, Email, Title, Phone from Contacts where (Email is not null and Title is null) limit 100"}'

The response is a data array plus info.more_records. Field API names come from Zoho's fields metadata. Confirm Title and Phone exist in your layout before a live run.

python
import os, requests

ZOHO = os.environ["ZOHO_API"]
ZH = {"Authorization": f"Zoho-oauthtoken {os.environ['ZOHO_TOKEN']}"}

def missing_titles(limit=100):
    q = ("select id, Last_Name, Email, Title, Phone from Contacts "
         f"where (Email is not null and Title is null) limit {limit}")
    r = requests.post(f"{ZOHO}/crm/v8/coql", headers=ZH,
                      json={"select_query": q}, timeout=30)
    if r.status_code == 204:
        return []
    r.raise_for_status()
    return r.json()["data"]

A 204 means no rows matched. Treat it as done.

Step 2: Enrich each email

Enrich takes exactly one of linkedin_url, email, phone or person_id. Send the Zoho email.

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"}'

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}, 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: Write back in one batch

Zoho's update call takes a data array. Each item needs the record id, and one call updates at most 100 records. Update, insert and upsert cost 1 credit per 10 records (update docs and limits, September 2026). Send only the fields you change.

python
def build_update(contact, cd):
    exp = (cd.get("contact_current_experiences") or [{}])[0]
    row = {"id": contact["id"]}
    if exp.get("job_title"):
        row["Title"] = exp["job_title"]
    phones = [p["phone"] for p in cd.get("contact_phones") or []]
    if phones and not contact.get("Phone"):
        row["Phone"] = phones[0]
    return row if len(row) > 1 else None

def push(rows):
    r = requests.put(f"{ZOHO}/crm/v8/Contacts", headers=ZH,
                     json={"data": rows}, timeout=60)
    r.raise_for_status()
    return r.json()

Step 4: Run it on 20 contacts first

Do a dry run. Print what would change and write nothing. Then switch writes on.

python
import time

DRY = True
rows, spent = [], 0
for c in missing_titles(20):
    cd = enrich(c["Email"])
    time.sleep(0.3)
    if cd is None:
        continue
    spent += 1
    row = build_update(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)}")

The loop makes one LeadOcean call per contact and a single Zoho write per batch. On Free the limit is 5 requests a second, so a sequential loop with a pause stays under it. Zoho's Free edition allows 5,000 API credits in a rolling 24 hours (September 2026). This run uses a fraction of that.

Step 5: Find contacts Zoho 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&limit=1" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{"jobLevel": ["Director"], "jobFunction": ["Sales & Business Development"], "country": ["GB"]}'

Search the same body without count, with limit up to 100 and the cursor from meta for the next page. Create the results in Zoho with the Records 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 into Zoho.

What you get

The dry run prints one line per contact, and the last line is your audit trail. This is the format with placeholder values.

text
554023000000310003 -> {'id': '554023000000310003', 'Title': 'Head of Sales', 'Phone': '+1 415 555 0133'}
554023000000310012 -> {'id': '554023000000310012', '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": "+1 415 555 0133" }],
      "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 Director, jobFunction Sales & Business Development, country GB. It returned 53,747 people, spanning 24,779 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 203,467.

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.
Empty search resultsA bad enum value, such as director for jobLevel.Use exact strings: Director, VP, C-Team. Check leadocean_list_enum_values.
Search stops at 10,000 rowsOne search pages to 10,000 rows (meta.depthCapped).Narrow the filters, or use an export on a paid plan.

FAQ

Does LeadOcean have a native Zoho CRM integration?

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

Will this overwrite titles my team typed in?

Not as written. Step 1 selects only contacts with no Title, and Step 3 sends a phone only when Zoho holds none. Widen the query on purpose, never by accident.

How many records does a run spend?

One per contact we hold. A 100-contact run spends at most 100 records. The dataset refreshes monthly, so re-running a contact inside 30 days returns the same record and spends another.

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.

For other CRMs, see the best CRM enrichment tools and the CRM enrichment use case.

Fill your Zoho CRM contacts with titles and phones

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

Get your free API key →