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

How-to guide

How to automate prospecting with a real-time enrichment REST API (curl and Python)

Size the list for free, page through search, enrich only the people you keep. Four requests, one script.

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

TL;DR

  • Automated prospecting on a REST API is a loop: size the list (free), search and page (spends records), enrich only the rows you keep.
  • It takes about 20 minutes and about 120 LeadOcean records, so it fits inside the Free plan's 1,000 records.
  • Every call goes to https://api.leadocean.io with an x-api-key header. Search bodies are flat JSON and filters are arrays.

Prerequisites

  • A LeadOcean account. The Free plan is $0, needs no card and gives you 1,000 records for life.
  • An API key from app.leadocean.io, exported as $LEADOCEAN_API_KEY.
  • curl and Python 3 with requests.
  • A target in mind. This recipe uses VPs of Sales in Canada.

Step 1: Size the list before you spend anything

Send count=true as a query parameter, not in the body, and limit 1 in the body. The body rejects unknown fields. A sizing call returns no rows and costs no records.

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": ["CA"],
    "limit": 1
  }'

meta.total is capped at 100,000 and meta.countOnly is true. If the number is too big or too small, change the filters and call again. Sizing is free as often as you like.

Enum values are exact strings. Check them first with GET /v1/enums or the free leadocean_list_enum_values MCP tool.

Step 2: Search and page with a cursor

Now pull rows. limit runs from 1 to 100 and the default is 25. Each row you receive is one record.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jobLevel": ["VP"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["CA"],
    "limit": 100
  }'

The response carries meta.nextCursor. Send it back as cursor in the same body to get the next page. One search pages to 10,000 rows. The last page has meta.depthCapped true and no nextCursor. Narrow the filters instead of walking further.

Step 3: Enrich only the rows you keep

Search rows are cheap to filter and costly to over-pull, so score them first. Then enrich the keepers. Enrich takes exactly one of linkedin_url, email, phone or person_id.

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 '{"person_id": "PERSON_ID_FROM_SEARCH", "reveal_email": true}'

There is no name plus domain lookup. Search by domain first, then enrich by person_id. A person we hold nothing for is a 404 and costs nothing. Since 17 October 2026 the enrich response also returns career history in resume_data: past roles, education, certifications and awards.

Step 4: Put it in one script

This script pages one search, keeps rows that pass your own rule, enriches them and backs off on a 429.

python
import os, time, requests

BASE = "https://api.leadocean.io"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}

def call(path, body):
    while True:
        r = requests.post(BASE + path, headers=H, json=body)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "1")))
            continue
        r.raise_for_status()
        return r.json()

query = {
    "jobLevel": ["VP"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["CA"],
    "limit": 100,
}
page = call("/v1/people/search", query)
for row in page["data"]:
    if not keep(row):          # your scoring rule
        continue
    person = call("/v1/people/enrich", {"person_id": row["meta"]["source_ids"]["person_id"], "reveal_email": True})
    print(person)

Swap print for a write to your CRM or queue. The API is the data layer: it has no sequencer, dialer or CRM of its own.

Step 5: Export when the list is big

Paging is for loops that run on a schedule or on a trigger. For a one-off list of thousands of rows, use POST /v1/exports instead. One request takes up to 50,000 rows and the daily ceiling is 500,000 rows.

The app at app.leadocean.io has an Exports page with a filter builder, a column picker, a price preview and a CSV download. It is the same export without code. Bulk CSV export is not on Free.

Design notes for a loop that runs unattended

Keep three habits and the loop stays cheap.

  • Size first, always. A sizing call is free, so gate every run on meta.total being inside a range you expect.
  • Store the person_id of every row you enrich. Re-enriching the same person spends another record.
  • Read email_status before you send. Bounced emails are not refunded, so do not send to risky or unknown addresses.

Pace the loop to your plan. Free allows 5 requests a second per account. Solo allows 50, Agency 200 and Platform 10,000. Above the limit you get a 429 with a Retry-After header, which the script above already honors.

Paid plans have a fair-use pace instead of credits. Reaching it slows the key until the window resets, and meta.notice says which window. Nothing stops and nothing needs a top-up.

What you get

The sizing call is free, so here is a real one. On 21 October 2026 the free leadocean_count_leads MCP tool returned this for the Step 1 filters.

json
{
  "total": 5574,
  "totalIsExact": true,
  "totalAll": 18800,
  "distinctCompanies": 3873,
  "credits": 0
}

The total field counts people with a verified, catch_all_valid or catch_all email, which is that tool's default.

The totalAll field is the same filters with no email condition. credits: 0 shows the call spent nothing.

This is the response shape from the API reference for an enriched person, with placeholder values.

json
{
  "profile_data": { "profile_full_name": "Jane Doe" },
  "contact_data": {
    "email_status": "verified",
    "contact_emails": [{ "email": "jane@acme.com" }],
    "contact_phones": [{ "phone": "+1 415 555 0133" }],
    "contact_current_experiences": [{ "title": "VP Sales", "company": "Acme" }]
  },
  "resume_data": { "experiences": [], "educations": [] },
  "meta": { "source_ids": { "person_id": "<digits>" } }
}

Fields appear only when we hold them. Past employers carry no company_id yet.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCopy the key again from app.leadocean.io and check the env var is set
429Above the plan's rate limit (5 requests a second on Free, 200 on Agency)Wait for Retry-After, then retry; or move up a plan
402Out of records on the Free planCheck GET /v1/account; move to Solo at $97 a month
Empty resultsA bad enum value, for example VP Sales as a jobLevelUse exact strings: C-Team, VP, Director, Manager, Staff, Other
404 on enrichWe hold nothing for that personNothing is charged; try another identifier
Search stops with meta.depthCappedOne search pages to 10,000 rowsAdd filters and split the list

FAQ

How many records does a run like this spend?

One record is one person you receive. A 100-row search page is 100 records, and each enrichment that finds someone is one more. Sizing calls, counts and misses cost nothing.

Can I run this on the Free plan?

Yes. Free has every feature, the whole database and 5 requests a second, inside 1,000 records for life. Bulk CSV export is not on Free.

When should I export instead of paging?

Use POST /v1/exports for big lists. An export takes up to 50,000 rows a request and 500,000 a day. It needs Solo or above.

Can an AI agent run the same loop?

Yes. The hosted MCP server at https://api.leadocean.io/mcp has 13 tools, including free counts and filter lookups. It signs in with OAuth, so no key is pasted.

Run your first sizing call free

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

Get your free API key →