An outbound playbook is three things: an audience, a trigger and a message. Most playbook templates give you the message and wave at the audience. This guide does the reverse and writes the audience as a JSON query you can size, run and version.
TL;DR
- Write each playbook as one flat JSON filter. Size it with a free call before you pull a single record.
- It takes about 30 minutes for three playbooks and spends about 25 LeadOcean records on the first pull. Sizing spends none.
- Gate every pull on
email_status, then hand the list to your sequencer. LeadOcean is the data layer, not the sender.
Prerequisites
- A LeadOcean account on the free plan (1,000 records for life, 5 requests a second). Sign up at app.leadocean.io.
- An API key exported as
$LEADOCEAN_API_KEY. curlandjq. Optional: the hosted MCP server athttps://api.leadocean.io/mcpif you work from Claude or ChatGPT.- A sequencer you already use. LeadOcean has no sequencer, dialer or CRM.
Step 1: Write the playbook as a filter
Each playbook is a flat JSON body. Filters are arrays of strings. Keep one file per playbook so the audience is reviewable in a pull request.
{
"jobLevel": ["VP", "C-Team"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"employeeRange": ["51-200"],
"industry": ["Computer Software"]
}Enum values are exact strings. If you guess "Software" you get zero rows. Look the value up first:
curl -s "https://api.leadocean.io/v1/enums" -H "x-api-key: $LEADOCEAN_API_KEY" | jq 'keys'The MCP tool leadocean_list_enum_values does the same with a search term, for example q: "software" on industry.
Step 2: Size the audience for free
Sizing is free. Send count=true as a query parameter, not in the body, with limit 1.
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","C-Team"],"jobFunction":["Sales & Business Development"],
"country":["US"],"employeeRange":["51-200"],"industry":["Computer Software"],
"limit":1}' | jq '.meta'meta.total is capped at 100,000. meta.countOnly is true and data comes back empty. If the number is under 50 or over 20,000, change the filter before you write any copy. A playbook with 30 people is a manual list. A playbook with 200,000 is not a playbook.
For company-level playbooks, GET /v1/companies/count returns an exact total with breakdowns by industry, headcount band, HQ country and HQ state. It is also free.
Step 3: Pull the first page
Add excludeTitle to drop noise, then pull a small page. Page size is 1 to 100, and meta.nextCursor pages forward.
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","C-Team"],"jobFunction":["Sales & Business Development"],
"country":["US"],"employeeRange":["51-200"],"industry":["Computer Software"],
"excludeTitle":["assistant","intern"],"limit":25}' > page1.json
jq '.meta' page1.jsonTwenty-five rows is 25 records. One search pages to 10,000 rows. If meta.depthCapped is true, narrow the filter instead of walking further.
Step 4: Gate on email status
Never send to what you have not read the status of. The mailable set is verified, catch_all_valid and catch_all. Everything else stays out.
jq '[.data[] | select(.contact_data.email_status | IN("verified","catch_all_valid","catch_all"))] | length' page1.jsonThere is no refund or credit-back for bounced emails, so this filter is your protection. To fetch an address you do not yet hold, enrich by person_id with reveal_email set:
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}'Enrich takes exactly one of linkedin_url, email, phone or person_id. An enrichment that finds nothing costs nothing. Since October 2026 it also returns career history (past roles, education, certifications), which is useful for the opener.
Step 5: Save three playbooks
Same query shape, different trigger. Each is a file in your repo.
| Playbook | Audience filter | Trigger you add | Message angle |
|---|---|---|---|
| New sales leader | jobLevel VP, jobFunction Sales & Business Development | Recent role start in the career history | Their first 90 days, one fix |
| Tech-stack swap | technologies set to the tool you replace | Tool detected on the company website | Migration cost, in numbers |
| Region expansion | country set to the new market, employeeRange bands | Your own launch date | Local proof, local language |
The tech-stack file, for example, is one more flat body:
{
"jobLevel": ["VP", "Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"technologies": ["HubSpot"],
"limit": 25
}The technologies filter covers about 7.8M crawled sites. It tells you a tool is on the site, not that the team is unhappy with it. Write the message for that uncertainty.
For a funding trigger, add fundingType and minLastFundingYear to the search. Do not use them to size, since the free count rejects those two filters. Size without them and expect the real list to be smaller.
Rerun each file monthly. The dataset refreshes monthly and each record carries its own fetched_at date. See how to automate outbound sales for wiring the result into a sender.
What you get
I ran the Step 1 filter through the free MCP tool leadocean_count_leads on 2026-10-21:
{
"total": 54,
"totalIsExact": true,
"totalAll": 205,
"distinctCompanies": 30
}total counts mailable addresses (verified, catch-all valid or catch-all). totalAll is the same filters with no email condition. The gap of 151 people is the reason to gate on status. The industry label Computer Software is exact and narrow, so a broader label such as Software Development gives a larger audience.
A record-level search row follows the response shape from the API reference, shown here with placeholder values.
{
"success": true,
"data": [
{
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
"contact_data": {
"has_email": true,
"email_status": "verified",
"email_type": "work",
"contact_current_experiences": [{ "...": "company name, domain, job title" }]
},
"meta": { "source_ids": { "person_id": "1234567890" } }
}
],
"meta": { "count": 25, "nextCursor": "(opaque string)" }
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Re-export $LEADOCEAN_API_KEY, send it as the x-api-key header |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Honour Retry-After, or throttle the loop |
| 402 | The 1,000 free records are spent | Move to Solo ($97 a month) or Agency ($497 a month) |
| Empty results | Enum value is not an exact string | Look it up with /v1/enums or leadocean_list_enum_values |
| Count rejects a filter | fundingType or minFunding in the free count | Size without it, apply it at search time |
| Export refused | Over 50,000 rows a request or 500,000 a day | Split by country, and run bulk CSV on Solo or above |
FAQ
How many playbooks should a team run at once?
Three. One per trigger you can act on this month. More than that and nobody reads the replies.
Does sizing cost records?
No. A count, a breakdown and an empty page cost nothing. Only people and companies you receive count.
Can I run this from Claude or ChatGPT instead of curl?
Yes. The hosted MCP server has 13 tools, including leadocean_count_leads and leadocean_list_enum_values, and signs in with OAuth. No key is pasted. See how to automate outbound sales.
Where does the sending happen?
In your sequencer. LeadOcean returns the list and the email status. For the wider picture, see the outbound sales automation hub and B2B outbound sales for B2B teams. Plans are on pricing.
Size your first outbound playbook for free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →