API Docs Get API key
Developer Tutorial

How to Find an Email From a LinkedIn URL

A practical, copy-paste guide to turning a LinkedIn profile URL into a verified work email with one REST call — no LinkedIn login, no scraping, no ban risk. Every lookup is 1 credit, with no per-result multipliers.

You have a LinkedIn profile URL — maybe from a Sales Navigator list, a webinar sign-up, or a connection request — and you need the person's email to actually reach them. Copying it by hand, guessing [email protected], or risking your LinkedIn account with a browser scraper are all bad options. An API is the clean one.

This tutorial shows how to find an email from a LinkedIn URL with the LinkFinder AI API, end to end: authentication, your first lookup, handling profiles where no email exists, processing a whole CSV of URLs, surviving rate limits, and pushing the result into a CRM. Everything runs against a single endpoint — POST https://api.linkfinderai.com — and you choose the lookup with a type field. The relevant type here is linkedin_profile_to_email. One request costs one credit, including lookups that come back empty, so the cost is easy to reason about up front.

Examples are shown in cURL, Python, and Node.js. You only need the free tier (100 credits) to follow along.

1

Get your API key

Create a free account and grab your key from the dashboard under Settings → API Key. The free trial includes 100 credits, which is plenty to test the full email-lookup flow below.

Treat the key like a password. Set it as an environment variable rather than hardcoding it, and never ship it in client-side JavaScript or commit it to a repo.

# Add to your shell profile or a .env file (never commit it)
export LINKFINDER_API_KEY="lf_live_your_key_here"
import os
API_KEY = os.environ["LINKFINDER_API_KEY"]
BASE_URL = "https://api.linkfinderai.com"
const API_KEY = process.env.LINKFINDER_API_KEY;
const BASE_URL = "https://api.linkfinderai.com";
2

Make your first email lookup call

Every request is a POST to the same URL with two fields: type set to linkedin_profile_to_email, and input_data set to the LinkedIn profile URL. Pass your key in the Authorization: Bearer header. That's the entire request.

curl -X POST "https://api.linkfinderai.com" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer $LINKFINDER_API_KEY" \
     -d '{
       "type": "linkedin_profile_to_email",
       "input_data": "https://linkedin.com/in/sarah-mitchell-sales"
     }'
import os, requests

API_KEY = os.environ["LINKFINDER_API_KEY"]

resp = requests.post(
    "https://api.linkfinderai.com",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json={
        "type": "linkedin_profile_to_email",
        "input_data": "https://linkedin.com/in/sarah-mitchell-sales",
    },
)

data = resp.json()
print(data["status"], data["result"])  # success  [email protected]
const API_KEY = process.env.LINKFINDER_API_KEY;

const resp = await fetch("https://api.linkfinderai.com", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    type: "linkedin_profile_to_email",
    input_data: "https://linkedin.com/in/sarah-mitchell-sales",
  }),
});

const data = await resp.json();
console.log(data.status, data.result); // success  [email protected]
Only have a name and company, not a URL? Resolve the profile first with lead_full_name_to_linkedin_url (name + company), then feed that URL into linkedin_profile_to_email. Each of those is its own 1-credit call.
3

Read the response shape

The email lookup returns a small, predictable object. Always branch on status first — a 200 response can still carry status: "error" with a null result when no email could be matched to that profile.

{
  "result": "[email protected]",
  "status": "success"
}
{
  "result": null,
  "status": "error",
  "message": "Email not found"
}

If you want more than the address — name, title, company, location — pair the lookup with linkedin_profile_to_linkedin_info on the same URL. That returns the full profile so you can store a complete contact record alongside the email:

{
  "full_name": "Sarah Mitchell",
  "job_title": "VP of Sales",
  "company_name": "CloudCore",
  "company_domain": "cloudcore.io",
  "industry": "Software",
  "city": "Austin",
  "state": "Texas",
  "country": "United States"
}
A lookup that returns null still costs 1 credit. De-duplicate your list of URLs and cache results so you never pay twice for the same profile.
4

Handle the not-found case

Not every profile has a discoverable email, so a robust lookup wraps the call and decides what to do when result is null. A clean pattern: try the direct email lookup, and if it comes back empty, fall back to resolving the company website so you at least keep a usable domain on the record.

import os, requests

API_KEY = os.environ["LINKFINDER_API_KEY"]
BASE_URL = "https://api.linkfinderai.com"

def call(enrichment_type, input_data, **extra):
    """One request = one credit. Returns result or None."""
    resp = requests.post(
        BASE_URL,
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={"type": enrichment_type, "input_data": input_data, **extra},
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    return data.get("result") if data.get("status") == "success" else None

def email_from_linkedin(profile_url):
    record = {"linkedin": profile_url, "email": None, "company_domain": None}

    # 1) Direct: LinkedIn URL -> email
    email = call("linkedin_profile_to_email", profile_url)
    if email:
        record["email"] = email
        return record

    # 2) Fallback: keep the company domain for manual follow-up
    info = call("linkedin_profile_to_linkedin_info", profile_url)
    if info and info.get("company_name"):
        record["company_domain"] = call("company_name_to_website", info["company_name"])

    return record

print(email_from_linkedin("https://linkedin.com/in/sarah-mitchell-sales"))
const API_KEY = process.env.LINKFINDER_API_KEY;
const BASE_URL = "https://api.linkfinderai.com";

async function call(type, input_data, extra = {}) {
  const resp = await fetch(BASE_URL, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ type, input_data, ...extra }),
  });
  const data = await resp.json();
  return data.status === "success" ? data.result : null;
}

