LinkedIn API Integration in Python Django (OAuth 2.0 + OIDC)

Blog / Django · March 31, 2016 · Updated June 10, 2026 · 7 min read
LinkedIn API Integration in Python Django (OAuth 2.0 + OIDC)

To integrate the LinkedIn API in a Python Django app today, use OAuth 2.0 three-legged authorization with Sign In with LinkedIn using OpenID Connect (OIDC). Redirect the user to LinkedIn's authorization URL with the openid profile email scopes, exchange the returned code for an access token at /oauth/v2/accessToken, then call GET https://api.linkedin.com/v2/userinfo to read the member's id, name, photo, and verified email. The legacy OAuth 1.0a flow, the r_basicprofile/r_emailaddress scopes, the /v1/people/~ endpoint, and the old python-linkedin library are all retired in 2026 and will not work.

Key takeaways

  • OAuth 2.0 + OpenID Connect is the only self-serve sign-in path now. Request openid profile email — not the removed r_basicprofile / r_emailaddress scopes.
  • Read profile data from /v2/userinfo, not /v1/people/~. The response is OIDC claims (sub, name, email, picture).
  • Posting on a member's behalf needs the w_member_social scope and the Posts API (/rest/posts); the old ugcPosts endpoint is deprecated.
  • Broad data is restricted. Connections, full work history, and organization analytics require Marketing Developer Platform partner approval — they are not available self-serve.
  • Use requests on Python 3; the unmaintained python-linkedin library and Python 2 urllib examples no longer apply.
  • For production sign-in, django-allauth's linkedin_oauth2 provider handles the whole OIDC handshake for you.

What changed in the LinkedIn API, and why the old guide breaks

If you followed a pre-2023 tutorial (including the original version of this post), every step now fails. LinkedIn rebuilt authentication around OAuth 2.0 and OpenID Connect, renamed the scopes, and replaced the profile endpoints. Here is the old-vs-current mapping so you can migrate quickly.

