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

How-to guide

How to Personalize Sales Emails That Get Replies (With Queries You Can Run)

A reply needs a line that is true of one reader. Size the segment, pull a signal, enrich one person, write around one fact.

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

Personalization that gets replies is one specific line the reader recognizes as true, tied to one reason you are writing. A first-name merge tag is not that. This guide shows the queries, the output and a rule for picking the line.

TL;DR

  • Size a narrow segment for free, search it with a signal filter, enrich one person, write one line around one fact.
  • It takes about 25 minutes and spends about 11 records, inside the free 1,000.
  • If no fact passes the swap test in Step 4, send a short plain email. Do not fake a hook.

Prerequisites

Step 1: Size a narrow segment for free

Narrow beats broad. A tight segment shares a context, and a shared context is what lets one line sound personal. Counting spends no records.

Start with the role, the level, the country and a headcount band. Add a filter only when it changes who you would write to. If the count is under a few hundred, you can read every row. If it is over 20,000, the segment is too broad for a personal line, so cut it by country, headcount or a keyword.

bash
curl -s "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "jobLevel": ["Director"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["GB"],
    "employeeRange": ["51-200"]
  }' | jq '.meta'

On 2026-10-21 the free MCP tool leadocean_count_leads returned 6,930 people with a verified, catch_all_valid or catch_all email, across 4,642 companies. With no email condition the same filters matched 33,279. Say which number you mean when you quote one.

Step 2: Search with a signal filter

A signal is a reason to write now. The search body is flat JSON, filters are arrays and limit runs 1 to 100. Here the signal is a tool on the company website.

The technologies filter reads what a company runs on its site. Other useful signals in the same body are keywords for text in a person's own profile and minLastFundingYear for recent funding. Funding filters narrow the list at search time, but do not use them in a free count. Pick one signal per campaign so the reason for writing stays the same across the list.

bash
curl -s https://api.leadocean.io/v1/people/search \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "jobLevel": ["Director"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["GB"],
    "employeeRange": ["51-200"],
    "technologies": ["HubSpot"],
    "emailStatus": ["verified"],
    "limit": 10
  }' > people.json

jq -r '.data[] | [.meta.source_ids.person_id,
  .contact_data.contact_current_experiences[0].job_title,
  .contact_data.contact_current_experiences[0].company_name,
  .contact_data.email_status] | @tsv' people.json

Ten rows is ten records. Page on with cursor when you want more. One search pages to 10,000 rows, then meta.depthCapped turns true. Narrow the filters instead of walking deeper.

Step 3: Enrich one person for candidate facts

Search gives title and company. Enrich adds career history: past roles, schools, certifications and awards. Those are facts a stranger would not guess. Enrich takes exactly one of linkedin_url, email, phone or person_id.

bash
PID=$(jq -r '.data[0].meta.source_ids.person_id' people.json)

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\": \"$PID\"}" > person.json

jq '{now: .data.contact_data.contact_current_experiences[0],
     past: .data.resume_data.experiences[0:2],
     awards: .data.resume_data.awards[0:1]}' person.json

A person we hold nothing for is a 404 and costs nothing. Past employers carry no company_id yet, so compare them by name. Each section appears only when held.

Read the output before you decide anything. Look for a move between companies, a certification, a published piece, or an award. A long tenure at one employer is also a fact, and a safe one to leave alone. Skip anything that feels like you watched the person. A line that is true but creepy loses the reply.

Step 4: Pick the fact with the swap test

The swap test: paste your line into an email to the next person on the list. If it still reads true, it is a merge tag in disguise.

text
Fact                              Swap test   Why
Title: Director of Sales          Fails       Every row has it
Company size: 51-200              Fails       The whole segment shares it
Tool on site: HubSpot             Weak        Shared, but it explains why you write
Past role at a named employer     Passes      Specific to this career path
Award in resume_data.awards       Passes      Rare and checkable

Pick one fact. Use the signal from Step 2 as the reason and the career fact as the opener. Two openers read like a script.

The reason matters as much as the opener. The opener earns the next sentence, and the reason is why the email exists. If you cannot state the reason in one sentence, the email is not ready. A good opener on a weak reason still gets deleted.

Step 5: Write around the one fact

Open with the fact, give one reason, ask one question. Keep it under 90 words.

text
Subject: {{past_company}} to {{company}}

Hi {{first_name}},

You went from {{past_title}} at {{past_company}} to leading sales at {{company}}.
{{one_line_on_why_hubspot_matters_to_your_offer}}

I work on {{problem}}. Is routing on your list this quarter?

{{your_name}}

Fill the variables in your own script or sending tool. Read each email aloud once. If a sentence only works because of a variable, cut it.

Keep the subject line short and lowercase-friendly. Name a place or a company, not a pitch. Avoid fake threads like "Re:" and avoid asking for fifteen minutes in the first email. One question that can be answered in a sentence gets more answers than a calendar link.

Step 6: Check email status before you send

Read contact_data.email_status on every row. We do not refund bounces.

Send only verified rows to start. If you add catch_all rows later, send them from a separate, smaller batch and watch bounces before you scale. Sending volume and domain health are your sending tool's job, so keep each batch small enough to read replies by hand.

bash
jq -r '.data[] | select(.contact_data.email_status=="verified")
  | .meta.source_ids.person_id' people.json | wc -l

What you get

Response shape from the API reference, with placeholder values (not a real person):

json
{
  "success": true,
  "data": [
    {
      "profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "Director of Sales at Acme" },
      "contact_data": {
        "has_email": true,
        "email_status": "verified",
        "contact_current_experiences": [
          { "job_title": "Director of Sales", "company_name": "Acme", "company_domain": "acme.com" }
        ]
      },
      "meta": { "source_ids": { "person_id": "1234567" } }
    }
  ],
  "meta": { "count": 1, "credits": 1, "nextCursor": null }
}

Enrich adds resume_data with experiences[], educations[], certifications[] and awards[], each present only when held.

The output of Step 1 on 2026-10-21 is a free count: 6,930 mailable people, 33,279 with no email condition. Steps 2 and 3 spend records. The search page of 10 spends 10 and the enrichment spends 1, so the whole run is 11.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCopy the key from app.leadocean.io and export it again
429Above the plan rate limit (5 a second on Free, 200 on Agency)Honor Retry-After, or move to a higher plan
402Records spent (Free has 1,000 for life)Check GET /v1/usage, then pick a plan on pricing
Empty resultsA bad enum value, such as director instead of Director for jobLevelCheck exact strings with leadocean_list_enum_values
404 on enrichNothing held for that personNo cost. Try another person_id
Export refusedOver 50,000 rows a request or 500,000 a daySplit the export. Bulk CSV is not on Free

FAQ

How much personalization is enough?

One true line. More than one reads as research by script, and the extra facts are better saved for the follow-up.

Are first name and company name personalization?

No. Every record has both. Treat them as formatting.

What if the record has no usable fact?

Send a short, plain email that says why you are writing. See the pharma cold email samples for how plain copy can still land.

Can I do this for 1,000 people?

Yes, with a script. Loop over the search page, enrich each person_id, and keep the swap test as a filter. Use the Exports page in the app for bulk CSV on a paid plan.

How many records does this cost?

About 11 here. Counting is free, and an enrichment that finds nobody costs nothing.

Find the one fact that makes your next email worth answering

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →