Skip to content

Commit e51b121

Browse files
Add endorsePersonSkill and endorsePersonSkills operations
- endorsePersonSkill: endorse one skill on a 1st-degree connection's profile by its exact name. - endorsePersonSkills: endorse every skill whose name matches any of the given case-insensitive RE2 patterns (all skills when omitted); returns the selected skills in profile order, each with its own success/error. - Add stEndorsements limit category and the skillNotFound, endorsementUnavailable and limitExceeded action error types. - Bump version to 2.4.0.
1 parent ff54a80 commit e51b121

16 files changed

Lines changed: 529 additions & 3 deletions

‎README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,29 @@ To get started with Linked API Node.js SDK, read these essential guides first. S
99
4. [Handling results and errors](https://linkedapi.io/sdks/handling-results-and-errors/) – see how to process successful results and errors.
1010
5. [Persisting and cancelling workflows](https://linkedapi.io/sdks/persisting-and-cancelling-workflows/) – learn how to save, resume, and cancel workflows.
1111

12+
## Skill endorsements in 2.4.0
13+
14+
Use `endorsePersonSkill` to endorse one skill by its exact name, or `endorsePersonSkills` to select skills by case-insensitive RE2 patterns. Omit `skillNamePatterns` to endorse every skill. Endorsements require a 1st-degree connection and preserve any existing endorsement.
15+
16+
```typescript
17+
const workflow = await linkedApi.endorsePersonSkill.execute({
18+
personUrl: 'https://www.linkedin.com/in/example-person',
19+
skillName: 'Project Management',
20+
});
21+
const result = await linkedApi.endorsePersonSkill.result(workflow.workflowId);
22+
console.log(result.errors);
23+
24+
const skillsWorkflow = await linkedApi.endorsePersonSkills.execute({
25+
personUrl: 'https://www.linkedin.com/in/example-person',
26+
skillNamePatterns: ['business', 'project\\s*manage'],
27+
});
28+
const skillsResult = await linkedApi.endorsePersonSkills.result(skillsWorkflow.workflowId);
29+
console.log(skillsResult.data, skillsResult.errors);
30+
```
31+
32+
The single-skill operation returns no data. The multi-skill operation returns only selected skills in profile order, as `{ skillName, success, error? }`. Per-skill failures stay on their items; a failure opening the profile or reading skills returns no data and a top-level error. Each successful endorsement counts toward the `stEndorsements` limit category.
33+
34+
See [examples/endorse-skills.ts](examples/endorse-skills.ts) for both operations and error handling.
35+
1236
## License
1337
This project is licensed under the MIT – see the [LICENSE](https://github.com/Linked-API/linkedapi-node/blob/main/LICENSE) file for details.

‎examples/endorse-skills.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import LinkedApi, { LinkedApiError } from '@linkedapi/node';
2+
3+
async function endorseSkillsExample(): Promise<void> {
4+
const linkedApi = new LinkedApi({
5+
linkedApiToken: process.env.LINKED_API_TOKEN!,
6+
identificationToken: process.env.IDENTIFICATION_TOKEN!,
7+
});
8+
const personUrl = 'https://www.linkedin.com/in/example-person';
9+
10+
try {
11+
const workflow = await linkedApi.endorsePersonSkill.execute({
12+
personUrl,
13+
skillName: 'Project Management',
14+
});
15+
const result = await linkedApi.endorsePersonSkill.result(workflow.workflowId);
16+
if (result.errors.length > 0) {
17+
console.error('Skill endorsement failed:', result.errors);
18+
} else {
19+
console.log('Skill endorsed successfully');
20+
}
21+
22+
const skillsWorkflow = await linkedApi.endorsePersonSkills.execute({
23+
personUrl,
24+
skillNamePatterns: ['business', 'project\\s*manage'],
25+
});
26+
const skillsResult = await linkedApi.endorsePersonSkills.result(skillsWorkflow.workflowId);
27+
if (skillsResult.errors.length > 0) {
28+
console.error('Unable to read the profile skills:', skillsResult.errors);
29+
}
30+
for (const { skillName, success, error } of skillsResult.data ?? []) {
31+
if (success) {
32+
console.log('Endorsed:', skillName);
33+
} else {
34+
console.error('Endorsement failed:', skillName, error);
35+
}
36+
}
37+
} catch (error) {
38+
if (error instanceof LinkedApiError) {
39+
console.error('Linked API error:', error.type, error.message);
40+
} else {
41+
console.error('Unknown error:', error);
42+
}
43+
}
44+
}
45+
46+
if (require.main === module) {
47+
void endorseSkillsExample();
48+
}

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@linkedapi/node",
3-
"version": "2.3.17",
3+
"version": "2.4.0",
44
"description": "Control your LinkedIn accounts and retrieve real-time data, all through this Node.js SDK.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

‎src/core/operation.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export const OPERATION_NAME = {
3030
ignoreInvitation: 'ignoreInvitation',
3131
retrieveConnections: 'retrieveConnections',
3232
removeConnection: 'removeConnection',
33+
endorsePersonSkill: 'endorsePersonSkill',
34+
endorsePersonSkills: 'endorsePersonSkills',
3335
searchCompanies: 'searchCompanies',
3436
searchPeople: 'searchPeople',
3537
searchJobs: 'searchJobs',

‎src/index.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
CreatePost,
99
CreateRepost,
1010
CustomWorkflow,
11+
EndorsePersonSkill,
12+
EndorsePersonSkills,
1113
FetchCompany,
1214
FetchJob,
1315
FetchPerson,
@@ -125,6 +127,8 @@ class LinkedApi {
125127
this.ignoreInvitation = new IgnoreInvitation(this.httpClient);
126128
this.retrieveConnections = new RetrieveConnections(this.httpClient);
127129
this.removeConnection = new RemoveConnection(this.httpClient);
130+
this.endorsePersonSkill = new EndorsePersonSkill(this.httpClient);
131+
this.endorsePersonSkills = new EndorsePersonSkills(this.httpClient);
128132
this.searchCompanies = new SearchCompanies(this.httpClient);
129133
this.searchPeople = new SearchPeople(this.httpClient);
130134
this.searchJobs = new SearchJobs(this.httpClient);
@@ -168,6 +172,8 @@ class LinkedApi {
168172
this.ignoreInvitation,
169173
this.retrieveConnections,
170174
this.removeConnection,
175+
this.endorsePersonSkill,
176+
this.endorsePersonSkills,
171177
this.searchCompanies,
172178
this.searchPeople,
173179
this.searchJobs,
@@ -1390,6 +1396,19 @@ class LinkedApi {
13901396
*/
13911397
public removeConnection: RemoveConnection;
13921398

1399+
/**
1400+
* Endorse a skill by its exact name on a 1st-degree connection's profile.
1401+
* Letter case and extra spaces are ignored. An existing endorsement is preserved.
1402+
*/
1403+
public endorsePersonSkill: EndorsePersonSkill;
1404+
1405+
/**
1406+
* Endorse skills matching any RE2 pattern, or every skill when patterns are omitted.
1407+
* Results contain only selected skills in profile order, with per-skill errors on each item.
1408+
* Failures opening the profile or reading skills appear in the top-level errors with no data.
1409+
*/
1410+
public endorsePersonSkills: EndorsePersonSkills;
1411+
13931412
/**
13941413
* React to a LinkedIn post with an emoji reaction.
13951414
*
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type {
2+
TEndorsePersonSkillsParams,
3+
TPersonSkill,
4+
TPersonSkillEndorsement,
5+
} from '../types/actions';
6+
import { LinkedApiError } from '../types/errors';
7+
import type {
8+
TWorkflowCompletion,
9+
TWorkflowDefinition,
10+
TWorkflowSingleData,
11+
} from '../types/workflows';
12+
13+
import { BaseMapper, TMappedResponse } from './base-mapper.abstract';
14+
15+
type TSkillCompletion = TPersonSkill & TWorkflowSingleData;
16+
17+
export class EndorsePersonSkillsMapper extends BaseMapper<
18+
TEndorsePersonSkillsParams,
19+
Array<TPersonSkillEndorsement>
20+
> {
21+
public override mapRequest(params: TEndorsePersonSkillsParams): TWorkflowDefinition {
22+
const { personUrl, skillNamePatterns } = params;
23+
24+
return {
25+
actionType: 'st.openPersonPage',
26+
personUrl,
27+
basicInfo: false,
28+
then: {
29+
actionType: 'st.retrievePersonSkills',
30+
then: {
31+
actionType: 'st.doForSkills',
32+
...(skillNamePatterns !== undefined ? { skillNamePatterns } : {}),
33+
then: {
34+
actionType: 'st.endorsePersonSkill',
35+
},
36+
},
37+
},
38+
};
39+
}
40+
41+
public override mapResponse(
42+
completion: TWorkflowCompletion,
43+
): TMappedResponse<Array<TPersonSkillEndorsement>> {
44+
const root = Array.isArray(completion) ? completion[0] : completion;
45+
if (!root) {
46+
throw LinkedApiError.unknownError();
47+
}
48+
if (root.error) {
49+
return { errors: [root.error] };
50+
}
51+
52+
const then = Array.isArray(root.data) ? undefined : root.data?.then;
53+
const skillsAction = Array.isArray(then)
54+
? then.find((action) => action.actionType === 'st.retrievePersonSkills')
55+
: then;
56+
if (!skillsAction || skillsAction.actionType !== 'st.retrievePersonSkills') {
57+
throw LinkedApiError.unknownError();
58+
}
59+
if (skillsAction.error) {
60+
return { errors: [skillsAction.error] };
61+
}
62+
if (!Array.isArray(skillsAction.data)) {
63+
throw LinkedApiError.unknownError();
64+
}
65+
66+
const skills = skillsAction.data as Array<TSkillCompletion>;
67+
const data: Array<TPersonSkillEndorsement> = [];
68+
for (const skill of skills) {
69+
const endorsement = Array.isArray(skill.then)
70+
? skill.then.find((action) => action.actionType === 'st.endorsePersonSkill')
71+
: skill.then;
72+
if (!endorsement || endorsement.actionType !== 'st.endorsePersonSkill') {
73+
continue;
74+
}
75+
76+
data.push({
77+
skillName: skill.name,
78+
success: endorsement.success,
79+
...(endorsement.error ? { error: endorsement.error } : {}),
80+
});
81+
}
82+
83+
return {
84+
data,
85+
errors: [],
86+
};
87+
}
88+
}

0 commit comments

Comments
 (0)