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

How-to guide

How to Find an Email From Job Postings (2026)

Start with a job posting and end with a work email for the person who owns that hire. Manual way first, then the API, then the bulk way.

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

TL;DR

  • A job posting names the company, the team and the role, and rarely a person. Read those three, then ask LeadOcean for the person who likely owns the hire and their checked work email.
  • It takes about 15 minutes the first time, mostly getting a key. Each posting costs about 4 of your 1,000 free records: 3 for the search rows, 1 for the email.
  • For a list of postings, collect the company domains and export the matching managers in one request. The free plan allows 5 requests a second. Agency allows 200.

Prerequisites

  • A LeadOcean account and key from app.leadocean.io. The free plan has no card and 1,000 records, one-off. Free keys get both the search and enrich scopes.
  • The key in your shell as $LEADOCEAN_API_KEY.
  • curl and jq. For the bulk step, a file with one company domain per line.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"

A posting is a public signal, not an invitation to email. Write to the person with a reason that ties to the role, and keep it short.

Step 1: Read the posting for three things

Pull the company, the team and the seniority out of the posting. The page text is where they live. Job pages also carry structured data: Google's job posting markup has a hiringOrganization field with the employer's name and sameAs URL (Google Search Central, September 2026). That URL gives you the domain.

bash
curl -s "https://jobs.acme.com/senior-sdr" \
  | grep -o '"hiringOrganization"[^}]*}' | head -1

Not every board publishes the markup, so fall back to reading the page. Write down three values:

text
company domain:  acme.com
team:            Sales
posting level:   Senior SDR  ->  the owner is usually one level up: Manager or Director

The hiring manager is rarely the person who posted. For a Senior SDR, look for a Sales Manager or Director. For an engineering role, an Engineering Manager. One level above the role is a good default.

Step 2: Size the owners for free

Ask how many people hold that kind of seat before you spend a record. A search with count=true as a query parameter and limit at 1 is a free sizing call. Send the domain as an array.

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 '{
    "domain": ["acme.com"],
    "jobFunction": ["Sales & Business Development"],
    "jobLevel": ["Manager", "Director", "VP"],
    "limit": 1
  }' | jq '.meta'

A zero means we hold nobody at that level there. Stop and save the records. A small number such as 3 or 5 means you can read every row.

Step 3: Search the team and pick the owner

Run the same filters without count. Each person returned costs 1 record, so keep limit at 3.

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 '{
    "domain": ["acme.com"],
    "jobFunction": ["Sales & Business Development"],
    "jobLevel": ["Manager", "Director", "VP"],
    "limit": 3
  }' | jq -c '.data[] | {person_id: .meta.source_ids.person_id}'

Rows are thin: name, headline and current role. Read them before you pick. The posting's team name and office help here. A "Director, Sales Development" beats a "VP, Sales" for an SDR hire at a large company. At a 20-person company, the VP is the owner.

If the posting names a recruiter or hiring manager, search on q with that name and the domain instead.

Step 4: Get the work email

Send the person_id to the work email endpoint. It returns one address at the person's current employer, or data: null.

bash
curl -s -G "https://api.leadocean.io/v2/people/email/work" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  --data-urlencode "person_id=PERSON_ID_FROM_STEP_3" | jq

Only checked addresses come back: verified, catch_all_valid or catch_all. So null means "none that qualify", not "this person has no email". The call costs 1 record, found or not.

Do not guess jane@acme.com from a pattern. Guessed addresses bounce. LeadOcean gives no refund or credit-back for bounced emails, so read the status before you send.

Step 5: Run it over a list of postings

Put one company domain per line in domains.txt. Loop the search, take the first row per domain, and look up the email. The loop takes the top row, so read a few results before you trust it.

