The free reverse email search tool most developers want is a single request: email in, person out. LeadOcean's Free plan gives you 1,000 records, one-off, no card, so you can run this for real. This guide shows the query, the output and the cost of each call.
TL;DR
- One
GET /v2/people/reversecall returns the name, title, employer and LinkedIn URL behind an email address. - It takes about 10 minutes for one address. A loop over a list takes about 20.
- Each reverse call costs 1 record, including one that finds nobody. The 1,000 Free records cover 1,000 lookups.
Prerequisites
- A LeadOcean account on the Free plan: start free. Work email required, no card.
- An API key from the app, exported as
$LEADOCEAN_API_KEY. Free keys carry theenrichscope this endpoint needs. curlfor steps 1 to 3. Python 3 and therequestspackage for step 4.- An email address to test. Use one you have the right to look up.
export LEADOCEAN_API_KEY="paste-your-key-here"Step 1: Run the reverse lookup
Send one email to /v2/people/reverse with your key in the x-api-key header. Send exactly one of email or phone, never both.
curl -s -G "https://api.leadocean.io/v2/people/reverse" \
--data-urlencode "email=jane.doe@acme.com" \
-H "x-api-key: $LEADOCEAN_API_KEY"--data-urlencode matters. A plus tag such as jane+sales@acme.com breaks without it, because + reads as a space in a query string.
POST works too, with a JSON body of {"email": "jane.doe@acme.com"}. Use POST if your address list comes from a system that already speaks JSON.
Step 2: Read the response
The response is a thin identity block, not the full profile. This is the shape from the API reference, with placeholder values.
{
"success": true,
"data": {
"person_id": "1234567",
"person_group_id": "1234567",
"linkedin_url": "https://www.linkedin.com/in/jane-doe",
"full_name": "Jane Doe",
"job_title": "Finance Manager",
"company_name": "Acme",
"company_domain": "acme.com",
"query": { "email": "jane.doe@acme.com" },
"matched": { "email": "jane.doe@acme.com", "status": "verified",
"verified_batch_date": "2026-09-14", "type": "work" },
"matched_in_record": true
},
"meta": { "source": "leadocean", "schema": "leadocean.contact.reverse.v2",
"stability": "stable", "credits": 1 }
}Three fields decide whether you trust the answer:
matched.statusis the deliverability of the address you sent. Read it before you send anything.matched.typesays whether it is a work or a personal address.matched_in_recordistruewhen the record lists your exact address.falsemeans we matched the person on it upstream but the record does not carry it. It does not mean nobody was found.
Every identity field can be null, so a real match can arrive without a LinkedIn URL or a title.
Step 3: Pull the full profile only when it is worth it
The reverse call names the person. To get everything else, call enrich with the same email.
curl -s -G "https://api.leadocean.io/v1/people/enrich" \
--data-urlencode "email=jane.doe@acme.com" \
-d "reveal_email=true" \
-H "x-api-key: $LEADOCEAN_API_KEY"Enrich costs one record when it finds the person and nothing when it does not. reveal_email=true returns every email and phone we hold for the person, at no extra charge. Without it, you get the profile and flags (has_email, email_status, email_type) but not the addresses.
Run reverse first when you only need to know who someone is. Skip it and go straight to enrich when you want the profile anyway. Doing both on one address spends 2 records.
Step 4: Run it over a list
The docs describe no batch version of the reverse endpoint, so a list means one call per address. This script reads emails.txt (one address per line) and writes matches.csv.
import csv, os, sys, time, requests
KEY = os.environ["LEADOCEAN_API_KEY"]
URL = "https://api.leadocean.io/v2/people/reverse"
COLS = ["email", "full_name", "job_title", "company_domain",
"status", "type", "matched_in_record", "person_group_id"]
def lookup(email):
for _ in range(5):
r = requests.get(URL, params={"email": email},
headers={"x-api-key": KEY}, timeout=30)
if r.status_code == 429: # over your plan's rate (5 a second on Free)
time.sleep(int(r.headers.get("Retry-After", "1")))
elif r.status_code == 503: # store unreachable, never billed
time.sleep(5)
else:
return r
r.raise_for_status()
with open("emails.txt") as src, open("matches.csv", "w", newline="") as out:
w = csv.writer(out)
w.writerow(COLS)
for line in src:
email = line.strip()
if not email:
continue
r = lookup(email)
if r.status_code == 404: # nobody holds it, still 1 record
w.writerow([email] + [""] * (len(COLS) - 1))
continue
if r.status_code == 402:
sys.exit("Out of records. Free records do not reset.")
r.raise_for_status()
d = r.json()["data"]
m = d.get("matched") or {}
w.writerow([email, d.get("full_name"), d.get("job_title"),
d.get("company_domain"), m.get("status"), m.get("type"),
d.get("matched_in_record"), d.get("person_group_id")])Dedupe on person_group_id, not person_id. The same person can return under different person_id values when you reach them through different contact points. Two records with the same person_group_id are one person as far as our de-duplication knows.
Before a large run, read GET /v1/account. It is free and shows records used and remaining.
Test a sample first. The docs publish no match rate, so take 100 addresses from your list and count the rows that come back with a name. LeadOcean holds 693M people and 366M of them have an email (measured 2026-09-23), a little over half. An address outside that set finds nobody.
Each reverse call costs one record whatever it returns. A sparse match still counts, and so does a miss. If your list has many addresses we may not hold, run it through /v1/people/enrich instead: an enrich that finds nobody is a 404 and costs nothing.
Step 5: Ask an AI agent instead
If you work in Claude or another MCP client, skip the script. Connect https://api.leadocean.io/mcp (OAuth sign-in, no key pasted) and ask in plain language.
Who is jane.doe@acme.com? Give me her title, employer and LinkedIn URL.The agent calls leadocean_get_lead with email. That tool returns the full person record, so it behaves like the enrich call rather than the thin reverse block. It costs 1 record when it finds someone and nothing when it finds nobody.
What you get
The status values are the part worth knowing before you act on a match. This is the live output of the free leadocean_list_enum_values MCP tool for email_status, run on 2026-10-21, with the long description cut.
{
"name": "email_status",
"total": 13,
"values": ["verified", "catch_all_valid", "catch_all", "risky", "unknown",
"untested", "invalid", "role", "disposable", "spam_trap",
"abuse", "derived", "none"],
"labels": {
"verified": "Deliverable",
"catch_all_valid": "Catch-all, mailbox confirmed",
"catch_all": "Catch-all domain",
"risky": "Risky",
"unknown": "Unknown",
"untested": "Not yet checked",
"invalid": "Undeliverable",
"role": "Role address",
"disposable": "Disposable address",
"spam_trap": "Spam trap",
"abuse": "Abuse / complainer",
"derived": "Derived, not verified",
"none": "No email"
}
}Two rules follow from the tool's own description:
- On a business address,
verifiedmeans our own verifier sent to it and it accepted. - On a personal address such as Gmail, we do not verify the domain. A
verifiedthere means a supplier asserted it. Treat it as a lead to check, not a confirmed address. catch_all_validis a mailbox on a catch-all domain whose existence was confirmed. It ranks betweenverifiedandcatch_all.
The record-level response in step 2 is the response shape from the API reference. No real person is shown here.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing, wrong or revoked key | Check the x-api-key header and that $LEADOCEAN_API_KEY is set in this shell. |
| 400 | Validation failed, for example both email and phone sent, or neither | Send exactly one. The details field names the bad parameter. |
| 403 | The key lacks the enrich scope, or the account is suspended | Create a key with the enrich scope in the app. |
| 404 | Nobody holds this address | Not an error in your code. On the reverse endpoint it still costs 1 record. Check the spelling before you send. |
| 429 | Faster than your plan's rate per account: 5 a second on Free, 50 on Solo, 200 on Agency | Wait for Retry-After, then retry. A sequential loop rarely hits this. |
| 402 | Free plan: the 1,000 records are spent | They do not reset. Move to Solo ($97 a month) or Agency ($497 a month) to keep going. |
FAQ
Is there a truly free reverse email search tool?
Free here means 1,000 records, one-off, no card. Each person found spends one. After that, Solo is $97 a month and Agency $497 a month. Both run at a fair-use pace with no per-record price. See pricing.
Does it work on Gmail and other personal addresses?
The endpoint accepts any address and matched.type labels it. Personal addresses are the weak case: we do not verify consumer domains, so read matched.status with care. Our guide to reverse Gmail search covers that case.
Can I search by name or by social profile?
Not with this endpoint. It takes an email or a phone number. To go the other way, from a profile to an address, see find an email from an X profile or find an email from an email signature.
How does it compare with other free tiers?
FullEnrich lists a reverse email lookup at 1 credit and a free trial of 50 credits, no card (credit costs and pricing, September 2026). LeadOcean's Free plan is 1,000 records at 1 record per reverse call. Compare the unit: a credit and a record are not the same thing.
Can I do this without code?
Not for a reverse lookup. The Exports page at app.leadocean.io is a filter builder with a column picker, a price preview and a CSV download. The docs describe no way to upload a list of emails there. Use the API or an agent. Every input and output field is on the reverse email lookup page, and more guides are in the blog.
Look up your first email free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →