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

How-to guide

How to Personalize Outbound Sales Emails (With Queryable B2B Data)

Pull the fields that make a cold email specific, check the list size for free, then merge them into a template. Five steps, about 25 records.

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

Personalization that works is a field you can query, not a guess a writer makes at 4 pm. If the data says "VP of Sales, US, joined this role recently", the first line writes itself. This guide builds that pipeline with the LeadOcean API.

TL;DR

  • Segment by structured fields (job level, function, country), size the list for free, then enrich only the people you will email.
  • Takes about 30 minutes. A 25-person test batch spends about 25 records of your 1,000 free ones.
  • Personalize on facts the record holds (current role, past roles, email status), and skip anything you would have to invent.

Prerequisites

  • A LeadOcean account and API key from app.leadocean.io. The Free plan gives 1,000 records for life, no card, 5 requests a second.
  • The key in your shell: export LEADOCEAN_API_KEY=your_key_here.
  • curl and jq, or any HTTP client.
  • A sender you already use. LeadOcean returns data. It has no sequencer, so the merge happens in your own tool.

Step 1: Pick the segment with filters, not a title guess

Generic lists produce generic emails. Start with fields that map to one pain point. Enum values are exact strings, so check them first with GET /v1/enums (or the free MCP tool leadocean_list_enum_values). For job level the valid values are C-Team, Director, Manager, Other, Staff and VP.

bash
curl -s "https://api.leadocean.io/v1/enums" \
  -H "x-api-key: $LEADOCEAN_API_KEY" | jq 'keys'

Pick one level, one function and one country. A narrow segment lets one angle fit everyone in it.

Step 2: Size the list before you spend anything

A count costs nothing. On the search endpoint, count=true goes in the query string (not the JSON body, which rejects unknown fields), with limit 1 in the body. The body is flat JSON and filters are arrays.

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": ["US"],
    "limit": 1
  }' | jq '.meta'

meta.total is capped at 100,000 on this endpoint. The MCP tool leadocean_count_leads returns the exact total plus facets. If the number is too large to write for, add a filter (company size or industry) until it is not.

Step 3: Pull a small batch of people

Fetch 25 people. Each person returned is one record. limit runs 1 to 100, and you page with cursor.

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": ["US"],
    "limit": 25
  }' > batch.json
jq '.meta' batch.json

Keep the person_id from each row (meta.source_ids.person_id). You need it in the next step, because there is no name plus domain lookup: search first, then enrich by person_id.

Step 4: Enrich for the personalization fields

Enrichment takes exactly one of linkedin_url, email, phone or person_id. Add reveal_email to get the address back. A person we hold nothing for is a 404 and costs nothing.

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_3", "reveal_email": true}' \
  > person.json

Since October 2026 the response carries career history. The current role sits in contact_current_experiences[]. Past roles, education, certifications and awards sit in resume_data (experiences[], educations[], certifications[], awards[]), each when held. Past employers have no company_id yet, so match them on name only.

Check the email status before you write a word. Bounces are not refunded.

Step 5: Merge fields into a template

Choose one personalization line per email, drawn from a field the record holds. Three that survive scrutiny:

FieldOpening line pattern
Current role in contact_current_experiences[]"You run sales at {company}, so ..."
Past employer in resume_data.experiences[]"You spent time at {past_company} before this ..."
Certification in resume_data.certifications[]"Saw you hold {certification} ..."

A short merge script:

python
import json

p = json.load(open("person.json"))["data"]
past = (p.get("resume_data") or {}).get("experiences") or []

line = "I'll keep this to two sentences."
if past:
    # check the key name and the order of experiences[] in your own response
    line = f"You were at {past[0].get('company_name', 'your last company')} before this role."

print(line)

Test the key names against your own response first. If a field is empty, fall back to a plain line. A wrong detail costs more than no detail.

A full template

Keep the email under 90 words. One line of proof that you read the record, one line of relevance, one ask.

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

Hi {{first_name}},

{{personal_line}}

{{one line on what you do for teams like theirs}}
Worth a look this week?

Jane

