Back to main README: click here
- V1 Docs: https://linkedin.api-docs.io
- V2 Docs:
- Other projects that use the unofficial Voyager API:
- LinkedIn DataHub (this powers a lot of the backend)
- LinkedIn
Rest.li
(also powers some of the backend)
One common way that different pieces of information are requested from Voyager is simply by using different endpoints; /me
for information about the logged-in user, /identity/profiles/.../skillCategory
for skills, etc.
However, certain endpoints have advanced filtering in place, where there is another layer of passing request data - through the query string. The syntax and nature of filtering sort of feels like GraphQL or something like that. Especially since these endpoints will sometimes even return schema
info, which describes the shape of the actual data that is being returned.
The syntax varies, but as of 2020, the more advanced filtered endpoints usually look like this:
{endpoint}/?q={authOrObjectOfInterest}&authOrObjectOfInterest={authOrObjectOfInterest_VALUE}&decorationId={schemaFilter}
To break this down further:
q={authOrObjectOfInterest}
- This can be either who is authorizing the request (e.g.
admin
,viewee
, etc.), or what the request is about (e.g.memberIdentity
, if about another LI user)
- This can be either who is authorizing the request (e.g.
authOrObjectOfInterest={authOrObjectOfInterest_VALUE}
- If the endpoint uses the
q=
param (described above), the value for the query is usually passed by repeating theauthOrObjectOfInterest
string, as part of a new param key-pair. - For example, if the first part of the query string was
q=memberIdentity
, then the next part might be&memberIdentity=joshuatz
. This says that the query is about a member, with the ID ofjoshuatz
.
- If the endpoint uses the
decorationId={schemaFilter}
- For endpoints that are generic base endpoints, and rely heavily on schema filtering, this property is extremely important. Unfortunately, it is also extremely un-documented....
schemaFilter
is a string, written with dot notation, which looks like some sort of nested namespace. E.g.com.linkedin.voyager.dash.deco.identity.profile.PrimaryLocale-3
For some endpoints, only
decorationId
is required, and the other query parameters are completely omitted. Keep in mind that LI knows who is making the request based on the Auth headers.
It looks LinkedIn has been transitioning some page components to pull data from special endpoints flavored with GraphQL. For example, voyagerIdentityGraphQL
.
Using these requires knowing a special queryId
value ahead of time - this could be (maybe?) a server-side generated ID for an allowed query. Getting this ID is... tricky. Short answer is that it relies on some particulars of how LI is using Ember and bundling.
Here is a quickly cobbled-together function to extract a queryId based on a known registered component path:
/**
* Retrieve a GraphQL ID for a pre-registered query (?)
* WARNING: This relies heavily on LI internal APIs - not stable, and should be avoided when other alternatives
* can be used instead
* @param {string} graphQlModuleString - The module path specifier that the GraphQL query is "registered" under. For example, `graphql-queries/queries/profile/profile-components.graphq`
*/
function getGraphQlQueryId(graphQlModuleString) {
if (typeof window.require === 'function') {
const frozenRegisteredQuery = window.require(graphQlModuleString);
return window.require('@linkedin/ember-restli-graphql/-private/query').getGraphQLQueryId(frozenRegisteredQuery);
}
return undefined;
}
Voyager, like the main LinkedIn API, can use different "protocol versions" of the REST API. You might run into issues with certain endpoints and query string formats if you don't specify the correct version via the x-restli-protocol-version
header. E.g., the voyagerIdentityGraphQL
endpoint should probably always use the v2 header:
x-restli-protocol-version: 2.0.0
For more details, see the official API docs for Protocol Versions.
The usual paging querystring parameters are:
count={number}
- I think most APIs would call this
limit
instead
- I think most APIs would call this
start={number}
LI usually responds with paging data in every response, regardless if there is enough entities to need paging anyways. Paging data can be returned at multiple levels; at both the root level (for the thing you requested), as well as at multiple nested sub-levels (for children of the thing you requested).
The paging information object usually looks like this:
type PagingInfo = {
count: number;
start: number;
total?: number;
$recipeTypes?: string[];
// I've never actually seen this property populated...
// This is probably actually `Array<com.linkedin.restli.common.Link>`
links?: string[];
}
Note that several paging properties are often omitted.
As a quick side-note, I've noticed that a lot of the endpoints with dash
anywhere in the path use the newer decorationId
query syntax. This seems to also correspond with a shift in LI's UI towards true SPA functionality, where more of the page is lazy-loaded with filtered data that is slotted into Ember JS templates.
Here are some quick notes on Voyager responses and how data is grouped / nested:
- Elements can be nested several layers deep; you might have an initial collection of elements, where each sub-element is actually a group that contains pointer to further collections of elements
- If you want to get the final layer of elements, you have to be careful about how you get them
- If you simply filter by
$type
, you are going to get elements out of order, and LI does not provide indexes (typically) on elements - The only way to preserve the true order (which will match the rendered result on LI) is to traverse through levels
- If you simply filter by
- Currently,
com.linkedin.restli.common.CollectionResponse
seems to be used for each layer where the element is a collection that points to sub elements (under*elements
) key - This can also make paging a little messy.
- If you want to get the final layer of elements, you have to be careful about how you get them
- LI has limits on certain endpoints, and the amount of nested elements it will return
- See PR #23 for an example of how this was implemented
- Make sure you always include the
Host
header if making requests outside a web browser (browsers will automatically include this for you)- Value should be:
www.linkedin.com
- If you forget it, you will get 400 error (
invalid hostname
)
- Value should be:
- For inline data,
<code></code>
with request payload usually follows<img><code></code>
with response payload - It appears as though whatever language the profile was first created with sticks as the "principal language", regardless if user changes language settings (more on this below).
- You can find this under the main profile object, where you would find
supportedLocales
- the default / initial locale is under -defaultLocale
- You can find this under the main profile object, where you would find
LI seems to be making changes related to this; this section might not be 100% up-to-date.
There are some really strange quirks around multi-locale profiles. When a multi-locale user is logged in and requesting their own profile, LI will refuse to let the x-li-lang
header override the defaultLocale
as specified by the profile (see issue #35). However, if someone else exports their profile, the same exact endpoints will respect the header and will return the correct data for the requested locale (assuming creator made a version of their profile with the requested locale).
Even stranger, this quirk only seems to apply to certain endpoints; e.g. /me
respects the requested language, but /profileView
does not (and always returns data corresponding with defaultLocale
) 🙃
Furthermore, the /dash
subset of endpoints does not ever (AFAIK) change the main key-value pairs based on x-li-lang
; instead, it nests multi-locale data under multiLocale
prefixed keys. For example:
{
"firstName": "Алексе́й",
"multiLocaleFirstName": {
"ru_RU": "Алексе́й",
"en_US": "Alexey"
}
}
I've put some basics LI types in my global.d.ts
. Eventually, it would be nice to re-write the core of this project as TS, as opposed to the current VSCode-powered typed JS approached.