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

How-to guide

How to Enrich a CSV of Contacts (in ChatGPT)

Add one MCP URL to ChatGPT, enrich a small batch of rows in chat, then run the full file with a short script.

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

TL;DR

  • Add the LeadOcean MCP server to ChatGPT as a custom MCP server, paste up to 25 rows into a prompt, and ask for an enriched CSV back. Then run the full file with a script against POST /v1/people/enrich.
  • It takes about 20 minutes. Each person we match spends one record. A person we hold nothing for is a 404 and costs nothing.
  • A 25-row batch spends at most 25 of your 1,000 free records. A 200-row file spends at most 200. Your real number depends on the match rate.

Prerequisites

  • A LeadOcean account from app.leadocean.io. The free plan has no card and 1,000 records, one-off. Its keys get both the search and enrich scopes.
  • A ChatGPT account that can add a custom MCP server. OpenAI's docs, October 2026, say account and workspace policies apply.
  • An API key in your shell as $LEADOCEAN_API_KEY, for the script step only. The chat steps use OAuth and need no key.
  • Python 3 and curl.
  • A CSV with one identifier per row: an email or a linkedin_url. Enrich takes exactly one of linkedin_url, email, phone or person_id. There is no name plus company lookup.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"

Step 1: Connect LeadOcean to ChatGPT

Add the server URL as a custom MCP server with OAuth. No key is pasted into the chat. These are the steps in OpenAI's docs, October 2026.

text
ChatGPT Plugins > plus button > Add custom MCP server
Name: LeadOcean
URL:  https://api.leadocean.io/mcp
Authentication: OAuth
Create as a plugin

Sign in to LeadOcean and approve. The server exposes 13 tools. The full walkthrough, with sign-in and troubleshooting, is on Connect LeadOcean to ChatGPT.

Step 2: Prepare a small CSV

Keep the file plain. Use a header row, one person per row and no commas inside fields. The identifier column is what we match on. Name and company are for your own checking.

csv
first_name,last_name,company,email,linkedin_url
Jane,Doe,Acme,jane.doe@acme.com,
John,Roe,Example Corp,,https://www.linkedin.com/in/john-roe-example

These rows are placeholders. Start with your first 25 real rows. A small batch shows your match rate before you commit the whole file.

Clean the identifier column first. Trim spaces, lowercase the emails and use full LinkedIn profile URLs. A malformed identifier comes back as not found, which costs nothing but wastes a model turn.

Step 3: Enrich a batch in chat

Start a new conversation, type @ and select the LeadOcean plugin. Paste the rows into the prompt. This avoids guessing how ChatGPT handles a file attachment.

text
Use the LeadOcean plugin only. Do not browse the web.
For each row below, call leadocean_get_lead with the email, or the linkedin_url
when there is no email. Skip rows with neither. Do not call any other tool that
spends records.
Return one CSV code block with the original columns plus job_title, company,
email_status and has_phone. Write not_found for rows that return nothing.
Finish with two numbers: rows matched and rows not found.

first_name,last_name,company,email,linkedin_url
Jane,Doe,Acme,jane.doe@acme.com,
John,Roe,Example Corp,,https://www.linkedin.com/in/john-roe-example

ChatGPT may ask you to confirm a tool call. Check the arguments, then approve.

A leadocean_get_lead call that finds nobody costs nothing, the same as the REST 404. Each match costs one record, so 25 rows spend at most 25. Copy the CSV block into pilot_enriched.csv.

Chat is the right tool up to a few dozen rows. Each row is a separate tool call and a model turn, so a long file gets slow and hard to check.

Step 4: Read the result before you scale

Look at three things in the batch. First, the match rate: if most rows came back empty, your identifiers are the problem, not the data. Second, the email_status column, which tells you how much of the file is safe to send. Third, whether the job titles are current enough for your use.

Fix the CSV, not the prompt, if rows failed. Statuses are exact strings, and the free leadocean_list_enum_values tool lists them. Ask ChatGPT to call it once if a value looks odd.

Step 5: Run the full file with a script

Past a few dozen rows, send one enrich request per row from a script. Pass reveal_email as true to get the addresses and phones we hold, at no extra record. The script stops on a 402, so it never loops against an empty balance.

python
import csv, json, os, time, urllib.request, urllib.error

KEY = os.environ["LEADOCEAN_API_KEY"]
URL = "https://api.leadocean.io/v1/people/enrich"

def enrich(body):
    req = urllib.request.Request(
        URL, data=json.dumps(body).encode(), method="POST",
        headers={"x-api-key": KEY, "content-type": "application/json"})
    try:
        with urllib.request.urlopen(req) as r:
            return r.status, json.load(r)
    except urllib.error.HTTPError as e:
        if e.code == 429:
            time.sleep(int(e.headers.get("Retry-After", "1")))
            return enrich(body)
        return e.code, None