Notice what is not in it: no "hope you are well", no invented compliment, no claim about their quarter. If the record holds nothing usable, send the plain version. A plain email to the right person beats a forced opener to the wrong one.

Sort by email status first

Before merging, split the batch on email_status. Send to verified first, then catch_all_valid and catch_all at a lower daily volume. Hold risky, unknown and untested back. Drop invalid, role, disposable, spam_trap and abuse. This protects the sending domain, which matters more than any opening line.

Test one variable at a time

Send the personalized version to half of a segment and the plain version to the other half. Measure replies, not opens. With a segment of a few hundred people the result is noisy, so treat a difference of a few replies as a hint, not a verdict. Run the same test on the next segment before you rewrite your whole playbook.

Pick the angle per segment, not per person

Write the angle once for the segment and let the fields fill in the details. A VP of Sales at a 1-10 person company and a VP of Sales at a 10001+ company have different problems. Split on the company size facet from Step 2, write two angles, and merge the personal line on top.

Keep a log of which field produced each opening line. After a week you will know whether past employers or current role earns more replies, and you can stop pulling the fields that do not.

What you get

A real free count run on 2026-10-21 for Step 2's segment (jobLevel VP, jobFunction Sales & Business Development, country US), through the leadocean_count_leads MCP tool:

json
{
  "total": 129494,
  "totalIsExact": true,
  "totalAll": 350517,
  "distinctCompanies": 58121,
  "credits": 0,
  "defaults": "Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)"
}

That is 129,494 people with a mailable address out of 350,517 matches, across about 58,121 employers (distinct company count is approximate). It cost 0 records. Run it again with facets on (the default) to split the list by industry and company size, which is how you cut it into segments one angle can fit.

Response shape from the API reference for an enrich call, with placeholder values:

json
{
  "success": true,
  "data": {
    "profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
    "contact_data": {
      "email_status": "verified",
      "contact_emails": [{ "email": "jane@acme.com", "type": "work", "status": "verified" }],
      "contact_current_experiences": [{ "...": "company name, domain, job title" }]
    },
    "resume_data": {
      "experiences": [{ "...": "title, company name, start and end dates, job level" }],
      "educations": [],
      "certifications": []
    },
    "meta": { "source_ids": { "person_id": "1234567890" } }
  },
  "meta": { "credits": 1 }
}

The groups follow the API reference. Keys inside each past role are summarized, so read your own person.json before you map them.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerCheck $LEADOCEAN_API_KEY is exported and sent as x-api-key
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Honour the Retry-After header and slow the loop
402Out of records (Free is 1,000 for life)Check GET /v1/usage, then move to Solo ($97 a month)
Empty dataEnum value misspelled, for example vp instead of VPPull exact strings from GET /v1/enums
404 on enrichWe hold nothing for that personNothing is charged. Skip the person
Search stops at 10,000 rowsDepth cap (meta.depthCapped is true, no nextCursor)Narrow the filters instead of paging further

FAQ

How much personalization is enough for a cold email?

One true, specific line is enough. Segment-level relevance (right role, right pain) does more work than a clever opener. Put the effort into the segment first.

Does personalization help if the email bounces?

No. Read email_status before sending. Defaults count verified, catch_all_valid and catch_all as mailable, and LeadOcean does not refund records for bounced emails.

Can I do this from an AI client instead of curl?

Yes. LeadOcean runs a hosted MCP server with 13 tools. leadocean_count_leads and leadocean_list_enum_values are free, so an assistant can size and shape a segment before it spends records.

How many records does a test cost?

About 25 for the batch in Step 3, plus enrichment on the people you keep. The Free plan holds 1,000, so a first test uses a small part of it. Counts, filter listings and empty enrichments cost nothing.

Where do I send the emails?

Use your own sender. LeadOcean has no sequencer. For list handoff, POST /v1/exports writes up to 50,000 rows a request (500,000 a day), and bulk CSV export is not on Free. See pricing.

Related reading: how to personalize sales emails that get replies, 7 personalized email strategies for sales and marketing alignment and outbound sales automation.

Personalize from fields you can query. Start free.

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

Get your free API key →