LinkedIn client: OAuth, profile and posts
LinkedIn is the LinkedIn API client in the Socialift Python SDK. It connects a member through 3-legged OAuth and OpenID Connect (openid, profile, email), exchanges, introspects and — where allowed — refreshes tokens, reads the profile from userinfo, and creates, lists and deletes text or link posts on the member's behalf via the Posts API. LinkedIn private messages and Lead Gen Forms are not part of this client.
from socialift import LinkedIn10 methodsas of socialift 1.3.0Updated:
How do I set up the LinkedIn client?
Create the client with LinkedIn(client_id=..., client_secret=...). LinkedIn.from_env() reads LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET and LINKEDIN_API_VERSION. The Client ID and Client Secret are on your app's Auth tab in the LinkedIn Developer Portal, where you also add the redirect URL — see the "LinkedIn app setup" guide.
Available scopes depend on the products added to the app: Sign In with LinkedIn using OpenID Connect gives openid, profile, email; Share on LinkedIn gives w_member_social. The SDK requests these four by default. Reading a member's posts needs r_member_social, a restricted permission LinkedIn grants to approved users only.
The Posts API (/rest/*) requires a LinkedIn-Version: YYYYMM header on every call; the SDK takes it from api_version. LinkedIn supports each monthly version for at least one year and answers sunset versions with HTTP 426, so keep api_version or LINKEDIN_API_VERSION on an active version.
import asyncio
import os
from socialift import LinkedIn
async def main():
# 1) Explicit arguments
li = LinkedIn(
client_id=os.environ["LINKEDIN_CLIENT_ID"],
client_secret=os.environ["LINKEDIN_CLIENT_SECRET"],
api_version="202609", # an active LinkedIn-Version (YYYYMM)
)
# 2) Or from the environment: LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET,
# LINKEDIN_API_VERSION
# li = LinkedIn.from_env()
async with li:
me = await li.me(os.environ["LINKEDIN_TOKEN"]) # from exchange_code()
print(me.id, me.full_name, li.person_urn(me.id))
asyncio.run(main())
Client and sign-in
Creating the client, the authorization URL and exchanging code for a token.
exchange_codefrom_envlogin_url
exchange_code
asyncExchanges the callback code for an access token at /oauth/v2/accessToken (form-urlencoded) and returns a TokenBundle.
await li.exchange_code(code: str, *, redirect_uri: str) -> socialift.models.TokenBundle- LinkedIn rule: the
codeis valid for 30 minutes, andredirect_urimust match the authorization request. - Access tokens last 60 days;
refresh_tokenis only returned if programmatic refresh is enabled for your app. - Raises
ApiErrorif the response has noaccess_token.
from_env
classmethodCreates a client from the LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET and LINKEDIN_API_VERSION environment variables so keys stay out of code.
LinkedIn.from_env(**kwargs) -> LinkedIn- Empty values are not validated by the constructor: a missing
client_idsurfaces asConfigurationErrorinlogin_url. - Without
LINKEDIN_API_VERSIONthe SDK default version is used — check that it is still active at LinkedIn.
Builds the LinkedIn authorization URL (/oauth/v2/authorization) to send the member to; it sends no request. Scopes are space-separated, defaulting to openid, profile, email, w_member_social.
li.login_url(
*,
redirect_uri: str,
state: Optional[str] = None,
scopes: Optional[list[str]] = None,
) -> str- Raises
ConfigurationErrorwithoutclient_id. stateis CSRF protection: reject the callback if it does not match what you sent (LinkedIn recommendation). The SDK does not store it.- LinkedIn rule: members cannot pick individual scopes, and requesting a scope different from the one previously granted invalidates all previous access tokens.
redirect_urimust match one of the redirect URLs on the app's Auth tab.
url = li.login_url(redirect_uri="https://siz.uz/cb", state=state)Token lifecycle
Refreshing with a refresh token and introspecting token status.
introspect
asyncChecks a token at /oauth/v2/introspectToken and returns LinkedIn's response as a dict: active, status, expires_at, scope, auth_type and more. Use it to see whether a token still works and when it expires.
await li.introspect(token: str) -> dictstatusisactive,expiredorrevoked;scopeis only returned for 3-legged OAuth tokens.- LinkedIn rule: with valid credentials but a token from another app, the response is 200 with
active: false— nothing is raised.
info = await li.introspect(token)refresh
asyncGets a new access token with a refresh token (grant_type=refresh_token) and returns a TokenBundle, so members do not have to sign in again when a token expires.
await li.refresh(refresh_token: str) -> socialift.models.TokenBundle- LinkedIn issues programmatic refresh tokens only to approved Marketing Developer Platform partners; otherwise the member must authorize again.
- Refresh tokens last 365 days and refreshing does not extend that; the new access token lasts 60 days.
- Raises
ApiErrorif the response has noaccess_token.
bundle = await li.refresh(refresh_token)Profile
The OpenID Connect profile and the author URN.
me
asyncReturns the member's profile from OpenID Connect userinfo (/v2/userinfo) as a Profile: id (= sub), name, picture and email. You need sub to build the post author URN.
await li.me(token: str) -> socialift.models.Profile- Needs the
openidandprofilescopes, plusemailfor the address; per LinkedIn,emailis optional and may be missing. Profile.usernameis the email, orsubwhen absent;Profile.rawincludes an extralanguagederived from the locale.
Required permissions:openidprofileemail
me = await li.me(token)person_urn
staticmethodTurns sub into urn:li:person:{sub} — the author value for posts and create_post. A static method that sends no request.
LinkedIn.person_urn(sub: str) -> strcreate_postandpostswrap any value not starting withurn:this way automatically.
Required permissions:r_member_socialw_member_social
posts = await li.posts(token, author=li.person_urn(sub), count=10)post = await li.create_post(token, author=li.person_urn(sub), text="Yangiliklar!",
article_url="https://siz.uz/blog")Posts
Posts API: listing, creating and deleting a member's posts.
create_post
asyncCreates a text post (optionally with a link) on the member's behalf via the Posts API and returns {id, body}; id is the post URN from the x-restli-id header. visibility is PUBLIC or CONNECTIONS.
await li.create_post(
token: str,
*,
author: str,
text: str,
visibility: str = 'PUBLIC',
article_url: Optional[str] = None,
article_title: Optional[str] = None,
) -> dict- Requires
w_member_social(Share on LinkedIn product). - For a link the SDK sets
content.articlewithsourceandtitle(defaulting to the URL). The Posts API does not scrape URLs, so no thumbnail or description is generated. - Any other
visibilityraisesConfigurationError. - No image, video or document upload in this method.
Required permissions:w_member_social
post = await li.create_post(token, author=li.person_urn(sub), text="Yangiliklar!",
article_url="https://siz.uz/blog")delete_post
asyncDeletes a post by URN (urn:li:share:... or urn:li:ugcPost:...) and returns {deleted: True, urn}. The URN is fully encoded in the request path.
await li.delete_post(token: str, post_urn: str) -> dict- LinkedIn rule: deletes are idempotent — an already deleted post also returns 204; batch delete is not supported.
- Requires
w_member_social; this cannot be undone.
Required permissions:w_member_social
await li.delete_post(token, post_urn)posts
asyncLists an author's posts with the Posts API q=author finder and returns LinkedIn's response (elements, paging). Paginate with start and count.
await li.posts(token: str, *, author: str, start: int = 0, count: int = 10) -> dict- Member posts require
r_member_social, which LinkedIn restricts to approved users; on 403 the SDK raises aPermissionErrorwith an explanation. - LinkedIn rule:
countdefaults to 10 with a maximum of 100; results are sorted by last-modified time, newest first. - A page can hold fewer than
countitems even when more posts exist — usepaging.linksto find the next page.
Required permissions:r_member_social
posts = await li.posts(token, author=li.person_urn(sub), count=10)Methods shared by all clients
Every HTTP client (Telegram, Facebook, Instagram, LinkedIn, TikTok, Ads) has these low-level methods — for calling an endpoint the SDK doesn't wrap yet or for reading pages of results. The connection is closed automatically when the async with block ends.
request
asyncSends an arbitrary HTTP request and returns the JSON response — for endpoints the SDK doesn't wrap with a dedicated method yet. You pass the token and signing parameters yourself in params or headers (access_token, and for Meta appsecret_proof via socialift.crypto.appsecret_proof).
await li.request(
method: str,
url: str,
*,
params: Optional[Mapping[str, Any]] = None,
data: Optional[Any] = None,
json: Optional[Any] = None,
headers: Optional[Mapping[str, str]] = None,
) -> AnyNonevalues inparamsare not sent.- Rate limits (
RateLimitError) and transient server errors (HTTP 429, 500, 502, 503, 504) are retried according toRetryPolicy; aRetry-Afterheader is honoured. Network failures are raised asTransportError.
get
asyncShorthand for request("GET", ...).
await li.get(url: str, **kw) -> Anypost
asyncShorthand for request("POST", ...).
await li.post(url: str, **kw) -> Anydelete
asyncShorthand for request("DELETE", ...).
await li.delete(url: str, **kw) -> Anypaginate
asyncFollows Meta's cursor pagination (paging.next) and returns every data item as a single list.
await li.paginate(
url: str,
*,
params: Optional[Mapping[str, Any]] = None,
max_items: int = 500,
page_size: int = 100,
) -> list[dict]max_items(default 500) is a safety cap — a large account can have tens of thousands of records.page_sizesets thelimitof each request.
close
asyncCloses the HTTP session the SDK created itself (a session you passed in is left open). Called automatically when you use async with Client(...).
await li.close() -> NoneFrequently asked questions
How long does a LinkedIn access token last?
Per LinkedIn, all access tokens are currently issued with a 60-day lifespan (expires_in). Programmatic refresh tokens are available only to approved Marketing Developer Platform partners and last 365 days; using one does not extend the refresh token's own lifetime. Without a refresh token the member has to authorize again.
Why do I get 403 when reading LinkedIn posts?
posts() reads an author's posts with q=author, and member posts require r_member_social. LinkedIn marks that permission as restricted and grants it to approved users only. Without it the SDK raises a PermissionError with an explanatory message.
What does HTTP 426 mean in the LinkedIn API?
The LinkedIn-Version header on a /rest/* call points to a version that is no longer active. LinkedIn supports each version for at least one year and then returns 426 with the NONEXISTENT_VERSION code. Update api_version or LINKEDIN_API_VERSION in the SDK to an active YYYYMM value.
How do I set the author of a LinkedIn post?
A personal post's author is urn:li:person:{sub}, where sub is the Profile.id returned by me(). person_urn(sub) builds that string, and create_post and posts wrap any value that does not start with urn: for you.
Does Socialift support LinkedIn messages or Lead Gen Forms?
No. The LinkedIn client covers sign-in, tokens, the profile and posts only. The SDK has no methods for private messages or Lead Gen Forms.