TL;DR
- A SERP API runs a search query and hands back the results page as JSON: title, URL, snippet and rank. LeadOcean does not sell one. It sells the people and company data you join to those results.
- The recipe takes about 20 minutes and spends about 11 LeadOcean records: 10 for the people search, 1 for the enrich. The web search call, the count and the filter lookups cost 0 records.
- The search side below uses Crustdata's Web Search API as the worked example. Any SERP API that returns URLs works the same way.
Prerequisites
- A LeadOcean account from app.leadocean.io. The free plan has no card and 1,000 records, one-off. Free runs at 5 requests a second.
- A LeadOcean key exported as
$LEADOCEAN_API_KEY. - A key for the SERP API you pick, exported as
$SERP_API_KEY. Crustdata's Web Search API works on all plans and has a free account with starting credits (Crustdata docs, September 2026). curl,jq.
export LEADOCEAN_API_KEY="paste-your-key-here"
export SERP_API_KEY="paste-your-serp-key-here"Step 1: Run the search
A SERP API takes a query string and returns ranked results as JSON. Ask for the pages that mention a buying signal, not for a person.
Crustdata's endpoint is POST https://api.crustdata.com/web/search/live. It takes a bearer token and an x-api-version header. It costs 1 credit a query and allows 10 requests a minute by default (Crustdata docs, September 2026). No public 1M price: Crustdata lists credits per call, not a dollar figure (Crustdata docs, September 2026).
curl -s https://api.crustdata.com/web/search/live \
-H "authorization: Bearer $SERP_API_KEY" \
-H "x-api-version: 2025-11-01" \
-H "content-type: application/json" \
-d '{
"query": "\"we are hiring\" \"head of growth\" series a saas",
"sources": ["web"],
"location": "US",
"start_date": 1760000000
}' > serp.jsonAlways set sources. Result fields differ by source, so an open query gives you uneven parsing. start_date is a Unix timestamp in seconds, and site limits results to one domain (reference).
Step 2: Pull the domains out of the results
A result row is a page, not a company. Strip each URL down to a hostname and drop the sites that are never your target.
jq -r '.results[].url' serp.json \
| sed -E 's#https?://(www\.)?([^/]+).*#\2#' \
| grep -v -E 'linkedin\.com|indeed\.com|glassdoor\.com|greenhouse\.io|lever\.co|ycombinator\.com' \
| sort -u > domains.txt
wc -l domains.txtJob boards and applicant systems host the post, not the employer. Keep the ones you cannot map to a company out of the file, or the next step matches the board's staff. Print domains.txt and read it before you spend anything.
Step 3: Count the people for free
LeadOcean sizes a list before it returns a row. On REST, a count is a search with count=true as a query parameter and limit at 1. The body is flat JSON and every filter is an array.
DOMAINS=$(jq -R . domains.txt | jq -s .)
curl -s "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d "{\"domain\": $DOMAINS, \"jobLevel\": [\"VP\", \"C-Team\"], \"limit\": 1}" \
| jq '.meta | {total, distinctCompanies, countOnly}'meta.total is capped at 100,000. A count costs nothing, so run it again with different levels or functions until the total fits the job. To check an enum value first, call GET /v1/enums or the MCP tool leadocean_list_enum_values.
Step 4: Search ten people
Search rows are a thin projection. Each person returned counts one record, so this spends 10.
curl -s "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d "{
\"domain\": $DOMAINS,
\"jobLevel\": [\"VP\", \"C-Team\"],
\"emailStatus\": [\"verified\", \"catch_all_valid\", \"catch_all\"],
\"limit\": 10
}" > people.json
jq '.meta | {count, credits, nextCursor, depthCapped}' people.jsonPage with meta.nextCursor. One search stops at 10,000 rows, and then meta.depthCapped is true.
Step 5: Enrich one person
Search finds people. Enrich returns the full record for one. Pass the person_id from the search row, since there is no name-plus-domain lookup.
PID=$(jq -r '.data[0].meta.source_ids.person_id' people.json)
curl -s "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d "{\"person_id\": \"$PID\", \"reveal_email\": true}" | jq '.data | keys'This costs 1 record, and reveal_email adds no extra charge. A person we hold nothing for is a 404 and costs nothing.
An agent can run Steps 3 to 5 itself over the hosted MCP server at https://api.leadocean.io/mcp (13 tools, OAuth sign-in). Pair it with a search tool and it runs the whole loop. More guides are on the blog. Related reading: AI agent data setup, people enrichment API, social posts API.
What you get
The SERP response shape, from the Crustdata docs, with placeholder values:
{
"success": true,
"query": "\"we are hiring\" \"head of growth\" series a saas",
"timestamp": 1760000000000,
"results": [
{ "source": "web", "title": "Head of Growth | Acme", "url": "https://acme.com/careers/head-of-growth", "snippet": "Acme is hiring a Head of Growth ...", "position": 1 }
],
"metadata": { "total_results": 7, "failed_pages": [], "empty_pages": [] }
}The people count is a real result. We ran the free leadocean_count_leads tool on 2026-10-21 (trimmed). Titles were "Head of Growth" and "VP Marketing", industry Computer Software, country US.
{
"total": 17,
"totalIsExact": true,
"totalAll": 55,
"distinctCompanies": 17,
"credits": 0
}That is 17 people with a mailable email (verified, catch_all_valid or catch_all), and 55 without the email filter. Your numbers will differ as the data changes.
A people search page answers with data, an array of thin person rows, and meta with count, limit, credits, nextCursor and depthCapped (API reference). Each row carries the person's id at meta.source_ids.person_id, which Step 5 reads, and the email flags has_email, email_status and email_type. A row never carries the address itself.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 on LeadOcean | Missing, wrong or revoked x-api-key | Re-copy the key from app.leadocean.io and export $LEADOCEAN_API_KEY again. |
| 401 on the SERP call | Bad bearer token for the search vendor | Check $SERP_API_KEY. Crustdata returns "Invalid API key in request". |
| 429 | Above your plan's rate (5 requests a second on Free, 200 on Agency) | Wait the Retry-After seconds. Crustdata's own limit is 10 requests a minute, so space the SERP calls too. |
| 402 | The 1,000 free records are spent | Check GET /v1/account, lower limit, or move to a paid plan. |
Empty data on a valid query | An enum value outside the catalogue, such as jobLevel: "vp" | Pass the exact string from GET /v1/enums, such as VP. |
depthCapped: true, no nextCursor | One search paged past 10,000 rows | Narrow the filters and run several smaller searches. |
FAQ
What is a SERP API?
A SERP API runs a search query and returns the results page as structured JSON instead of HTML. Each row has a title, URL, snippet and position. You skip scraping and bot checks.
Does LeadOcean have a SERP API?
No. LeadOcean returns people and company records, not search results pages. Use a SERP API to find pages and LeadOcean to find the people behind them.
Can an AI agent do this?
Yes. Give the agent a SERP tool and the LeadOcean MCP server. Tell it to call leadocean_count_leads before any tool that spends records.
How fresh is the data?
A SERP call is live. The LeadOcean dataset is refreshed monthly, and each record carries its own fetched_at date.
Turn search results into people you can reach
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →