DEV Community

Aniefon Umanah
Aniefon Umanah

Posted on

Publishing to LinkedIn Without the Official API: A Cookie-Based Approach

The LinkedIn API for posting is notoriously restricted. You need to apply for partnership access, and even then, you're limited to organization pages. For individual developer accounts trying to automate their own posts? You're out of luck.

So I built a workaround: using browser cookies and Playwright to automate publishing to LinkedIn as if you're using the actual website.

The Core Problem

LinkedIn's official API won't let you post to personal profiles. But every developer knows that where there's a browser session, there's a way. The challenge is capturing that session in a way that's both secure and reliable.

The Cookie Capture Strategy

Instead of storing passwords or dealing with OAuth flows that don't grant the permissions we need, I'm capturing the active browser session:

interface LinkedInExtensionConnectInput {
  userId: string;
  cookies: LinkedInCookie[];
  userAgent: string;
}
Enter fullscreen mode Exit fullscreen mode

The key cookies we need are li_at (LinkedIn authentication token) and JSESSIONID. These are captured from an active browser session via a Chrome extension, then stored for automation:

const liAtCookie = cookies.find((c) => c.name === 'li_at');
const jSessionCookie = cookies.find((c) => c.name === 'JSESSIONID');

if (!liAtCookie || !jSessionCookie) {
  throw new BadRequestException(
    'Missing required LinkedIn session cookies.'
  );
}
Enter fullscreen mode Exit fullscreen mode

The Publishing Flow

Here's where it gets interesting. With Playwright, we can replay that browser session:

const browser = await chromium.launch({ headless: false });

const context = await browser.newContext({
  userAgent: credentials.userAgent
});

// Inject the captured cookies
const playwrightCookies = this.convertCookies(credentials.cookies);
await context.addCookies(playwrightCookies);
Enter fullscreen mode Exit fullscreen mode

Now we have a browser that's "logged in" as the user, without ever touching their password.

The LinkedIn UI Challenge

LinkedIn's UI changes frequently, which makes selector-based automation fragile. The solution? Try multiple selectors until one works:

const startPostSelectors = [
  '[placeholder="Start a post"]',
  'div[data-placeholder="Start a post"]',
  '.share-box-feed-entry__top-bar',
  '.share-creation-state__text-field',
  'div[role="button"]:has-text("Start a post")',
  'button:has-text("Start a post")',
];

let startPostButton = null;
for (const selector of startPostSelectors) {
  try {
    startPostButton = await page.waitForSelector(selector, { 
      timeout: 2000 
    });
    if (startPostButton && await startPostButton.isVisible()) {
      break;
    }
  } catch {
    continue;
  }
}
Enter fullscreen mode Exit fullscreen mode

This defensive approach means when LinkedIn tweaks their HTML, we don't immediately break.

Handling Session Expiry

The li_at cookie has an expiration date. We extract and store it:

const expiresAt = liAtCookie.expires
  ? new Date(liAtCookie.expires * 1000).toISOString()
  : undefined;
Enter fullscreen mode Exit fullscreen mode

Then before publishing, we check:

if (credentials.expiresAt && new Date(credentials.expiresAt) < new Date()) {
  return {
    success: false,
    error: 'LinkedIn session has expired. Please reconnect.'
  };
}
Enter fullscreen mode Exit fullscreen mode

This prevents failed publish attempts and tells the user exactly what they need to do.

The Tradeoff

This approach sacrifices the elegance of a proper API integration for something that actually works. It's more brittle—LinkedIn could change their UI tomorrow. It requires a headless browser, which means more resources. And users need to reconnect via the extension when their session expires.

But it works. For developers who want to automate their personal LinkedIn presence, that's what matters.

The alternative is manually posting or not posting at all. Given that choice, a little browser automation doesn't seem so bad.

Top comments (0)