TL;DR
- Warming a lead before a call means two small touches and one specific reason to call, all before you dial. You need a list with phones and a fact about the person's role.
- It takes about 45 minutes and spends up to 35 of your 1,000 free records. Counts, empty pages and enrichments that find nothing cost nothing.
- The method is plain: size the list, pull callable people, enrich the first ten, touch twice, then call.
Prerequisites
- A LeadOcean API key from app.leadocean.io, as
$LEADOCEAN_API_KEY. The free plan has no card, 1,000 records for life and 5 requests a second. - A LinkedIn account and an email account you already send from. Steps 5 and 6 are manual.
curlandjq. A spreadsheet works for the call sheet.- A phone, or a dialer you already own. LeadOcean is a data API, so it has no dialer, sequencer or CRM.
Step 1: Define one buyer and count them for free
Pick one narrow buyer. A call list of 25 people who share a job, a size band and a country is easier to warm than 250 mixed ones.
Count them first. count=true goes in the query string, not the JSON body, and "limit": 1 in the body keeps the page empty.
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"],"employeeRange":["51-200"],"hasPhone":true,"limit":1}' \
| jq '.meta | {total, totalIsExact, countOnly}'Enum values are exact strings. Check them with GET /v1/enums before you guess, because a wrong value returns an empty list.
Step 2: Pull 25 people who have a phone number
Search the same buyer, drop the count flag and keep one page. One page of 25 spends 25 records, so keep limit at 25 for the first run.
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"],"employeeRange":["51-200"],"hasPhone":true,"hasEmail":true,"emailStatus":["verified","catch_all_valid"],"limit":25}' \
> page1.json
jq '.meta | {count, nextCursor}' page1.jsonhasPhone and emailStatus do two jobs. You get a number to dial and a verified address for the warm-up email in Step 5. Page again with cursor from meta.nextCursor only when you have worked this page.
Step 3: Give every call a reason to exist
A warm call has a reason that is true of this person, not of "VPs of sales". Pull the fields that give you one: the current role, the company and its size.
Enrich the ten people you will call first. Enrich takes exactly one of linkedin_url, email, phone or person_id. Each search row carries its id at meta.source_ids.person_id.
for id in $(jq -r '.data[:10][] | .meta.source_ids.person_id' page1.json); do
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\":\"$id\",\"reveal_email\":true}" |
jq -c '.data | {name: .profile_data.profile_full_name,
role: .contact_data.contact_current_experiences[0].job_title,
company: .contact_data.contact_current_experiences[0].company_name,
past: [.resume_data.experiences[:2][]? | {title, company_name}],
phone: .contact_data.contact_phones[0].phone}' >> callsheet.jsonl
sleep 0.3
doneSince 17 October 2026 person enrich returns career history in resume_data: past roles, education and certifications, each when held. A person who joined from a competitor, or who moved from engineering into sales leadership, gives you a true first line. Past employers carry no company_id yet, so match them by name.
Step 4: Write one opener per person from the data
Write one sentence per call. Read the row, pick the single most specific fact, and say it.
role change: "You moved into the VP Sales seat at Acme about a year ago. I help new sales leaders ..."
past employer: "I saw you ran partner sales at Globex before this. Do you still ..."
size band: "Companies of 50 to 200 people usually hit the same ramp problem. Is that live for you?"Do not read a script. Keep the opener under 20 seconds and end it with a question the person can answer in a few words. If a row gives you nothing specific, move it to the bottom of the list.
Step 5: Touch them twice before you call
Two light touches are enough. Do both the day before and the morning of the call.
- Day -1: view the person's LinkedIn profile from your own account. Do this by hand.
- Day 0, morning: send a two-line email to the verified address that mentions the same fact as your opener, with no ask beyond a reply.
Subject: Quick question on ramp time at Acme
Jane, saw you moved into the VP Sales role last year. Do new reps at Acme ramp
on the same playbook, or did you rebuild it? Calling you at 2pm either way.Read email_status before you send. Send only to verified or catch_all_valid, and hold catch_all back. Bounced emails are not refunded.
Step 6: Call in blocks and log the outcome
Call in blocks of ten, within a few hours of the email. Log one of four outcomes: no answer, wrong person, call back, or meeting. After the block, move the no-answers to a second window.
name,phone,opener_fact,outcome,next_step
Jane Doe,+1 415 555 0133,moved into VP Sales,call back,Thu 2pmThose values are placeholders. Check your local rules on cold calling and do-not-call lists before you dial. LeadOcean returns the number; the compliance check stays with you.
Step 7: Review the first block before you scale
After ten calls, read the outcomes before you pull page two. Look at which opener facts got a conversation and which got a hang-up. Keep the facts that worked and drop the rest from the next template.
Check three things:
- How many numbers were wrong or disconnected. If it is more than a few, tell your rep to log the number so you can compare it with the record's
fetched_atdate. The dataset is refreshed monthly. - How many people the email reached. Compare the replies with the
email_statusyou sent to. - How many rows had no usable fact. Those need a better filter, not a better script.
Then widen carefully. Add cursor from meta.nextCursor to get the next 25, or narrow the filters to a smaller, sharper buyer. One search pages to 10,000 rows, and a capped page has meta.depthCapped true and no nextCursor. Narrow the filters before you walk the index.
Keep the cadence fixed so you can compare blocks: same touches, same time window, same outcome labels. Change one thing per block. If you change the opener and the list at once, you will not know which one moved the result.
What you get
The audience in Step 1 was sized with leadocean_count_leads on 21 October 2026. Filters: jobLevel VP, jobFunction Sales & Business Development, country US, employeeRange 51-200.
{ "total": 18567, "totalIsExact": true, "totalAll": 60456, "distinctCompanies": 12192, "credits": 0 }That is 18,567 VPs with a mailable email (verified, catch_all_valid or catch_all) and 60,456 with no email condition. The 18,567 work at 12,192 companies. The same filters with hasPhone true and no email condition matched 49,715 people. Those two counts are different filters, so do not subtract one from the other.
One line of callsheet.jsonl looks like this, with placeholder values. The field names follow the response shape from the API reference.
{"name":"Jane Doe","role":"VP Sales","company":"Acme",
"past":[{"title":"Director, Partnerships","company_name":"Globex"}],
"phone":"+1 415 555 0133"}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Copy the key from app.leadocean.io and check $LEADOCEAN_API_KEY is exported |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Keep sleep 0.3 in the loop, or honour the Retry-After header |
| 402 | Records are spent | Check GET /v1/usage. Free is 1,000 records for life. See pricing |
| Empty results | A filter value is not an exact enum string | Look it up with GET /v1/enums, for example Sales & Business Development |
| 404 on enrich | We hold nothing for that person | Nothing was billed. Skip the row |
| Export refused | Over 50,000 rows a request or 500,000 a day | Narrow the filters. Bulk CSV export is not on Free |
FAQ
What does it mean to warm up a lead before cold calling?
It means the person has seen your name or a relevant message before the phone rings, and you open with a fact about them. Two light touches and a specific opener do most of the work.
How many records does the whole run cost?
Up to 35. One search page of 25 is 25 records. Ten enrichments add up to 10 more, and a person we hold nothing for is a 404 and costs nothing.
Do I need a sequencer or a CRM?
No. A spreadsheet and your own email account are enough for 25 people. If you want to automate sends later, see the best cold email warm-up tools.
Can I warm a list from social engagement instead?
Yes. People who comment on your posts are warmer than a list pulled by filter. See how to get warm leads from LinkedIn posts.
Where do the email templates come from?
Write your own around one fact per person. For a full email sequence, see generate leads with cold email.
How soon after the email should I call?
Call the same day, within a few hours, and say so in the email. The email gives the call a reason, and a fixed time in the message turns the call into a follow-through instead of an interruption. If nobody picks up, leave one short voicemail that repeats the fact and do not call again that day.
Build a call list your reps can open with a real fact
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →