Jul 18, 2023

LinkedIn Developer API

#programming #api

I had the inspiration to build an app today and after a couple hours, I'm giving up.

I wanted to fetch a random LinkedIn connection, and take a couple actions on it: if I remembered who it was, write a little note and save it, if I didn't, delete the connection. I thought this would be a fun little game I could play in idle moments and also use it as a way to reconnect with people I had lost touch with.

To make this app, I'd need to:

  1. sign in with LinkedIn
  2. fetch my connections
  3. store "notes" some data somewhere
  4. send a delete request for a particular connection

When I started, I had no idea how I would do any of these things, but I assumed LinkedIn would have a sufficient API to sign in and fetch/delete connections.

I started by creating a new repository with an index.html. I used vercel dev to serve this HTML, because I knew from past experience that auth flows from third party services don't work on file:// URLs, and that I'd need a server for an OAuth flow, so serverless functions would be nice. I added a Sign in button:

<a href="linkedin.com/somethingsomething">Sign in</a>

I read the Authentication guide on LinkedIn's Developer portal (which was surprisingly easy to follow), and updated my Sign in link to point to the correct URL with a redirect_url param pointing to /api/auth-callback.

Turns out, you need to create a Linkedin Company page to setup an Developer App to get the OAuth client ID and secret. This was a bit annoying, but I created one with the minimal amount of details needed.

I set up the client ID and redirect URL on my sign in link:

<a href="https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id={redacted}&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth-callback&state=foobar&scope=r_liteprofile%20r_emailaddress">
Sign in

but shortly after, I realized that while a serverless function would be a collect the information I needed from the auth process, I wouldn't be able to do anything after that. I needed to be able to redirect somewhere.

Enter middleware.

Some grappling later, I ended up with some working code that redirected to / with a query param ?token=${token}. (This was not meant to be right, I just wanted to get access to the token so I could make real requests).

The grappling involved originally reading a page of docs that only included Next.js example code, so I couldn't figure out how to do a redirect. But it was user error, I had landed on a random docs page instead of starting from the Overview page that linked to the Middleware API.

See middleware code
export const config = {
matcher: "/auth-callback",

export default async function (req) {
const reqURL = new URL(req.url);
const { searchParams: params } = reqURL;
if (params.has("error")) {
return Response.redirect(new URL("/?failed=true", req.url));

const code = params.get("code");

let token;
try {
// hand wave over next step
token = await getToken(code);
} catch (e) {
// nothing

if (!token) {
return Response.redirect(new URL("/?failed=true", req.url));

return Response.redirect(new URL(`/?token=${token}`, req.url));

// get token using authCode
async function getToken(code) {
const queryParams = new URLSearchParams({
grant_type: 'authorization_code',
client_id: '{redacted}',
client_secret: '{redacted}',
redirect_uri: "http://localhost:3000/auth-callback",

const res = await await fetch(
`https://www.linkedin.com/oauth/v2/accessToken?${queryParams}`, {
method: "POST",
headers: {
"Content-Type": "application/json",

const json = await res.json();
if (res.ok) {
return json.access_token;

throw new Error("Failed", json.error);

This took at least an hour or so, but then I found out that the basic provisioning I had set up for my Developer App only had access to a /me endpoint that did not include my connections. Grappling through Linkedin "Products", which enable access to "scopes", which grant permissions to endpoints, I saw that there was no way to get the /connections endpoint. The two Products that gave me access to that endpoint were the "Advertising API" and the "Community Management API". I could request access to the former by filling out a form (No thanks), but the latter wasn't even request-able.

So I finally hit a dead end and gave up. Maybe I'll try it again some day with web scraping instead.

If you like this post, please share it on Twitter. You can also email me email me or subscribe to my RSS feed.