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

How-to guide

How to Build Personalized Sales Email Templates (From Real Prospect Data)

A personalized template is a template with a field you can fill from data. Here are five, and the queries that fill them.

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

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=...
  • curl and jq.
  • 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.

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'

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.

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"],
    "employeeRange": ["51-200"],
    "limit": 10
  }' > page1.json

jq -r '.data[].meta.source_ids.person_id' page1.json

Each 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.

bash
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.

bash
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"
done

Save 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):

json
{
  "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:

json
{
  "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

ErrorCauseFix
401Missing or wrong x-api-keyCheck $LEADOCEAN_API_KEY is exported and sent in that header.
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Wait for Retry-After, then slow the loop.
402Out of recordsFree is 1,000 records for life. Move to a paid plan, see /pricing.
Empty resultsA filter value is not an exact enum stringCheck GET /v1/enums, or the leadocean_list_enum_values tool.
404 on enrichNobody matched the inputCosts nothing. Try a different input or skip the person.
Export refusedOver 50,000 rows a request or 500,000 a daySplit 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?
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 →