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

How-to guide

How to Build an Online Lead Generation Portal (API and MCP)

A portal is a filter, a free count and a list you can download. Here are the real queries, in four steps.

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

An online lead generation portal is a place where you describe who you want, see how many exist, and download them. You can build the working core of one against an API in about 20 minutes.

Most pages on this topic compare directory sites and lead marketplaces. This one shows the queries. If you want a hosted screen with no code, the LeadOcean app (app.leadocean.io) has an Exports page: filter builder, column picker, price preview and CSV download. It is not a sequencer, dialer or CRM. This guide covers the API route behind it.

TL;DR

  • Four steps: size the audience for free, pull a sample, enrich one person, export the list as CSV.
  • It takes about 20 minutes. The test spends about 28 LeadOcean records. Counting spends none.
  • The Free plan covers Steps 1 to 3: 1,000 records for life, no card. Bulk CSV export is not on Free, so Step 4 needs Solo ($97 a month) or higher.

Prerequisites

  • A LeadOcean account and an API key from app.leadocean.io, stored as $LEADOCEAN_API_KEY.
  • curl and jq.
  • Optional: the hosted MCP server at https://api.leadocean.io/mcp (OAuth sign-in, no key pasted) if you would rather run Step 1 from an AI client.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"

Step 1: Size the audience before you pull anything

A portal that spends records to show a number is a bad portal. Send count=true as a query parameter with limit 1 in the body. It 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": ["CA"],
    "limit": 1
  }' | jq '.meta'

meta.total is capped at 100,000, so a wide filter reads as a floor. Filters are arrays of strings. Enum values are exact: VP, not vp, and Sales & Business Development, not Sales.

From MCP, the same sizing is the free tool leadocean_count_leads. For companies, use GET /v1/companies/count or leadocean_count_companies. Both are free.

Step 2: Pull a sample page

Pull 25 people to check the fit before you commit to a list. A search page of 25 is 25 records.

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": 25
  }' > page1.json

jq '.meta' page1.json

The body is flat JSON. limit runs 1 to 100. To get the next page, pass the nextCursor value back as cursor. One search pages to 10,000 rows. When meta.depthCapped is true, narrow the filters instead of walking further.

To walk more than one page, loop on the cursor:

bash
FILTERS='{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"limit":25}'
BODY="$FILTERS"
for i in 1 2 3; do
  curl -s -X POST "https://api.leadocean.io/v1/people/search" \
    -H "x-api-key: $LEADOCEAN_API_KEY" -H "Content-Type: application/json" \
    -d "$BODY" > "page$i.json"
  CURSOR=$(jq -r '.meta.nextCursor // empty' "page$i.json")
  [ -z "$CURSOR" ] && break
  BODY=$(echo "$FILTERS" | jq -c --arg c "$CURSOR" '. + {cursor: $c}')
done

Three pages of 25 spend 75 records. Keep the test at one page.

Step 3: Enrich one person

Search gives you the profile. Enrich adds contact detail and career history. Enrich takes exactly one of linkedin_url, email, phone or person_id. There is no name plus domain lookup, so search first and enrich by person_id.

bash
PERSON_ID=$(jq -r '.data[0].meta.source_ids.person_id' page1.json)

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\", \"reveal_email\": true}"

A person we hold nothing for is a 404 and costs nothing. For a direct phone, call GET /v2/people/phone. Check email_status before you send: bounced emails are not refunded.

Step 4: Export the list as a CSV

This is the download button of your portal. One request takes up to 50,000 rows, and the cap is 500,000 rows a day.

bash
curl -s -X POST "https://api.leadocean.io/v1/exports" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ca-vp-sales",
    "filters": {
      "jobLevel": ["VP"],
      "jobFunction": ["Sales & Business Development"],
      "country": ["CA"]
    },
    "limit": 5000,
    "columns": ["profile_full_name", "current_job_title", "current_company_name",
                "current_company_domain", "email_N_address", "email_N_status", "phone_N_number"]
  }'

Column ids come from GET /v1/exports/columns. Numbered columns are requested as email_N_address, and the CSV headers come back as email_1_address, email_2_address. Poll GET /v1/exports/{id} (free) until status is done, then download from GET /v1/exports/{id}/download. Each row is one record.

What you get

This is the free count for the Step 1 query, run with the MCP tool leadocean_count_leads on 2026-10-21. It uses the default mailable filter (verified, catch_all_valid or catch_all):

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

So 5,574 mailable VPs of sales in Canada across 3,873 employers. The same filters with no email condition match 18,800 people (totalAll). That gap is the reason to read the count label.

A person row from search looks like this. This is the response shape from the API reference, with placeholder values. Emails and phones appear only on enrich with reveal_email.

json
{
  "success": true,
  "data": [
    {
      "profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
      "contact_data": {
        "has_email": true,
        "has_phone": true,
        "email_status": "verified",
        "contact_current_experiences": [{ "...": "company name, domain, job title" }]
      },
      "meta": { "source_ids": { "person_id": "1234567890" } }
    }
  ],
  "meta": { "count": 25, "nextCursor": "(opaque string)" }
}

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCheck $LEADOCEAN_API_KEY is exported and the header name is exact
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Wait for Retry-After, then slow the loop
402Out of records (Free is 1,000 for life)Upgrade at /pricing; counts still work
Empty resultsBad enum value, such as vp for VPCheck exact strings with GET /v1/enums or leadocean_list_enum_values
404 on enrichNobody matched the inputCosts nothing. Try another identifier
Export refusedOver 50,000 rows a request, 500,000 a day, or on the Free planSplit the filter, or move to Solo or higher

FAQ

Is a portal the same as a lead marketplace?

No. A marketplace sells fixed lists. A portal lets you set the filters, see the count, then take the rows. The API above is the second kind.

Do I need to code?

Not for a one-off list. Use the Exports page in the app. Use the API when you want the portal inside your own tool.

How many records does a test cost?

About 28 here: 25 from the sample page and up to 3 enrichments. The count and a 404 are free.

How does this fit the funnel?

The portal sits at the top: it builds the list. See outbound lead generation, the lead generation funnel and predictive lead generation for what comes next.

Build your lead portal on a free API key

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

Get your free API key →