async function emailFromLinkedin(profileUrl) {
  const record = { linkedin: profileUrl, email: null, companyDomain: null };

  const email = await call("linkedin_profile_to_email", profileUrl);
  if (email) { record.email = email; return record; }

  const info = await call("linkedin_profile_to_linkedin_info", profileUrl);
  if (info && info.company_name) {
    record.companyDomain = await call("company_name_to_website", info.company_name);
  }
  return record;
}

console.log(await emailFromLinkedin("https://linkedin.com/in/sarah-mitchell-sales"));
The fallback only fires when the direct lookup misses, so most profiles cost a single credit. The extra calls only happen on the minority that need them.
5

Process a CSV of LinkedIn URLs

The single-profile function scales straight to a list. Read a CSV of LinkedIn URLs, look up an email for each row, and write the result back out. The only thing to respect is your plan's requests-per-second limit, so add a small delay between calls.

import csv, time

INPUT = "profiles.csv"        # column: linkedin
OUTPUT = "profiles_with_email.csv"
REQS_PER_SEC = 5              # match your plan (Starter = 5/s)
DELAY = 1.0 / REQS_PER_SEC

with open(INPUT, newline="") as f_in, open(OUTPUT, "w", newline="") as f_out:
    reader = csv.DictReader(f_in)
    fields = ["linkedin", "email", "company_domain"]
    writer = csv.DictWriter(f_out, fieldnames=fields, extrasaction="ignore")
    writer.writeheader()

    for row in reader:
        record = email_from_linkedin(row["linkedin"])
        writer.writerow(record)
        print(f"{row['linkedin']} -> {record.get('email')}")
        time.sleep(DELAY)     # stay under the rate limit

print("Done.")
De-duplicate profiles.csv first and skip URLs you've already processed. Since empty lookups still cost a credit, a clean input list is the single biggest lever on your bill.
6

Handle errors & rate limits

Production lookups run unattended, so handle the failure cases explicitly. These are the status codes you'll actually hit:

CodeMeaningWhat to do
200SuccessCheck result — it may be null (still 1 credit)
401UnauthorizedMissing/invalid key — check the Authorization header
402Insufficient creditsTop up or wait for the next billing cycle
422Invalid requestVerify the type value and that input_data is a valid profile URL
429Rate limit exceededBack off exponentially: 1s, 2s, 4s
500Server errorRetry after ~30s; contact support if it persists

Wrap your request helper with retry-and-backoff so a burst of 429s or a transient 500 doesn't kill a long CSV run:

import time, requests

def call(enrichment_type, input_data, max_retries=4, **extra):
    delay = 1.0
    for attempt in range(max_retries):
        resp = requests.post(
            BASE_URL,
            headers={"Authorization": f"Bearer {API_KEY}",
                     "Content-Type": "application/json"},
            json={"type": enrichment_type, "input_data": input_data, **extra},
            timeout=30,
        )

        if resp.status_code in (429, 500):
            time.sleep(delay)       # 1s -> 2s -> 4s -> 8s
            delay *= 2
            continue

        if resp.status_code == 402:
            raise RuntimeError("Out of credits — top up to continue.")

        resp.raise_for_status()
        data = resp.json()
        return data.get("result") if data.get("status") == "success" else None

    raise RuntimeError("Exhausted retries after repeated rate limiting.")
PlanCredits / moRequests / secBatch size
Starter5,0005 req/sUp to 500
Professional20,00010 req/sUp to 500
Enterprise50,00020 req/sUp to 500
HyperGrowth250,00050 req/sUp to 500
7

Verify & sync to your CRM

Once you have an email, the last step is making it usable and getting it where your team works.

Option A — Direct from code

Post the result straight to your CRM's API right after the lookup. Map email, linkedin, and any profile fields onto your contact record and upsert. Before a first send, run the address through your email-sending platform's verification or a syntax/MX check to protect your domain reputation.

Option B — No-code via Zapier or Make

If you'd rather not maintain a per-CRM integration, call the endpoint from a generic HTTP step. One Zapier connection then fans out to HubSpot, Salesforce, Pipedrive, Airtable, Google Sheets, and thousands of other apps.

# Zapier: "Webhooks by Zapier" -> POST action
URL:     https://api.linkfinderai.com
Method:  POST
Headers: Authorization: Bearer YOUR_API_KEY
         Content-Type: application/json
Body:    {"type": "linkedin_profile_to_email",
          "input_data": "{{linkedin_url}}"}
# Make: HTTP -> "Make a request" module
URL:     https://api.linkfinderai.com
Method:  POST
Headers: Authorization: Bearer YOUR_API_KEY
Body:    JSON -> {"type": "linkedin_profile_to_email",
                 "input_data": "{{linkedin_url}}"}
Re-check on a schedule. People change jobs, so an email that resolved six months ago may now bounce. A periodic re-run of your highest-value contacts keeps the CRM clean and your deliverability healthy.

Find emails from LinkedIn URLs in 2 minutes

Spin up the flow above on the free tier — 100 credits, an API on every plan, and flat 1-credit-per-request pricing with no annual contract.

Get your API key

No credit card required • API on every plan • Flat pricing • Cancel anytime