DEV Community

Cover image for There is no LinkedIn email API. Here's what to use instead.
Erik Strömberg
Erik Strömberg

Posted on

There is no LinkedIn email API. Here's what to use instead.

You have a LinkedIn URL. You want the person's email. You search "linkedin email api" and find a maze of marketing pages and Stack Overflow threads from 2018 that all say roughly the same thing: there isn't one.

That's true. There is no first-party LinkedIn API that takes a profile URL and returns an email address. There never has been, and there isn't going to be. But there is a perfectly normal way to do this, and it's been quietly running under the hood of every CRM, sales tool, and recruiting platform for a decade. Here's what's actually going on.

What LinkedIn's APIs actually return

LinkedIn does have APIs. They are aggressively gated and aggressively scoped:

  • Marketing Developer Platform — ad campaign management, ad analytics, posting on behalf of pages. No contact data.
  • Talent Solutions API — for ATS integrations. Returns candidates who applied to your job, not arbitrary lookups.
  • Sales Navigator API — internal use by Sales Nav itself, not really a public developer surface.
  • Sign In With LinkedIn (OIDC) — returns the email of the signed-in user, with their consent, scoped to that session. You cannot use this to look up anyone else.
  • Profile API — restricted to LinkedIn-approved partners with formal agreements; even then, contact info isn't part of the schema for anyone other than the authenticated user.

Every LinkedIn API exposes data about the authenticated user (consent-driven) or paid customer activity within your own account. None of them lets you say "give me the email for linkedin.com/in/somebody."

Why? Because that's not what LinkedIn sells. LinkedIn's product is the platform — the network effect, InMail, recruiter seats. Letting third parties bypass that with a GET would directly compete with their own monetization.

What "LinkedIn email API" actually means

When developers search for it, they almost always mean this:

GET /lookup?linkedin_url=https://linkedin.com/in/somebody
→ { "email": "somebody@example.com", ... }
Enter fullscreen mode Exit fullscreen mode

That's not a LinkedIn endpoint. It's an enrichment endpoint, served by a third-party service that maintains its own contact database and uses the LinkedIn URL as a join key.

Aggregators have been building these databases for over a decade by combining:

  • Public-web signals — corporate websites, conference speaker bios, GitHub commits, press releases, paper authorships, podcast guest pages.
  • Contributed data — address books voluntarily uploaded by users of various email and CRM tools, contact graphs from sales acceleration platforms, opt-in business directories.
  • B2B partnerships — companies that have shared customer lists in business-data co-ops in exchange for access to the pooled data.
  • Verification feedback — every time a user marks an email as bounced or correct, the database learns.

None of this requires LinkedIn's permission, because none of it comes from LinkedIn. The LinkedIn URL is the lookup key, not the source.

The two LinkedIn identifiers worth knowing

Before you can hit any of these APIs, you need to extract a stable identifier from the LinkedIn URL the user gave you. There are two:

1. The public identifier (slug):

https://linkedin.com/in/williamhgates
                       ^^^^^^^^^^^^^^^
                       public_identifier
Enter fullscreen mode Exit fullscreen mode

Stable until the user customizes it. Most enrichment APIs accept this as the input.

2. The numeric LinkedIn ID:

Sometimes you'll see URLs like:

https://linkedin.com/in/ACoAAA-3B7U-_b0123abc/
Enter fullscreen mode Exit fullscreen mode

That long string is an opaque, account-scoped encoding of the user's internal numeric ID. It looks intimidating, but for lookup purposes it functions as just another stable identifier — useful when the slug isn't available (e.g. some recruiter or Sales Navigator URLs).

When parsing user-supplied LinkedIn URLs, handle both:

from urllib.parse import urlparse

def parse_linkedin_url(url):
    path = urlparse(url).path.strip("/")
    if not path.startswith("in/"):
        return None
    slug = path.split("/")[1]
    # ACoAA... = numeric URN, anything else = public identifier
    if slug.startswith("ACoAA"):
        return ("linkedin_id", slug)
    return ("linkedin_public_identifier", slug)

parse_linkedin_url("https://linkedin.com/in/williamhgates")
# → ("linkedin_public_identifier", "williamhgates")
Enter fullscreen mode Exit fullscreen mode

What the lookup actually does

Pseudocode for what a serious enrichment API runs when you hit it:

