You can find a work email from a signature by using the identifiers in it, not the text itself. A name, a title, a company domain, a LinkedIn URL or a phone number each map to one person. LeadOcean returns one checked work address for that person, or null.
TL;DR
- Pull the strongest identifier from the signature: LinkedIn URL first, then phone, then name plus company domain.
- One lookup takes about 10 minutes by hand, or one API call. It spends 1 LeadOcean record, or up to 4 when you have to search by name first.
- The bulk script at the end runs a CSV of signatures under your plan's rate (5 requests a second on Free), one record per row.
Prerequisites
- A LeadOcean API key from app.leadocean.io. Free gives you 1,000 records, one-off, no card.
- The key in your shell:
export LEADOCEAN_API_KEY=your_key_here curlfor the single lookups and Python 3 withrequestsfor the bulk script.- The signature text, or a forwarded thread, a PDF or a screenshot you can copy it from.
Why you would need this at all: the sender used a personal address, the signature belongs to a colleague who was only cc'd, or the thread was forwarded without headers. In each case the signature has more about the person than the address bar does.
Step 1: Pull the identifiers out of the signature
Take the strongest identifier, because it decides the cost and the odds. A LinkedIn URL is the strongest, a direct phone number next, and name plus company domain last.
A typical signature:
Jane Doe
Head of Revenue Operations
Acme Inc. | acme.com
+1 415 555 0133
linkedin.com/in/janedoe-exampleCopy these into a note: linkedin_url, phone, full_name, title and domain. Take the domain from the website line, not from the sender address, because the sender may be on Gmail.
If the signature is pasted text, this snippet pulls the first three for you.
import re
sig = open("signature.txt").read()
linkedin = re.search(r"linkedin\.com/in/[\w\-%]+", sig)
phone = re.search(r"\+?\d[\d\s().\-]{8,}\d", sig)
domain = re.search(r"\b([a-z0-9\-]+\.(?:com|io|co|net|org|ai))\b", sig, re.I)
print("linkedin_url:", "https://www." + linkedin.group(0) if linkedin else None)
print("phone:", phone.group(0).strip() if phone else None)
print("domain:", domain.group(1).lower() if domain else None)Step 2: The manual way, and where it stops
Guessing is free but slow, and you cannot confirm a guess on many domains. Use it only for one or two people.
- Open the company site and look for any published address. Copy its pattern (
first.last@,flast@,first@). - Build candidates from the name and domain.
- Send a one-line check from a throwaway mailbox, or run an address checker.
first=jane; last=doe; domain=acme.com
for p in "$first.$last" "$first$last" "${first:0:1}$last" "$first" "${first:0:1}.$last"; do
echo "$p@$domain"
doneThis breaks on catch-all domains, which accept every address, so a clean check proves nothing. It also breaks when the person has changed jobs and the signature is stale. Checking each guess by hand costs more time than the lookup in Step 3.
Step 3: One lookup by LinkedIn URL
Send the LinkedIn URL to the work email endpoint. It returns the one current, checked address at the person's present employer, or data: null. It costs 1 record per call.
curl -s "https://api.leadocean.io/v2/people/email/work?linkedin_url=https://www.linkedin.com/in/janedoe-example" \
-H "x-api-key: $LEADOCEAN_API_KEY"Read status before you send anything. verified means the address passed a deliverability check. verified_batch_date names our run, and null there means the check came from a source we hold to the same standard. catch_all_valid means we confirmed the mailbox on a catch-all domain. catch_all means the domain accepts everything and we could not confirm the mailbox. There is no refund for bounces, so treat catch_all as a risk you choose.
Endpoint reference: find work email
Step 4: No LinkedIn URL? Search by name and domain
There is no name-plus-domain lookup. Search the company domain with the name as free text, then pass the person_id from the best row to the work email endpoint.
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": 3}'Search returns thin rows with no contact points. Each person returned counts as one record, so keep limit at 3 or less. Then take meta.source_ids.person_id from the matching row, check the title against the signature, and look it up.
curl -s "https://api.leadocean.io/v2/people/email/work?person_id=PERSON_ID_FROM_SEARCH" \
-H "x-api-key: $LEADOCEAN_API_KEY"If the signature only has a phone number, enrich by phone instead. Send exactly one identifier. reveal_email returns the addresses we hold at no extra charge.
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone": "+1 415 555 0133", "reveal_email": true}'To go the other way, from an address you already have to the person behind it, use GET /v2/people/reverse?email=.
Step 5: Run a whole inbox of signatures in bulk
Put one identifier per row in a CSV, then loop. The script below reads signatures.csv with a linkedin_url column and writes the result. Every call costs one record, including a call that finds nothing.
import csv, os, time, requests
KEY = os.environ["LEADOCEAN_API_KEY"]
URL = "https://api.leadocean.io/v2/people/email/work"
with open("signatures.csv") as f, open("emails.csv", "w", newline="") as out:
w = csv.writer(out)
w.writerow(["linkedin_url", "email", "status"])
for row in csv.DictReader(f):
r = requests.get(URL, params={"linkedin_url": row["linkedin_url"]},
headers={"x-api-key": KEY}, timeout=30)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 1)))
r = requests.get(URL, params={"linkedin_url": row["linkedin_url"]},
headers={"x-api-key": KEY}, timeout=30)
time.sleep(0.2) # 5 requests a second, the Free plan rate
if r.status_code == 404:
w.writerow([row["linkedin_url"], "", "no_record"])
continue
r.raise_for_status()
d = r.json()["data"] or {}
w.writerow([row["linkedin_url"], d.get("email", ""), d.get("status", "none")])The time.sleep(0.2) keeps the loop at the Free rate of 5 requests a second. Solo allows 50 and Agency 200, so shorten it on a paid plan. Before you send anything, filter emails.csv to verified and catch_all_valid. Rows with none or no_record are people we hold nothing usable for. Do not retry them.
If you want every person at one company rather than named people, use GET /v1/companies/{domain}/people. It stops at 10,000 rows. For a filtered list of up to 50,000 rows in a request, use POST /v1/exports or the Exports page in the app. Bulk CSV export is not on Free: it starts on Solo at $97 a month.
What you get
The work email endpoint answers with success and data and no meta. This is the response shape from the API reference, with placeholder values:
{
"success": true,
"data": {
"email": "jane.doe@acme.com",
"status": "verified",
"verified_batch_date": "2026-09-20"
}
}When no address qualifies, you get {"success": true, "data": null}. That means "none that qualify", not "this person has no email". A former employer's address is never returned here.
Before you spend records on a bulk run, size the audience for free. This leadocean_count_leads call ran on 2026-10-21 with jobLevel VP, jobFunction Sales & Business Development and country US. The count is mailable by default (verified, catch_all_valid and catch_all):
{
"total": 129494,
"totalIsExact": true,
"totalAll": 350517,
"credits": 0,
"defaults": "Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)",
"unsupportedFilters": []
}credits: 0 confirms the count is free. totalAll is the same filters with no email condition, so 129,494 of 350,517 US sales VPs have a mailable address in our data.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing, wrong or revoked key | Check x-api-key and that $LEADOCEAN_API_KEY is set in this shell |
| 429 | Faster than your plan's rate (5 a second on Free, 50 on Solo, 200 on Agency), or a fair-use window reached on a paid plan | Wait for the Retry-After header, then retry |
| 402 | The 1,000 free records are spent | Free records do not reset. Move to Solo ($97 a month) or Agency ($497 a month) on pricing |
| 404 | We hold no record for that person | Search by name and domain (Step 4), or try another identifier |
data: null | A real person, but no checked current work address | Do not guess one. Try the personal email endpoint or a phone lookup |
| 400 on export | Over the 50,000 rows per request ceiling, or a filter that cannot be applied | Split the request. Check enum values with GET /v1/enums |
FAQ
Can you find an email address from a signature alone?
Yes, if the signature carries a LinkedIn URL, a phone number, or a name and company. The signature text is not looked up directly. The identifiers in it are.
How many records does it cost?
One record for a LinkedIn lookup, whether or not it finds an address. Search by name first and add one record per row returned, so up to 3 more with limit 3.
Is the address safe to send to?
Send to verified and catch_all_valid. catch_all is a risk you choose, and bounced emails are not refunded, so read status first.
What if the person left the company in the signature?
The work endpoint returns the address at their current employer, not a former one. A stale signature still resolves through the LinkedIn URL or phone.
Does it work with Claude or an agent?
Yes. The hosted MCP server at https://api.leadocean.io/mcp signs in with OAuth and has 13 tools, free people and company counts included. See the find work email skill.
Related: find an email from Twitter, all how-to guides, pricing.
Turn your next signature into a verified email
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →