Skip to content

About

Read your own LinkedIn data from the CLI via the EU Digital Markets Act portability API

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

li — read your own LinkedIn data from the command line

LinkedIn closed its profile API in 2015 and never reopened it. But the Digital Markets Act obliges gatekeepers to give end users continuous, programmatic access to their own data — so LinkedIn had to open one anyway.

li is a small Bash client for that API. It reads your own LinkedIn data: profile, positions, skills, education, endorsements, groups, learning history, job-seeker preferences and more — 26 domains, full history, in about 45 seconds.

$ li whoami
Headline: Senior Software Engineer · 20 years of shipped systems, six domains
First Name: …

$ li snapshot SKILLS | jq -r '.[].Name' | head -3
TypeScript
Go (Programming Language)
Software Architecture

$ li all ~/my-linkedin
  POSITIONS                    12 records
  SKILLS                       26 records
  …

No scraping, no cookie stealing, no Terms of Service violation. This is the official, sanctioned route — it is just badly documented and gated behind two traps that cost an evening to find.

Who can use this

Members located in the EEA or Switzerland. LinkedIn determines this from the location on your profile. That is a DMA scope limitation, not a choice this tool makes — outside the EEA the only route is still the manual data export.

Requirements

bash, curl, jq. Nothing else.

Install

git clone https://github.com/bart1208/linkedin-portability
install -m755 linkedin-portability/li ~/.local/bin/li

Setup

Roughly fifteen minutes, and no OAuth flow to implement — LinkedIn hands you a token from a form.

1. Create a developer app

Go to linkedin.com/developers/apps → Create app.

⚠️ Trap 1 — the Company Page

LinkedIn requires every app to be attached to a Company Page, and for this product it must be LinkedIn's own default page: Member Data Portability (Member) Default Company.

Paste that name into the LinkedIn Page field and pick it from the autocomplete. Do not create your own page — the button offering to is right there, and it is the wrong move. With any other page the product never appears under Products, and the association cannot be changed after the app is created: the only fix is to throw the app away and make another.

The remaining fields are formalities. A privacy policy URL is optional; the logo is required and nobody will ever see it.

2. Request the product

Products tab → Request access on Member Data Portability API (Member) → accept the terms. Granted immediately, no human review. If it is not in the list, see Trap 1.

3. Generate a token

Docs and tools → OAuth Token Tools → Create token → select the app → tick r_dma_portability_self_serve → Request access token → log in → Allow.

⚠️ Trap 2 — your browser will probably break this

If you see:

Oops. We can't verify the authenticity of your request because the state parameter was modified.

your browser's built-in blocker is dropping the cookie holding the OAuth state. Confirmed in Brave (brave-browser#35322) and in Vivaldi. Firefox and Chrome work.

Two more things matter: be logged in to LinkedIn first — the login redirect is where the state is lost — and open the token page fresh, because the state is minted on page load and expires while a tab sits open.

The token lasts about a year. There is no refresh flow: when it expires, repeat step 3.

4. Give the token to li

mkdir -p ~/.config/li
printf '%s\n' 'AQV…' > ~/.config/li/token
chmod 600 ~/.config/li/token

Or export LINKEDIN_DMA_TOKEN. Or point LI_TOKEN_FILE wherever you like.

Usage

li snapshot [DOMAIN]     fetch a domain, all pages merged (no DOMAIN = every domain)
li domains               list the domains the API actually holds for you
li all [OUTDIR]          one <domain>.json per domain (default: ./linkedin-snapshot)
li changelog [SINCE_MS]  interaction events; rolling 28-day window only
li whoami                PROFILE domain, flattened — quick token check
li raw PATH              escape hatch, e.g. li raw 'memberSnapshotData?q=criteria'
li version

Progress output goes to stderr, data to stdout, so everything pipes cleanly.

The two endpoints, and which one to trust

This is the part the official documentation does not tell you.

Snapshot — complete, but not live

GET /rest/memberSnapshotData?q=criteria&domain=X returns your full history for a domain. 26 domains were available at time of writing:

AD_TARGETING  CAUSES_YOU_CARE_ABOUT  CERTIFICATIONS  COMPANY_FOLLOWS  CONNECTIONS
COURSES  EDUCATION  EMAIL_ADDRESSES  ENDORSEMENTS  Events  GROUPS  INBOX
INVITATIONS  JOB_SEEKER_PREFERENCES  LANGUAGES  LEARNING  PHONE_NUMBERS  POSITIONS
PROFILE  PROFILE_SUMMARY  PROJECTS  REGISTRATION  RICH_MEDIA  SKILLS
TRUSTED_GRAPH  WHATSAPP_NUMBERS

(Events really is spelled like that, alone among them.) li domains asks rather than trusting this list. GROUPS and TRUSTED_GRAPH are not in the manual export ZIP, so the API is a strict superset of it.

The snapshot is a periodically rebuilt archive, not a live read. It is built when you grant consent — which makes it look convincing at first, because everything up to that moment appears fresh. It is not. Profile edits made hours later were still absent from the snapshot minutes afterwards: headline unchanged, PROFILE_SUMMARY still empty, SKILLS byte-identical. The refresh cadence is undocumented; assume at least overnight.

Changelog — timely, but only 28 days

GET /rest/memberChangeLogs?q=memberAndApplication returns your activity as events, within seconds, and the payloads are rich: an edit to your headline arrives carrying the new headline text. It is the only real-time channel there is.

Two limits, both sharp:

  • Rolling 28-day window. Older events are gone.
  • Nothing before consent. LinkedIn starts archiving the moment you authorise the app.

Together those mean a gap in polling loses the only timely record that exists, permanently. If you care about the history, poll on a schedule.

Use the changelog to confirm a change landed; use the snapshot to read complete state. Reversing those will mislead you.

Other things that will bite you

  • Linkedin-Version: 202312 and nothing else. The portal advertises version monikers up to 2026-05, but those track product announcements, not this endpoint. Any other value fails with 426 NONEXISTENT_VERSION.
  • start is a page index, not a record offset.
  • paging.total is unreliable. It changed from 26 to 27 mid-scan, because some data comes from offline systems. The only trustworthy terminator is 404 No data found for this memberId.
  • The undomained query returns one domain per page, so enumerating domains means walking every page — and a single domain can span several (ENDORSEMENTS takes two).

li handles all four.

What this cannot do

There is no write API. The DMA mandates portability, not modification — no scope in any LinkedIn programme will change your headline, a position, or a skill. If you want to update your profile, you do it by hand. Don't go looking; it isn't there.

Privacy

Some domains contain other people's personal data — CONNECTIONS carries names, headlines and dates for everyone you are connected to; INBOX carries both sides of every private conversation; ENDORSEMENTS carries endorser names and profile URLs. Those people did not consent to being committed to your git repository, uploaded anywhere, or fed to anything.

li all writes whatever the API returns. Deciding what to keep is on you. A reasonable default is to keep only the domains that describe you, and to reduce ENDORSEMENTS to per-skill counts.

Your token is a bearer credential for your entire LinkedIn history. Treat it like a password: chmod 600, never in a repo, never in a shell history.

References

Licence

MIT. See LICENSE.

About

Read your own LinkedIn data from the CLI via the EU Digital Markets Act portability API

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages