Rust CLI and API library for LinkedIn, reverse-engineered from the Android app (com.linkedin.android). Provides programmatic access to LinkedIn's core features: profiles, messaging, feed, connections, search, and notifications.
This project is for personal and educational use only.
The CLI (linkedin-cli) exposes 22 subcommands across 7 domains:
| Command | Description |
|---|---|
auth login |
Authenticate with a li_at cookie (from browser or env var) |
auth status |
Check session validity (live API call or local-only) |
auth logout |
Clear stored session |
profile me |
Fetch your own profile |
profile view <id> |
View a profile by public identifier (vanity URL slug) |
profile visit <id> |
Visit a profile (registers in "who viewed my profile") |
profile viewers |
Show who viewed your profile |
feed list |
List feed updates (paginated) |
feed react <urn> |
React to a post (LIKE, PRAISE, EMPATHY, etc.) |
feed unreact <urn> |
Remove a reaction from a post |
feed comment <urn> <text> |
Comment on a feed post |
feed post <text> |
Create a new text post (public or connections-only) |
connections list |
List your connections (paginated) |
connections invite <id> |
Send a connection request with optional message |
connections invitations |
List pending received invitations |
connections accept <id> |
Accept a pending invitation |
search people <keywords> |
Search for people by keywords |
search jobs <keywords> |
Search for jobs by keywords |
messages list |
List conversations (cursor-based pagination) |
messages read <id> |
Read messages in a conversation |
messages send <recipient> <text> |
Send a message to a connection |
notifications list |
List notification cards (paginated) |
All list commands support --count, --start (or --before for cursor pagination), and --json for raw JSON output. Write commands (comment, post) require --yes to skip the confirmation prompt.
- Rust (stable) — install via rustup
- just — task runner (
cargo install justor via your package manager) - On macOS: Xcode Command Line Tools (
xcode-select --install)
# Build the workspace
just build
# Run all checks (build + test + lint + format)
just e2e| Recipe | Description |
|---|---|
just build |
Build the Rust workspace |
just test |
Run all tests |
just lint |
Run clippy (warnings are errors) |
just fmt |
Format all code |
just fmt-check |
Check formatting without modifying |
just e2e |
Full gate: build, test, lint, format check |
just run <args> |
Run the CLI with arguments |
LinkedIn API access requires a li_at session cookie. This is a cookie-based auth approach -- no OAuth app registration needed.
- Log into linkedin.com in Chrome
- Open DevTools: F12 (or Cmd+Option+I on macOS)
- Go to Application tab > Cookies >
https://www.linkedin.com - Find the
li_atcookie and copy its value
# Pass directly
linkedin-cli auth login --li-at "AQEDAQx..."
# Or via environment variable
export LINKEDIN_LI_AT="AQEDAQx..."
linkedin-cli auth login
# Verify the session works
linkedin-cli auth statusThe session is stored locally at ~/.config/linkedin-cli/session.json.
# Your own profile
linkedin-cli profile me
# View someone's profile
linkedin-cli profile view john-doe-123
# Visit a profile (shows up in "who viewed")
linkedin-cli profile visit john-doe-123
# Who viewed your profile
linkedin-cli profile viewers# List recent feed items
linkedin-cli feed list --count 20
# Like a post
linkedin-cli feed react urn:li:activity:7312345678901234567
# Celebrate a post
linkedin-cli feed react urn:li:activity:7312345678901234567 --type CELEBRATION
# Remove a reaction
linkedin-cli feed unreact urn:li:activity:7312345678901234567
# Comment on a post
linkedin-cli feed comment urn:li:activity:7312345678901234567 "Great post!" --yes
# Create a post
linkedin-cli feed post "Hello LinkedIn!" --yes
linkedin-cli feed post "Only for my network" --visibility CONNECTIONS_ONLY --yes# List conversations
linkedin-cli messages list --count 20
# Read a conversation
linkedin-cli messages read 2-abc123
# Send a message
linkedin-cli messages send john-doe-123 "Hey, wanted to connect about..."# List connections
linkedin-cli connections list --count 50
# Send a connection request
linkedin-cli connections invite john-doe-123
linkedin-cli connections invite john-doe-123 --message "Met you at the conference"
# List pending invitations
linkedin-cli connections invitations
# Accept an invitation (get ID and secret from invitations --json)
linkedin-cli connections accept 7312345678901234567 --secret abc123# Search for people
linkedin-cli search people "rust developer" --count 20
# Search for jobs
linkedin-cli search jobs "senior backend engineer"# List notifications
linkedin-cli notifications list --count 20All commands support --json for machine-readable output:
linkedin-cli profile me --json | jq '.firstName'
linkedin-cli feed list --json --count 5 | jq '.elements[].text'The linkedin-api crate can be used as a standalone Rust library:
[dependencies]
linkedin-api = { path = "linkedin/linkedin-api" }It provides:
LinkedInClient-- HTTP client with cookie jar, auth header decoration, and CSRF handlingSession-- session management (load, save, validate)- Typed request/response models for all supported endpoints
- Rest.li protocol handling (headers, pagination, union unwrapping)
Key dependencies: reqwest (with cookies + JSON), serde, chrono, thiserror, tokio.
Authentication uses cookie-based sessions rather than OAuth2 app tokens:
- User provides a
li_atcookie extracted from a browser session - The client also requires a CSRF token (
JSESSIONIDcookie) which is echoed as thecsrf-tokenheader - Sessions are persisted locally and reused across CLI invocations
LinkedIn uses two API styles, both in active use:
- Rest.li 2.0 -- LinkedIn's custom REST framework. Requires
X-RestLi-Protocol-Version: 2.0.0andX-RestLi-Methodheaders. Responses wrap data inelementsarrays withpagingmetadata. - GraphQL (Voyager/Dash) -- Newer endpoints use GraphQL queries with hardcoded query IDs (
queryIdparameter). The app is progressively migrating from Rest.li to Dash.
Every request includes a set of headers that mimic the Android app: User-Agent, X-Li-Lang, X-Li-Track, Accept-Language, and the CSRF token header.
- TLS fingerprint mismatch: The library uses rustls, not Chrome/BoringSSL. LinkedIn may detect this difference. Switching to
boring-tls(BoringSSL bindings for reqwest) would improve fingerprint fidelity. Seere/tls_configuration.md. - Query ID brittleness: GraphQL (Dash) endpoints use hardcoded
queryIdvalues extracted from the APK. These may change with app updates, requiring re-extraction. - Write operations: Posting, commenting, messaging, and connection requests hit LinkedIn's live systems. These may trigger additional validation, CAPTCHA challenges, or rate limiting that read-only operations do not.
- Rate limiting: LinkedIn actively detects automated access. Excessive requests can lead to CAPTCHA challenges or account restrictions. No built-in rate limiter is provided -- callers should throttle themselves.
- No real-time messaging: The current implementation uses request/response only. LinkedIn's real-time messaging system (long-poll / SSE) is documented but not implemented.
- Tokens and credentials are stored in
secrets/which is gitignored. Never commit cookies, session files, or captured API responses. - PII scan before any push to remote. Captured responses often contain names, emails, profile URLs, and other personal data.
- Session files at
~/.config/linkedin-cli/session.jsoncontain yourli_atcookie. Protect this file as you would a password. - Never commit APK files, decompiled output, or raw API responses.
linkedin/
linkedin-api/ Rust library crate (client, auth, models, services)
linkedin-cli/ Rust binary crate (clap CLI)
re/ Reverse engineering documentation
secrets/ Tokens, captured responses, PII (gitignored)
Justfile Build recipes