def lookup(identifier):
    # 1. Find every record across every source that matches this identifier
    matches = profiles.where(linkedin_public_identifier=identifier)
    if not matches:
        return None

    # 2. Expand: pull in any other records that share an email or
    #    secondary identifier with the initial matches.
    #    A person often has separate records from separate sources;
    #    this is how you merge them.
    matches = expand_by_shared_attributes(matches)

    # 3. Collect emails, phones, social handles
    emails = dedupe([e for m in matches for e in m.emails])
    return {
        "linkedin_public_identifier": identifier,
        "email_addresses": emails,
        "phone_numbers": [...],
        "github_login": ...,
        "twitter_username": ...,
    }
Enter fullscreen mode Exit fullscreen mode

Two non-obvious things going on here:

  • Identity resolution is the hard part. There's no canonical "person ID" across sources. You have to chain matches: this LinkedIn record shares an email with a CRM record, which shares a phone with a conference-speaker entry, etc. Done well, you end up with a merged view. Done badly, you accidentally fuse two different people who happen to share a generic info@ inbox.
  • Email classification matters more than people think. Anyone serious will separate personal addresses (@gmail.com, @protonmail.com) from work addresses. Outreach into a personal inbox has very different deliverability and acceptable-use implications than reaching someone at their employer.

A practical example

Here's enriching a CSV of LinkedIn URLs into emails, end to end:

import csv
import requests
from urllib.parse import urlparse

API = "https://peopledb.co/api/v1/people"
TOKEN = "YOUR_TOKEN"

def parse(url):
    path = urlparse(url).path.strip("/")
    if not path.startswith("in/"):
        return None
    slug = path.split("/")[1]
    return ("linkedin_id", slug) if slug.startswith("ACoAA") else ("linkedin_public_identifier", slug)

def lookup(url):
    parsed = parse(url)
    if not parsed:
        return None
    param, value = parsed
    r = requests.get(
        API,
        params={param: value},
        headers={"Authorization": f"Bearer {TOKEN}"},
    )
    return r.json() if r.ok else None

with open("input.csv") as f, open("output.csv", "w", newline="") as out:
    reader = csv.DictReader(f)
    writer = csv.DictWriter(out, fieldnames=["linkedin_url", "name", "work_email", "personal_email"])
    writer.writeheader()
    for row in reader:
        result = lookup(row["linkedin_url"]) or {}
        writer.writerow({
            "linkedin_url": row["linkedin_url"],
            "name": row.get("name", ""),
            "work_email":     (result.get("work_email_addresses") or [""])[0],
            "personal_email": (result.get("personal_email_addresses") or [""])[0],
        })
Enter fullscreen mode Exit fullscreen mode

That's the whole pipeline: parse, look up, write. The interesting work happens server-side; the client is twenty lines.

Things to keep in mind

A few things worth being honest about:

  • Coverage is not 100%. Aggregator APIs hit on a meaningful fraction of profiles — but never all of them. Plan for misses, not just hits.
  • Data ages. People change jobs, drop addresses, move to new domains. Anything you enrich today should be re-validated before you use it months later.
  • Verify before sending. A correctly-resolved email that bounces is still a bounce. SMTP-level validation (or a verification endpoint, if your provider has one) before bulk outreach is table stakes.
  • Compliance is on you. If you're enriching contacts at scale and reaching out from the EU or UK, GDPR's legitimate-interest tests apply. CCPA in California, CASL in Canada, similar regimes elsewhere. The API gives you data; what you do with it is your problem.
  • Don't scrape LinkedIn directly. Aside from the ToS issues, you'll be fighting CAPTCHAs and IP bans within minutes. The whole point of an enrichment API is that someone else has already absorbed that operational cost — across many sources, not just LinkedIn.

The shortcut

This is what PeopleDB does. The endpoint accepts either identifier:

# By public identifier (the slug)
curl "https://peopledb.co/api/v1/people?linkedin_public_identifier=williamhgates" \
  -H "Authorization: Bearer $PEOPLEDB_TOKEN"

# By numeric ID
curl "https://peopledb.co/api/v1/people?linkedin_id=ACoAAA-3B7U-_b0123abc" \
  -H "Authorization: Bearer $PEOPLEDB_TOKEN"
Enter fullscreen mode Exit fullscreen mode
{
  "linkedin_public_identifier": "williamhgates",
  "linkedin_id": "...",
  "github_login": null,
  "email_addresses": ["..."],
  "personal_email_addresses": ["..."],
  "work_email_addresses": ["..."],
  "phone_numbers": ["..."]
}
Enter fullscreen mode Exit fullscreen mode

The same identity-resolution layer also accepts github_login and github_id, so if a person shows up under both LinkedIn and GitHub in the index, you get the union — useful when you've got a contributor's GitHub handle but want to reach them at their work address.

There is no first-party LinkedIn email API. There probably never will be. What there is, is a category of enrichment APIs that have been quietly solving this for years. That's the trade.

Top comments (0)