bash
while read -r domain; do
  id=$(curl -s -X POST "https://api.leadocean.io/v1/people/search" \
    -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
    -d "{\"domain\": [\"$domain\"], \"jobLevel\": [\"Director\", \"VP\"], \"limit\": 1}" \
    | jq -r '.data[0].meta.source_ids.person_id // empty')
  [ -z "$id" ] && continue
  curl -s -G "https://api.leadocean.io/v2/people/email/work" \
    -H "x-api-key: $LEADOCEAN_API_KEY" \
    --data-urlencode "person_id=$id" \
  | jq -c --arg d "$domain" '{domain: $d, email: .data.email, status: .data.status}'
  sleep 0.5
done < domains.txt > results.jsonl

Each pass makes 2 requests, so the half-second sleep keeps you under 5 requests a second on the free plan. 100 domains spend up to 200 records, so run 10 first and read the hit rate.

For more than a few hundred postings, export instead of looping. POST /v1/exports takes the same filters and writes one CSV, up to 50,000 rows a request and 500,000 a day. Bulk CSV export is not on the free plan. Solo at $97 a month includes it (pricing, October 2026).

bash
curl -s https://api.leadocean.io/v1/exports \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{
    "name": "Sales managers at hiring companies",
    "filters": {
      "domain": ["acme.com", "example.com", "example.org"],
      "jobLevel": ["Manager", "Director"],
      "emailStatus": ["verified", "catch_all_valid"]
    },
    "limit": 100,
    "preset": "full"
  }' | jq '.data | {id, status, requested, estimatedTotal}'

The export reserves up to your limit when it starts, and each row written costs one record. Poll GET /v1/exports/{id}, then fetch GET /v1/exports/{id}/download when the status is done. The app does the same on its Exports page, with a filter builder and the record price before you start.

What you get

The size check below is a real run. We ran it on 2026-10-21 with the free leadocean_count_leads MCP tool, which spends 0 records: VP-level people in Sales & Business Development in the US. Mailable here means a verified, catch-all valid or catch-all address.

json
{
  "total": 129494,
  "totalIsExact": true,
  "totalAll": 350517,
  "unsupportedFilters": []
}

Both responses below are the response shape from the API reference. Every value is a placeholder, not a real record.

json
{
  "success": true,
  "data": [
    {
      "profile_data": {},
      "meta": { "source_ids": { "person_id": "1234567890" } }
    }
  ],
  "meta": { "count": 1, "nextCursor": null }
}

The work email call returns success and data, with no meta object.

json
{
  "success": true,
  "data": {
    "email": "jane.doe@acme.com",
    "status": "verified",
    "verified_batch_date": "2026-09-20"
  }
}

Troubleshooting

ErrorCauseFix
401Missing, wrong or revoked keyRe-copy the key from app.leadocean.io and re-export $LEADOCEAN_API_KEY. The header is x-api-key.
429Above the plan's rate limit: 5 a second on Free, 200 on AgencyWait for Retry-After, add a sleep, or move to a plan with a higher limit.
402The free 1,000 records are spentCheck GET /v1/account, then see the pricing page. Paid plans slow down at their fair-use pace instead of answering 402.
Empty search resultsA filter value that does not match, such as jobLevel written in lowercaseEnum values are exact strings. Check them with leadocean_list_enum_values, or drop the level filter.
data: null on the work email callNo checked address at the current employerAccept the miss, or enrich by linkedin_url if you have it.
Export refusedMore than 50,000 rows a request or 500,000 a day, or a Free planSplit the export, or move to a plan with bulk CSV.

FAQ

Can I get an email from a job posting alone?

Only if the posting prints one, which is rare. Most route you to an application form. Take the company and team to a data source instead.

Should I email the recruiter or the hiring manager?

Pick by what you sell. Selling to the team that is hiring: the manager. Selling recruiting services: the recruiter or head of talent. Search the right jobFunction for each.

How many records does one posting cost?

About 4: three search rows and one email call. A 404 on enrich costs nothing. The work email call costs 1 record even when it finds nothing.

What if the company has no domain on the posting?

Search with q set to the company name and read the first rows for a match. Then use GET /v1/companies/acme.com/people once you know the domain.

Where do I find people by other signals?

Turn a job posting into a checked work email

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

Get your free API key →