Aspect Old (this article's original, ~2016) Current (2026)
Auth protocol OAuth 1.0a / early OAuth 2.0 OAuth 2.0 (3-legged) + OpenID Connect
Profile scope r_basicprofile / r_liteprofile profile (OIDC)
Email scope r_emailaddress email (OIDC)
Profile endpoint GET /v1/people/~:(...) GET /v2/userinfo
Token endpoint /uas/oauth2/accessToken /oauth/v2/accessToken
Posting /v1/people/~/shares, then /v2/ugcPosts /rest/posts (Posts API); ugcPosts deprecated
HTTP client urllib / urllib2 (Python 2) requests (Python 3)
Helper library python-linkedin (unmaintained) requests / Authlib / django-allauth

What can you actually access from LinkedIn today?

Be realistic about scope before you build. LinkedIn tightly gates its data behind "products" you enable on your app, and the richest data needs partner review.

  • Self-serve (any app): sign-in identity via OpenID Connect — sub (stable member id), name, given_name, family_name, picture, locale, and the verified email.
  • Self-serve, with the Share product: create posts/shares as the authenticated member using w_member_social.
  • Partner-only (Marketing Developer Platform): connections, detailed work history, organization pages, follower and post analytics, and lead-gen forms. These require an application and approval — you cannot get them by ticking a scope box.

If your goal is "log in with LinkedIn and capture a verified email," you are fully self-serve. If your goal is "pull a member's connection graph," plan for a partner application or a different data source.

How do you create and configure a LinkedIn app?

  1. Go to the LinkedIn Developer Portal and create an app linked to a Company Page (required).
  2. On the Products tab, request "Sign In with LinkedIn using OpenID Connect". Add "Share on LinkedIn" only if you need to post.
  3. On the Auth tab, copy your Client ID and Client Secret, and add your exact redirect (callback) URL (for example https://yourapp.com/auth/linkedin/callback/). It must match byte-for-byte.
  4. Keep secrets out of source control — load them from environment variables into Django settings.
# settings.py
import os

LINKEDIN_CLIENT_ID = os.environ["LINKEDIN_CLIENT_ID"]
LINKEDIN_CLIENT_SECRET = os.environ["LINKEDIN_CLIENT_SECRET"]
LINKEDIN_REDIRECT_URI = "https://yourapp.com/auth/linkedin/callback/"

How do you build the OAuth 2.0 authorization redirect in Django?

Send the user to LinkedIn's authorization endpoint with the OIDC scopes and a random state value (stored in the session) to protect against CSRF.

# views.py
import secrets
from urllib.parse import urlencode

from django.conf import settings
from django.shortcuts import redirect

LINKEDIN_AUTH_URL = "https://www.linkedin.com/oauth/v2/authorization"


def linkedin_login(request):
    state = secrets.token_urlsafe(16)
    request.session["linkedin_state"] = state
    params = {
        "response_type": "code",
        "client_id": settings.LINKEDIN_CLIENT_ID,
        "redirect_uri": settings.LINKEDIN_REDIRECT_URI,
        "scope": "openid profile email",  # add "w_member_social" only to post
        "state": state,
    }
    return redirect(f"{LINKEDIN_AUTH_URL}?{urlencode(params)}")

How do you exchange the authorization code for an access token?

LinkedIn redirects back to your callback with code and state. Verify the state, then POST the code to the token endpoint with requests. With the openid scope you also receive an id_token (a JWT) alongside the access token.

# views.py (continued)
import requests
from django.conf import settings
from django.http import HttpResponseBadRequest

LINKEDIN_TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken"


def linkedin_callback(request):
    if request.GET.get("state") != request.session.pop("linkedin_state", None):
        return HttpResponseBadRequest("Invalid OAuth state")

    code = request.GET.get("code")
    if not code:
        # User denied access, or LinkedIn returned ?error=...
        return HttpResponseBadRequest("Authorization was not granted")

    token_res = requests.post(
        LINKEDIN_TOKEN_URL,
        data={
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": settings.LINKEDIN_REDIRECT_URI,
            "client_id": settings.LINKEDIN_CLIENT_ID,
            "client_secret": settings.LINKEDIN_CLIENT_SECRET,
        },
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        timeout=10,
    )
    token_res.raise_for_status()
    payload = token_res.json()
    access_token = payload["access_token"]
    # payload also contains "expires_in" (~5184000s / 60 days) and "id_token"
    return _fetch_profile(request, access_token)

How do you fetch the member profile and verified email?

Call the OpenID Connect /v2/userinfo endpoint with the access token as a Bearer credential. This replaces the old /v1/people/~ field-projection request entirely. You can either decode the id_token JWT locally or — simpler and always current — call userinfo.

# views.py (continued)
LINKEDIN_USERINFO_URL = "https://api.linkedin.com/v2/userinfo"


def _fetch_profile(request, access_token):
    profile = requests.get(
        LINKEDIN_USERINFO_URL,
        headers={"Authorization": f"Bearer {access_token}"},
        timeout=10,
    )
    profile.raise_for_status()
    data = profile.json()

    linkedin_id = data["sub"]              # stable member id (URN-safe)
    full_name = data.get("name")
    email = data.get("email")              # requires the "email" scope
    email_verified = data.get("email_verified")
    picture = data.get("picture")

    # ... create/update your Django User here, then log them in.
    return redirect("dashboard")

How do you post or share content on behalf of a member?

Posting requires the w_member_social scope (from the "Share on LinkedIn" product) and the modern Posts API at /rest/posts. The author is the member URN built from the sub claim you got from userinfo: urn:li:person:<sub>. Note that the older /v2/ugcPosts endpoint is deprecated and should not be used for new code.

# share.py
import requests


def share_text_post(access_token, member_sub, text):
    res = requests.post(
        "https://api.linkedin.com/rest/posts",
        headers={
            "Authorization": f"Bearer {access_token}",
            "LinkedIn-Version": "202506",        # required monthly version header
            "X-Restli-Protocol-Version": "2.0.0",
            "Content-Type": "application/json",
        },
        json={
            "author": f"urn:li:person:{member_sub}",
            "commentary": text,
            "visibility": "PUBLIC",
            "lifecycleState": "PUBLISHED",
            "distribution": {
                "feedDistribution": "MAIN_FEED",
                "targetEntities": [],
                "thirdPartyDistributionChannels": [],
            },
        },
        timeout=10,
    )
    res.raise_for_status()
    return res.headers.get("x-restli-id")  # URN of the created post

Should you use django-allauth instead of hand-rolling this?

For production sign-in, yes. django-allauth ships a linkedin_oauth2 provider that performs the full OpenID Connect handshake, stores tokens, and wires LinkedIn into Django's auth system — so you avoid maintaining the callback, state, and token-refresh code yourself. Build the manual flow above when you need fine-grained control (for example, custom posting logic); reach for allauth when you just need "Log in with LinkedIn."

# settings.py
INSTALLED_APPS = [
    # ...
    "django.contrib.sites",
    "allauth",
    "allauth.account",
    "allauth.socialaccount",
    "allauth.socialaccount.providers.linkedin_oauth2",
]

SITE_ID = 1

SOCIALACCOUNT_PROVIDERS = {
    "linkedin_oauth2": {
        "SCOPE": ["openid", "profile", "email"],
        # Configure the Client ID/Secret via a SocialApp in the Django admin
        # (or the SOCIALACCOUNT_PROVIDERS "APP" key on recent versions).
    }
}

Which LinkedIn scopes and endpoints do you actually need?

Scope Grants Product / access required
openid ID token (OIDC), sub member id Sign In with LinkedIn using OpenID Connect
profile Name, photo, vanity URL via /v2/userinfo Sign In with LinkedIn using OpenID Connect
email Verified email via /v2/userinfo Sign In with LinkedIn using OpenID Connect
w_member_social Create posts/shares as the member Share on LinkedIn / Community Management API
r_organization_social, r_member_social, analytics Org pages, read analytics, connections Marketing Developer Platform (partner approval)

This same OAuth 2.0 pattern powers most social integrations. If you are wiring up several providers, see our companion guides on the GitHub API in Django, the Twitter/X social API in Django, and single sign-on with Auth0 in Django.

Frequently Asked Questions

Is the python-linkedin library still usable in 2026?

No. The python-linkedin package is unmaintained and was built for OAuth 1.0a / legacy scopes and the retired /v1/people API, so it no longer works. Use requests directly (as shown above), or a maintained OAuth client such as Authlib or requests-oauthlib, or django-allauth for sign-in.

What LinkedIn scopes can I get without partner approval?

Self-serve apps can use openid, profile, and email (from "Sign In with LinkedIn using OpenID Connect") and w_member_social (from "Share on LinkedIn"). Anything broader — connections, organization data, analytics — requires Marketing Developer Platform partner approval.

How do I get a user's verified email from LinkedIn in Django?

Request the email scope in the authorization URL, complete the OAuth 2.0 token exchange, then call GET https://api.linkedin.com/v2/userinfo with the access token. Read data["email"] and data["email_verified"] from the JSON response.

Can I read a member's connections or full work history?

Not through the self-serve flow. The OpenID Connect userinfo endpoint returns identity claims (name, photo, email) only. Connection graphs and detailed positions require Marketing Developer Platform partner access, which is approval-gated and not available by simply requesting a scope.

Do LinkedIn access tokens expire, and can I refresh them?

Yes. Member access tokens are valid for about 60 days. Refresh tokens (valid roughly a year) are available, but programmatic refresh is granted to approved Marketing Developer Platform apps — standard sign-in apps typically re-run the OAuth flow to get a fresh token.

Should I build the OAuth flow manually or use django-allauth?

Use django-allauth's linkedin_oauth2 provider for standard "Log in with LinkedIn" — it manages the OIDC handshake, callbacks, and token storage. Build the manual requests flow when you need custom control, such as posting with w_member_social or storing tokens in your own model.

Share this article