TL;DR
- A GitHub profile gives you a name, a company and sometimes a public email. When the email is missing, take the name and company to LeadOcean and ask for the checked work email. Two calls.
- It takes about 15 minutes the first time, mostly getting a key. Each person costs about 2 records of your 1,000 free ones: 1 for the search row, 1 for the email.
- For a list, loop the same two calls over a file of names. 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,jqandgit. For the bulk step, a file with onename,companypair per line.
export LEADOCEAN_API_KEY="paste-your-key-here"One rule first. GitHub's acceptable use policy bars using information from the service for spamming, including unsolicited email to users (GitHub policy, September 2026). Write to people you have a real reason to write to, and keep it short and relevant.
Step 1: Read the public profile
Start with the profile itself. GitHub's users endpoint returns the public fields, including name, company and email. It needs no key for a few calls (GitHub REST docs, September 2026).
curl -s https://api.github.com/users/octocat | jq '{login, name, company, email, blog}'If email holds an address, you are done with the manual route. Most profiles show null, because the owner chose not to publish one. That is their choice, so treat a blank field as a no.
Step 2: Check the commit log
Commits carry an author email. Clone one of the person's public repos and read it from the log.
git clone --quiet https://github.com/octocat/Hello-World.git
cd Hello-World
git log --format='%an <%ae>' | sort | uniq -c | sort -rn | headMany people hide this address. GitHub lets an account keep its email private and author web commits with a no-reply address instead (GitHub docs, September 2026). Skip anything with noreply in it. It forwards nowhere useful.
A commit address can also be old. It may point to a former employer or a personal inbox. Do not send to it without a second source.
Step 3: Find the person in LeadOcean
You now have a name and, usually, a company. Search on both. The body is flat JSON and every filter is an array. Each person returned costs 1 record, so keep limit small.
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",
"companyName": ["Acme"],
"limit": 3
}' | jq -c '.data[] | {person_id: .meta.source_ids.person_id}'Read the rows before you pick one. Check the title and company against the GitHub profile. If the profile lists a company website, filter on domain instead of companyName: a domain is an exact key, a name is not. If the name is common, add a title filter such as ["engineer"] or a country.
To size a role before you spend anything, send count=true as a query parameter with limit 1. On 21 October 2026, the free leadocean_count_leads MCP tool counted 26,970 mailable people with the title "Staff Engineer" in the US. Mailable here means a verified, catch-all valid or catch-all address, the tool's default.
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.
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" | jqOnly 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
Put one name,company pair per line in people.csv. Loop both calls and write one JSON line per person. The loop takes the first search row, so read a few results before you trust it.
while IFS=, read -r name company; 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 "{\"q\": \"$name\", \"companyName\": [\"$company\"], \"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 name "$name" '{name: $name, email: .data.email, status: .data.status}'
sleep 0.25
done < people.csv > results.jsonlThe sleep keeps you under 5 requests a second on the free plan. Records add up fast: 500 people spend up to 1,000. Run 20 first and read the hit rate.
When you have an audience and no list of names, skip the loop. POST /v1/exports takes search filters and writes a 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).
Then filter on status before you send. Keep verified and catch_all_valid. Treat catch_all as a risk you size, not a green light.
jq -c 'select(.email != null and (.status == "verified" or .status == "catch_all_valid"))' \
results.jsonl > sendable.jsonl
wc -l sendable.jsonlWhat you get
Both responses below are the response shape from the API reference. Every value is a placeholder, not a real record.
A search row carries the id you need at meta.source_ids.person_id. The profile fields sit under profile_data and are left out here.
{
"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.
{
"success": true,
"data": {
"email": "jane.doe@acme.com",
"status": "verified",
"verified_batch_date": "2026-09-20"
}
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing, wrong or revoked key | Re-copy the key from app.leadocean.io and re-export $LEADOCEAN_API_KEY. The header is x-api-key. |
| 429 | Above the plan's rate limit: 5 a second on Free, 200 on Agency | Wait for Retry-After, add a sleep, or move to a plan with a higher limit. |
| 402 | The free 1,000 records are spent | Check GET /v1/account, then see the pricing page. |
| Empty search results | A filter value that does not match, or a company name written differently | Drop companyName and search on q alone, then check enum values with leadocean_list_enum_values. |
data: null on the work email call | No checked address at the current employer | Accept the miss, or enrich by linkedin_url if you have it. |
| Export refused | More than 50,000 rows a request or 500,000 a day, or a Free plan | Split the export, or move to a plan with bulk CSV. |
FAQ
Can I get an email from a GitHub username alone?
Only if the profile shows one or a commit exposes one. Many accounts publish neither. Take the name and company to a data source instead.
Is a commit email safe to use?
Not by itself. It can be old, personal or a no-reply address. Use it as a lead, and confirm with a checked work address.
How many records does one lookup cost?
About 2: one search row and one email call. A 404 on enrich costs nothing. The work email call costs 1 record even when it finds nothing.
Is it allowed to email people I find on GitHub?
GitHub's policy bars using its data for spam or unsolicited bulk email. Send relevant, individual messages and follow the laws that apply to you.
Where do I find people by role instead?
Use find email from LinkedIn profile, find email from Twitter or find email from Crunchbase. All sit in the blog.
Turn a GitHub username into a checked work email
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →