Von allen Social-APIs, die ich angebunden habe, braucht die von LinkedIn am längsten bis zum ersten erfolgreichen Call. Nicht weil sie schlecht entworfen wäre — sondern weil fünf einzelne Dinge stimmen müssen, bevor überhaupt etwas funktioniert, und jedes davon mit einer Fehlermeldung scheitert, die dir nicht sagt, welches der fünf es war.
Das ist die Reihenfolge, in der ich es heute machen würde, nachdem ich es einmal in der falschen gemacht habe.

1. Du brauchst einen öffentlichen HTTPS-Callback
LinkedIn leitet nicht auf localhost um. Nichts am OAuth-Flow funktioniert, bevor deine Entwicklungsmaschine eine öffentliche HTTPS-URL hat — fang also hier an, alles andere ist dahinter blockiert.
ngrok http 80
Das gibt dir etwas wie a1b2-93-184-216-34.ngrok-free.app. Das komplette Setup, inklusive der Forwarded-Header-Falle, die dein Framework http://-URLs erzeugen und den Flow trotzdem scheitern lässt, steht in dem ngrok-Artikel.
Zwei LinkedIn-spezifische Hinweise. Die Redirect-URI muss zeichengenau zu dem passen, was du registriert hast — ein Unterschied bei einem Schrägstrich am Ende ist eine Ablehnung. Und die URI muss im Auth-Tab der App registriert sein, bevor du sie benutzt, nicht danach; es gibt keine Verzögerung durch Propagation, aber eben auch keinen hilfreichen Fehler, nur ein allgemeines Scheitern.
2. Du musst die richtigen Products beantragen
Das ist der Schritt, der die Leute überrascht, und der, der dich Tage statt Minuten kosten kann.
Eine LinkedIn-App anzulegen gibt dir Zugriff auf nichts. Scopes hängen hinter Products, die du in der Developer Console beantragst, und jedes davon muss freigegeben sein, bevor der zugehörige Scope funktioniert. "Sign In with LinkedIn using OpenID Connect" ist meist sofort da. "Share on LinkedIn" ist meist sofort da. Alles rund um Organisationen, Ad Accounts oder reichhaltigere Profildaten läuft durch ein Review, und ein Review dauert, so lange es dauert.
Also: Finde jeden Scope heraus, den dein Feature braucht, und beantrage diese Products am ersten Tag, bevor du eine Zeile Code schreibst. Sonst baust du die Integration, bekommst ein sauberes Token und stellst dann fest, dass genau der eine Call, den du gebraucht hättest, 403 liefert, weil ein Product-Antrag noch offen ist.
Das Symptom zum Wiedererkennen: Die Autorisierung klappt, das Token ist gültig, und ein bestimmter Endpunkt liefert 403. Das ist fast nie dein Code. Das ist ein fehlendes Product.
3. Der Token-Tausch
Standard Authorization Code Flow. Schick den User zu LinkedIn, hol dir den code auf deinem Callback, tausch ihn ein:
$params = [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->redirectUrl,
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
];
$response = $this->client->request(
'POST',
'https://www.linkedin.com/oauth/v2/accessToken',
['body' => $params]
);
$data = $response->toArray();
// $data['access_token'], $data['expires_in']
Die redirect_uri hier muss identisch mit der aus dem Autorisierungsschritt sein. Derselbe String, noch mal. LinkedIn vergleicht sie und lehnt eine Abweichung mit einem Fehler ab, der wie ein allgemeiner Bad Request klingt.
Speicher die Gültigkeit zusammen mit dem Token. LinkedIn-Access-Tokens sind rund 60 Tage gültig, was lang genug ist, dass du diese ganze Integration vergessen hast, wenn eines abläuft — und ein Token, das zwei Monate nach dem Launch still stirbt, ist ein sehr verwirrender Bug-Report.
4. Der Version-Header ist Pflicht
LinkedIns REST-API verlangt, dass du angibst, welche monatliche Version du aufrufst. Lass den Header weg, und der Request scheitert, egal wie richtig alles andere ist:
const LINKEDIN_API_URL = 'https://api.linkedin.com/rest';
const LINKEDIN_VERSION = '202509';
$response = $this->client->request('GET', self::LINKEDIN_API_URL . '/me', [
'headers' => [
'Authorization' => 'Bearer ' . $token,
'LinkedIn-Version' => self::LINKEDIN_VERSION,
'X-Restli-Protocol-Version' => '2.0.0',
],
]);
Halte die Version in einer Konstante, nicht verstreut über die Aufrufstellen. Versionen werden rund ein Jahr unterstützt und hören dann auf zu funktionieren, das ist also ein Wert, den du ändern musst — und du willst, dass das eine Zeile mit einem Testlauf dahinter ist und keine Suche durch die Codebase.
Beachte außerdem, dass es zwei API-Oberflächen gibt: die älteren /v2-Endpunkte und die versionierten /rest-Endpunkte. Die Hälfte der Tutorials im Netz nutzt /v2. Schau in der aktuellen Dokumentation für deinen konkreten Endpunkt nach, welche gilt, denn eine Mischung produziert Fehler, die wie Auth-Probleme aussehen.
5. Leg das Token nicht im Klartext in deine Datenbank
Ein Access-Token ist eine Zugangsberechtigung, die im Namen eines Nutzers handelt. In meinem Setup gehen Tokens in einen Secrets Manager, und die Anwendungsdatenbank speichert nur Metadaten — welche Plattform, welcher Nutzer, wann es abläuft:
$localAccessData = new LocalTokenData();
$localAccessData->setPlattformName('linkedin');
$localAccessData->setExpires($expireDate);
$localAccessData->setPortalUserId($userData['id']);
$this->handleTokenService->saveToken($localAccessData, $data['access_token']);
Lokal brauchst du dieselbe Infrastruktur nicht — eine Umgebungsvariable reicht für die Entwicklung. Wichtig ist, dass der Codepfad derselbe ist, damit du keinen "nur für lokal"-Zweig schreibst, der sich dann von dem unterscheidet, der in Produktion läuft.
Der Workflow, der das erträglich gemacht hat
Zwei Dinge, beide von der ngrok-Seite.
Spiel den Callback noch mal ab, statt den Flow zu wiederholen. Der Traffic-Inspector auf localhost:4040 lässt dich exakt den Callback erneut abfeuern, den LinkedIn geschickt hat. Ohne ihn bedeutet jeder fehlgeschlagene Tausch, den kompletten Autorisierungs-Flow noch mal durchzuklicken — und ab dem fünften Mal machst du Fehler, nur um schneller durchzukommen.
Logge den rohen Response-Body bei Fehlern. LinkedIns HTTP-Statuscodes sind nicht sehr aussagekräftig; der Body sagt meistens etwas Konkretes darüber, welches Feld oder welcher Scope falsch ist. Die Exception zu fangen und nur "Request fehlgeschlagen" zu loggen wirft den einzigen nützlichen Teil weg.
Bei fünf unabhängigen Dingen, die je falsch sein können, besteht die ganze Arbeit darin, aus "es geht nicht" ein "welches der fünf" zu machen.
Wenn du heute anfängst
Beantrage zuerst die Products — das ist der einzige Schritt mit einer Warteschlange davor. Bring den Tunnel zum Laufen, während du wartest. Dann Auth, dann der Version-Header, dann das eigentliche Feature.
In dieser Reihenfolge ist es ein Nachmittag. In der Reihenfolge, die sich natürlich anfühlt — erst den Code schreiben, dann das Product-Gate entdecken — ist es eine Woche mit drei Tagen Warten in der Mitte.