with open("contacts.csv", newline="") as f, open("enriched.csv", "w", newline="") as g:
    rows = csv.DictReader(f)
    out = csv.DictWriter(g, fieldnames=rows.fieldnames + [
        "result", "email_status", "best_email", "best_phone"])
    out.writeheader()
    matched = 0
    for row in rows:
        body = {"reveal_email": True}
        if row.get("email"):
            body["email"] = row["email"]
        elif row.get("linkedin_url"):
            body["linkedin_url"] = row["linkedin_url"]
        else:
            out.writerow({**row, "result": "no_identifier"})
            continue
        code, data = enrich(body)
        if code == 402:
            print("Out of records. Stopping.")
            break
        if code != 200:
            out.writerow({**row, "result": "not_found" if code == 404 else f"http_{code}"})
            continue
        matched += 1
        c = data["data"].get("contact_data", {})
        emails, phones = c.get("contact_emails") or [], c.get("contact_phones") or []
        out.writerow({**row, "result": "matched",
                      "email_status": c.get("email_status") or "",
                      "best_email": emails[0]["email"] if emails else "",
                      "best_phone": phones[0]["phone"] if phones else ""})
    print(f"Matched {matched} rows. Records spent: {matched}.")

Run it with python3 enrich.py. The printed match count is your record cost. The script sends one request at a time, below even the Free rate of 5 requests a second, and writes a row for every input.

Step 6: Check the cost

GET /v1/account is free. It returns records used and remaining, so you can compare it with the script's count.

bash
curl -s https://api.leadocean.io/v1/account \
  -H "x-api-key: $LEADOCEAN_API_KEY" | jq '.data.records | {used, remaining}'

Filter on email_status before you send anything. Do not send to invalid, spam_trap, abuse or disposable. Bounces are not refunded.

What you get

The output is a CSV: your original columns plus result, email_status, best_email and best_phone. Rows with no match say not_found and spent nothing. This is the shape of an enriched file, with placeholder values.

csv
first_name,last_name,company,email,linkedin_url,result,email_status,best_email,best_phone
Jane,Doe,Acme,jane.doe@acme.com,,matched,verified,jane.doe@acme.com,+14155550133
John,Roe,Example Corp,,https://www.linkedin.com/in/john-roe-example,not_found,,,

Each enrich call returns this response shape from the API reference, with placeholder values. The contact_emails and contact_phones arrays appear only when reveal_email is true.

json
{
  "success": true,
  "data": {
    "profile_data": { "...": "name, headline, current role, company" },
    "contact_data": {
      "has_email": true,
      "has_phone": true,
      "email_status": "verified",
      "contact_emails": [
        { "email": "jane.doe@acme.com", "type": "work", "status": "verified" }
      ],
      "contact_phones": [
        { "phone": "+14155550133", "type": "mobile", "priority": 1 }
      ]
    }
  },
  "meta": { "source": "leadocean", "key_hash": "<cache key>" }
}

We read the status list from the free leadocean_list_enum_values tool on 2026-10-21 (description and labels trimmed).

json
{
  "name": "email_status",
  "total": 13,
  "values": ["verified","catch_all_valid","catch_all","risky","unknown","untested",
             "invalid","role","disposable","spam_trap","abuse","derived","none"]
}

On a work address, verified means our own verifier sent to the address and it accepted. A search row can show verified more loosely, so trust the enriched status.

Troubleshooting

ErrorCauseFix
401Missing, wrong or revoked key.Copy the key again from app.leadocean.io and re-export $LEADOCEAN_API_KEY.
402The free plan's 1,000 records are spent. They do not reset.Check GET /v1/account. Move to Solo ($97 a month) or Agency ($497 a month). See pricing.
429Above your plan's rate per account (5 a second on Free, 200 on Agency).Wait the Retry-After seconds. The script above already does.
404 on a rowWe hold nothing for that identifier.Nothing to fix and no record spent. Try the other identifier.
ChatGPT browses the web insteadIt chose a built-in tool over the plugin.Start the prompt with "Use the LeadOcean plugin only" and name leadocean_get_lead.
Rejected filter in chatA value is not in the catalogue, such as a guessed status.Have ChatGPT call leadocean_list_enum_values and use the exact string.

FAQ

How many records does a CSV enrichment cost?

One per row we match. A row we hold nothing for is a 404 and costs nothing. A 200-row file spends at most 200 records. The script's final count is the real number.

Can I enrich with only a name and a company?

No. Enrich needs one of linkedin_url, email, phone or person_id. With a name and a domain, search by domain first with /v1/people/search, then enrich the person by person_id. Search spends one record per person returned.

Should I use chat or the script?

Use chat for a batch of a few dozen rows, where you want to read the result. Use the script for anything larger. The script runs at API speed, and it does not need ChatGPT.

Does the free plan work for this?

Yes. Free is 1,000 records, one-off, with the same API and MCP server as the paid plans. Solo is $97 a month and Agency is $497 a month, with a fair-use pace instead of credits. See pricing.

Other editors have the same recipe: Windsurf, Cursor and VS Code. The MCP hub lists every client.

Enrich your first 25 contacts in ChatGPT, free

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

Get your free API key →