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 an X (Twitter) Profile (2026)

Start with a handle and end with a work email you can check before you send. Manual way first, then one API call, then the bulk loop.

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

TL;DR

  • LeadOcean takes no X handle. You read the name and employer off the profile, find the person with a domain search, then ask for their work email by person_id.
  • One profile takes about 15 minutes the first time, mostly getting a key. It spends 2 records: 1 for the search row, 1 for the email lookup.
  • For a list, loop the same two calls over a CSV of handles you have already resolved to a name and a company domain. Free runs at 5 requests a second, Agency at 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 CSV with one handle, one name and one company domain per line.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"

Step 1: Read the profile by hand

Open the profile and write down three things: the display name, the employer, and the link in the bio. This is free and takes two minutes.

An X profile is a poor source for an email. Most bios carry none, so the job is to identify the person, not to scrape an address. The employer and the link in the bio give you a company domain, and the domain is what the search needs.

Pull the domain out of the bio link so you can pass it on cleanly.

bash
echo "https://www.acme.com/about?ref=x" | sed -E 's#https?://(www\.)?([^/?]+).*#\2#'
text
acme.com

If the bio link points to a personal site or a newsletter, find the employer's domain from the profile text instead. Do not guess it.

Step 2: Find the person by company domain

Search people at that domain with the name from the profile. The search body is flat JSON, and filters are arrays. q is free text over name, title and headline, and domain is the company domain.

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 '{
    "q": "Jane Doe",
    "domain": ["acme.com"],
    "limit": 5
  }' | jq -c '.data[] | {person_id: .meta.source_ids.person_id}'

Each row costs 1 record. Keep limit small so a wrong guess costs little. Check the title and headline against the X bio before you trust a match.

The person_id you need sits at meta.source_ids.person_id on the row. It is a string of digits. Copy it as returned.

You can size a search first for nothing. Send count=true as a query parameter with limit 1, and the response comes back with meta.countOnly true and no rows.

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 '{"q": "Jane Doe", "domain": ["acme.com"], "limit": 1}' | jq '.meta.total'

If the total is 0, stop. You have no match for that name at that domain, and you have spent nothing.

Step 3: 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=1234567890" | jq

It only returns addresses whose deliverability was checked (verified), whose mailbox was confirmed on a catch-all domain (catch_all_valid), or whose domain accepts everything (catch_all). So null means "none that qualify", not "this person has no email".

The call costs 1 record, found or not. The pricing page lists what each plan includes.

Step 4: Pull the full record when you need more

Use enrich when you want the profile, every email and the phones in one response. Enrich takes exactly one of linkedin_url, email, phone or person_id. Set reveal_email to true for the contact points.

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": "1234567890",
    "reveal_email": true
  }' | jq '.data.contact_data'

A person we hold nothing for is a 404 and costs nothing. A found person costs 1 record, and reveal_email adds no extra charge. Since October 2026 the same call also returns career history under resume_data: past roles, education and certifications.

Use /v2/people/email/work when you only want the address to send to. Use enrich when you want the record to store.

Step 5: Run it over a list

Put one line per person in handles.csv with no header: handle, full name, company domain. You resolved the name and domain by hand in Step 1. Loop the search, take the first row, then ask for the work email.

bash
while IFS=, read -r handle name domain; do
  pid=$(curl -s -X POST "https://api.leadocean.io/v1/people/search" \
    -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
    -d "{\"q\":\"$name\",\"domain\":[\"$domain\"],\"limit\":1}" \
    | jq -r '.data[0].meta.source_ids.person_id // empty')
  [ -z "$pid" ] && { echo "{\"handle\":\"$handle\",\"email\":null}"; continue; }
  curl -s -G "https://api.leadocean.io/v2/people/email/work" \
    -H "x-api-key: $LEADOCEAN_API_KEY" --data-urlencode "person_id=$pid" \
  | jq -c --arg h "$handle" '{handle: $h, email: .data.email, status: .data.status}'
  sleep 0.25
done < handles.csv > results.jsonl

Records add up: 500 handles that all match spend about 1,000 records. Run 20 first and read the hit rate. On Free the loop runs at 5 requests a second at most, so the sleep 0.25 keeps it under the limit.

Bulk CSV export is not on Free. Solo at $97 a month includes it, and Agency at $497 a month adds 200 requests a second. Export takes search filters, not handles, so use POST /v1/exports when you have an audience definition but no list yet. It writes up to 50,000 rows a request and 500,000 a day.

Step 6: Filter on status before you send

An address is not a send list until you have read its status. Keep verified and catch_all_valid. Treat catch_all as a risk you size, not a green light.

bash
jq -c 'select(.email != null and (.status == "verified" or .status == "catch_all_valid"))' \
  results.jsonl > sendable.jsonl
wc -l sendable.jsonl

LeadOcean gives no refund or credit-back for bounced emails, so this filter is your protection. The dataset is refreshed monthly, and each record carries its own fetched_at date.

What you get

Response shape from the API reference, with placeholder values. 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"
  }
}

The enrich call returns the contact points under data.contact_data. Phones come back as contact_phones[].phone.

json
{
  "success": true,
  "data": {
    "contact_data": {
      "has_email": true,
      "has_phone": true,
      "email_status": "verified",
      "email_type": "work",
      "contact_emails": [
        {
          "email": "jane.doe@acme.com",
          "type": "work",
          "status": "verified",
          "verified_batch_date": "2026-09-20"
        }
      ],
      "contact_phones": [
        { "phone": "+14155550133", "type": "mobile" }
      ]
    }
  }
}

Troubleshooting

ErrorCauseFix
401Missing, wrong or revoked keyRe-copy the key from app.leadocean.io and re-export $LEADOCEAN_API_KEY. The header name is x-api-key.
429More than your plan's rate limit on one key (5 a second on Free, 200 on Agency)Honor the Retry-After header, add a short sleep between calls, or run fewer workers.
402The free 1,000 records are spentCheck GET /v1/account, then move to Solo at $97 a month or Agency at $497 a month.
Search returns no rowsThe name and domain do not match one person we holdTry the name without a middle initial, or drop q and search the domain by title. No records are spent on an empty page.
data: null on the work email callNo checked address at the current employer qualifiesThe call still cost 1 record. Try enrich, or GET /v2/people/email/personal if a personal address suits your use.
400 on a search or exportA filter value outside the catalogue, or an export over the ceilingResolve enum values with leadocean_list_enum_values or GET /v1/enums. Exports stop at 50,000 rows a request and 500,000 a day.

FAQ

Can I find an email from an X handle alone?

Not with LeadOcean. The API takes a LinkedIn URL, an email, a phone or a person_id, not an X handle. Read the name and employer off the profile, then search by company domain.

Does a lookup that finds nothing cost a record?

On the v2 contact endpoints, yes: 1 record per call, including a call that finds nothing. On POST /v1/people/enrich, a person we hold nothing for is a 404 and costs nothing.

Is it free?

The first 1,000 records are free for life, with no card. At 2 records per profile, that is about 500 profiles.

What if I already have their LinkedIn URL?

Skip Steps 1 and 2. See how to find an email from a LinkedIn profile and send the URL straight to the work email endpoint.

Where else can I start from?

The same pattern works from other profiles. See how to find an email from GitHub and from Crunchbase.

Find the work email behind any X profile, free

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

Get your free API key →