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.
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.
bash, curl, jq. Nothing else.
git clone https://github.com/bart1208/linkedin-portability
install -m755 linkedin-portability/li ~/.local/bin/liRoughly fifteen minutes, and no OAuth flow to implement — LinkedIn hands you a token from a form.
Go to linkedin.com/developers/apps → Create app.
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.
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.
Docs and tools → OAuth Token Tools → Create token → select the app → tick
r_dma_portability_self_serve → Request access token → log in → Allow.
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
stateis 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.
mkdir -p ~/.config/li
printf '%s\n' 'AQV…' > ~/.config/li/token
chmod 600 ~/.config/li/tokenOr export LINKEDIN_DMA_TOKEN. Or point LI_TOKEN_FILE wherever you like.
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.
This is the part the official documentation does not tell you.
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.
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.
Linkedin-Version: 202312and nothing else. The portal advertises version monikers up to2026-05, but those track product announcements, not this endpoint. Any other value fails with426 NONEXISTENT_VERSION.startis a page index, not a record offset.paging.totalis unreliable. It changed from 26 to 27 mid-scan, because some data comes from offline systems. The only trustworthy terminator is404 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 (
ENDORSEMENTStakes two).
li handles all four.
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.
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.
- Member Data Portability (Member) — official setup docs
- Member Snapshot API
- LinkedIn DMA Portability API Terms
- End user data portability — European Commission
MIT. See LICENSE.