GTM engineering means building your go-to-market as software: a list defined in code, enriched by an API, shipped on a schedule. Clay announced a $100M Series C at a $3.1B valuation "to power GTM engineering" (Clay blog, checked October 2026). The category is funded. This guide is the part a builder can run today.
You will make four calls against the LeadOcean API: count a list, search it, enrich one person, then export the lot as CSV. Each call is below with its output. For the role itself, read what a GTM engineer does.
TL;DR
- Four calls: count, search, enrich, export. About 15 minutes end to end.
- The free plan spends about 26 records on this walkthrough (25 search rows, 1 enrichment). Counts, empty pages and misses cost nothing.
- Count first, always. A free count tells you the size of the list before you spend a record on it.
Prerequisites
- A LeadOcean account on the free plan: 1,000 records for life, no card, 5 requests a second (sign up).
- An API key from the app, exported as
$LEADOCEAN_API_KEY. curlandjq. Any HTTP client works the same way.- Optional: the hosted MCP server at
https://api.leadocean.io/mcpif you want an agent to run these calls. It has 13 tools. See Claude Code for GTM engineering.
export LEADOCEAN_API_KEY="paste-your-key-here"What the funding changes for builders
Not much about the work, a lot about the expectation. Buyers now assume a GTM engineer owns the list, the enrichment and the send logic. They expect the pipeline to be reproducible, not a pile of one-off exports.
Reproducible means three things. The list is defined by a filter body you can read. The size is checked before the spend. The output lands in a file or a system you control. Everything below follows those three rules.
Pick your tool by how you work. If you think in tables, use a table tool and call LeadOcean from it (Clay integration, n8n integration). If you think in code, call the API directly. If you want an agent to do the loop, connect the MCP server.
Step 1: Count the list before you pull it
A free count sizes a list without spending a record. Send count=true as a query parameter with limit=1. The JSON body rejects unknown fields, so the flag goes in the URL.
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 '{"title": ["GTM Engineer"], "country": ["US"]}' | jq '.meta'meta.total is capped at 100,000, and the data array comes back empty with meta.countOnly true. Size a company list the same way with GET /v1/companies/count, which is also free and breaks the total down by industry, headcount band and HQ country.
If the number is wrong, change the filters now. Filters are arrays of strings. Enum values are exact strings, so check them with leadocean_list_enum_values or GET /v1/enums first.
Step 2: Search the list
Search returns people one page at a time. Each person returned costs one record. Page size is 1 to 100, and you walk pages with cursor.
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": ["GTM Engineer"], "country": ["US"], "limit": 25}' \
| jq '{rows: (.data | length), next: .meta.nextCursor}'Pass the returned nextCursor as "cursor" in the next body to get page two. One search pages to 10,000 rows. Past that, meta.depthCapped is true and no cursor comes back. The fix is narrower filters and several smaller searches, not harder paging.
Each row carries meta.source_ids.person_id. Keep it. Step 3 uses it.
Step 3: Enrich one person
Search rows carry email flags, not addresses. To get the address, enrich by person_id. Send exactly one identifier: linkedin_url, email, phone or person_id.
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": "1234567890", "reveal_email": true}' | jq '.data'reveal_email returns the address and costs no extra record. A person with nothing on file returns 404 and costs nothing. Since October 2026 the response also carries career history in resume_data: past roles, education, certifications and awards. Past employers have no company_id yet, so match them by name.
Step 4: Export the list as CSV
Past a few hundred rows, stop paging and export. POST /v1/exports takes the same filters as search, plus a row limit and a column preset. It runs async and reserves records up front.
curl -s -X POST "https://api.leadocean.io/v1/exports" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "gtm-engineers-us",
"filters": {"title": ["GTM Engineer"], "country": ["US"]},
"limit": 500,
"preset": "search"
}' | jq '.data'Poll GET /v1/exports/{id} until it is done, then fetch GET /v1/exports/{id}/download. The ceiling is 50,000 rows a request and 500,000 a day. Bulk CSV export is not on the free plan. On Free, page with search instead, or use a paid plan (pricing). You can also run exports from the Exports page in the app: filter builder, column picker and a price preview before you start.
The search preset has no contact points. The default full preset has 45 columns, including up to 3 emails and 2 phones, and needs the enrich scope.
Step 5: Put it on a schedule
A list you pull once is a spreadsheet. A list that refreshes itself is GTM engineering. The dataset is refreshed monthly, and each record carries its own fetched_at date, so a monthly job is the right cadence.
This script counts first and only pulls when the list has grown. It uses one free call per run until there is something new.
#!/usr/bin/env bash
set -euo pipefail
BODY='{"title": ["GTM Engineer"], "country": ["US"]}'
NOW=$(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 "$BODY" | jq '.meta.total')
LAST=$(cat last_count.txt 2>/dev/null || echo 0)
if [ "$NOW" -gt "$LAST" ]; then
echo "List grew from $LAST to $NOW. Start an export or a search here."
echo "$NOW" > last_count.txt
fiRun it from cron, GitHub Actions or an n8n schedule node. Keep the pull step separate from the count step, so a bad filter costs you nothing. Keep the record budget in the script too: read GET /v1/usage before a large pull and stop if the number left is lower than the rows you asked for.
Three habits separate a pipeline from a one-off pull:
- Version your filters in git. The filter body is the definition of your market.
- Log the count with its date. It is your only honest record of how a list changed.
- Enrich only the rows you will act on. Search is cheap to size and every enrichment is a record.
What you get
Step 1 as an MCP count, run on 21 October 2026 with leadocean_count_leads and the filter title: GTM Engineer, worldwide, no country filter:
{
"total": 475,
"totalIsExact": true,
"totalAll": 1617,
"distinctCompanies": 404,
"credits": 0,
"defaults": "Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)"
}That is 475 people with a mailable address, out of 1,617 with the title, spread over 404 companies. Cost: 0 records. A count with the default email filter is a mailable count. Without it the same filters match 1,617 people.
An enrichment response has this shape (response shape from the API reference, placeholder values):
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe" },
"contact_data": {
"email_status": "verified",
"contact_emails": [{ "email": "jane@acme.com", "type": "work", "status": "verified" }],
"contact_current_experiences": [{ "job_title": "GTM Engineer", "company_name": "Acme", "company_domain": "acme.com" }]
},
"resume_data": { "experiences": [], "educations": [] }
},
"meta": { "credits": 1 }
}Read contact_data.email_status before you send. Bounced emails are not refunded.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header. | Copy the key from the app again. Confirm $LEADOCEAN_API_KEY is set in this shell. |
| 429 | Above the plan's rate limit: 5 requests a second on Free, 200 on Agency. | Wait the Retry-After seconds, then retry. Throttle the loop. |
| 402 | Out of records. The free plan's 1,000 are spent. | Check GET /v1/account. Shrink the pull or move to Solo or Agency. |
| Empty results | A filter value that is not an exact enum string, such as vp for jobLevel. | List valid values with leadocean_list_enum_values. jobLevel is VP, seniority is vp. |
400 export_limit_rows | The export asked for more than 50,000 rows. | Split the export by country or industry into smaller requests. |
429 export_limit_daily | The 500,000 rows a day ceiling is reached. | Wait for resetsAt in the response, then run the next export. |
FAQ
What is a GTM engineer?
A GTM engineer builds the sales and marketing system as code: lists, enrichment, routing and outbound sequences wired together with APIs and agents. The role sits between RevOps and software engineering. See what a GTM engineer does.
Do I need Clay to do GTM engineering?
No. Clay is a table-and-workflow tool, and many teams use it. An API plus a script, n8n or an agent covers the same ground if you prefer code. LeadOcean has no table or workflow builder, so pair it with whichever you choose.
How many records will a first list cost?
A search row is one record and an enrichment that finds someone is one record. Counts, 404s and empty pages are free. Size with a count first, then multiply by the rows you will pull.
Where do I go next?
Read everything you need to know about AI GTM engineering for the wider picture. To compare training options, see the best AI GTM engineering courses. Every guide is in the blog.
Build your first GTM engineering list with a free API key
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →