A personalized sales email template has a slot for something true about the reader. Title, employer, headcount, a past role. If you cannot fill the slot from data, it is not personalization, it is a first name.
This guide is the short, technical version. You size a list, pull the fields, fill five templates and send from your own mail tool.
TL;DR
- Five templates, each tied to one field you can pull by API: title, industry, headcount band, past employer, new role.
- About 20 minutes the first time. Counting the list is free. The example spends about 11 records.
- LeadOcean supplies the data. It has no sequencer, so you send from your own tool.
Prerequisites
- A LeadOcean account. The Free plan has 1,000 records for life, no card, 5 requests a second.
- An API key in your shell:
export LEADOCEAN_API_KEY=... curlandjq.- A mail tool or sequencer that accepts merge fields.
Step 1: Count the audience before you write
Count first. Counting is free, and a list of 40 people needs a different email than a list of 40,000.
Example: VPs of sales in Canada.
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'count is a query parameter, not a body field. meta.total is capped at 100,000. Enum values are exact strings (VP, not vp), so check them with GET /v1/enums if a filter returns nothing.
Step 2: Pull the people and the fields
Run the same filters without count and take one page. The body is flat JSON, filters are arrays and limit runs 1 to 100.
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"],
"employeeRange": ["51-200"],
"limit": 10
}' > page1.json
jq -r '.data[].meta.source_ids.person_id' page1.jsonEach person returned counts as one record, so this page spends 10. Page on with cursor. One search pages to 10,000 rows, so narrow the filters instead of walking the index.
Step 3: Enrich the people you will write to
Search gives you the headline fields. Enrich one person by person_id (from meta.source_ids.person_id on the search row) to get the email and career history.
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_STEP_2", "reveal_email": true}' \
| jq '{current: .data.contact_data.contact_current_experiences, past: .data.resume_data.experiences, status: .data.contact_data.email_status}'Enrich takes exactly one of linkedin_url, email, phone or person_id. Since 2026-10-17 it returns past roles, education, certifications and awards. Past employers have no company_id yet. A person we hold nothing for is a 404 and costs nothing.
Read email_status before you send. Bounced emails are not refunded.
Step 4: Fill the templates
Each template needs one field. If the field is empty for a person, skip that template for them. Do not fake it.
1. Title. Field: the current role in contact_current_experiences.
Subject: A question for {{title}} teams Hi {{first_name}}, most {{title}}s I talk to lose time on {{problem}}. We fixed it by {{one_line_fix}}. Worth 15 minutes?
2. Industry. Field: industry.
Subject: {{industry}} and {{problem}} Hi {{first_name}}, {{industry}} teams hit {{problem}} around {{trigger}}. Here is one thing that worked: {{your_own_result}}.
3. Headcount band. Field: employeeRange.
Subject: At {{range}} people, this breaks Hi {{first_name}}, companies your size usually feel {{problem}} when {{trigger}}. Is that live for you?
4. Past employer. Field: a past role in resume_data.
Subject: Fellow {{past_company}} alum Hi {{first_name}}, I saw you spent time at {{past_company}}. Teams there often hit {{problem}}. Is it on your list this quarter?
5. New role. Field: a recent change in contact_current_experiences.
Subject: Congrats on {{new_title}} Hi {{first_name}}, new roles reset priorities. Is {{problem}} one of yours?
Use your own results for {{your_own_result}}. Do not borrow numbers from someone else's case study.
Step 5: Merge and send
Merge in a script, then hand the file to your sender. This version writes one draft per enriched person to a folder.
mkdir -p drafts
for f in enriched/*.json; do
id=$(jq -r '.data.meta.source_ids.person_id' "$f")
name=$(jq -r '.data.profile_data.profile_full_name' "$f")
printf 'Subject: A question for your team\n\nHi %s, ...\n' "$name" \
> "drafts/${id}.txt"
doneSave each Step 3 response to enriched/<person_id>.json first. Print one file and check the field names before you merge a whole list.
Keep to the plan's rate limit: 5 requests a second on Free, 200 on Agency. A 429 carries a Retry-After header. For bigger lists, POST /v1/exports takes up to 50,000 rows a request and 500,000 a day. Bulk CSV export is not on Free. The app at app.leadocean.io has an Exports page with a filter builder, a column picker and a price preview.
What you get
The Step 1 count, run with the leadocean_count_leads MCP tool on 2026-10-21 (jobLevel VP, jobFunction Sales & Business Development, country CA):
{
"total": 5574,
"totalIsExact": true,
"totalAll": 18800,
"distinctCompanies": 3873,
"credits": 0
}total counts only addresses worth sending to (verified, catch_all_valid or catch_all). totalAll has no email condition. Counting spends 0 records.
A search page, response shape from the API reference, with placeholder values:
{
"success": true,
"data": [
{
"profile_data": { "profile_full_name": "Jane Doe" },
"contact_data": { "has_email": true, "email_status": "verified" },
"resume_data": {},
"meta": { "source_ids": { "person_id": "1234567890" } }
}
],
"meta": { "count": 1, "limit": 10, "nextCursor": null }
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is exported and sent in that header. |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Wait for Retry-After, then slow the loop. |
| 402 | Out of records | Free is 1,000 records for life. Move to a paid plan, see /pricing. |
| Empty results | A filter value is not an exact enum string | Check GET /v1/enums, or the leadocean_list_enum_values tool. |
| 404 on enrich | Nobody matched the input | Costs nothing. Try a different input or skip the person. |
| Export refused | Over 50,000 rows a request or 500,000 a day | Split the export by filter. |
FAQ
How many fields should one email use?
One real field is enough. Two is fine. More starts to read as surveillance, and each extra field is another chance to be wrong.
Do I need to enrich everyone?
No. Search returns the headline fields, and enrichment costs a record per person. Enrich only the people you will write to.
Where do I send the emails from?
Your own mail tool. LeadOcean supplies the data and the export, nothing more.
What do the other template pages cover?
Follow-ups are in 10 follow-up templates for stalled deals. Alignment is in 7 personalized email strategies. Worked letters are in 11 personalized sales email letters.
What does it cost?
Free is 1,000 records for life. Paid plans start at $97 a month and have a fair-use pace, not credits. See /pricing.
Fill your next template with real fields, not guesses
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →