Skip to content

Commit b8b7b29

Browse files
Add admin webhook commands
1 parent 35e92a6 commit b8b7b29

9 files changed

Lines changed: 313 additions & 1 deletion

File tree

examples/webhooks.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { LinkedApiAdmin, LinkedApiError, parseWebhookEvent } from '@linkedapi/node';
2+
import express from 'express';
3+
4+
// 1. Register a webhook and inspect deliveries via the admin client.
5+
async function manageWebhook(): Promise<void> {
6+
const admin = new LinkedApiAdmin({
7+
linkedApiToken: process.env.LINKED_API_TOKEN!,
8+
});
9+
10+
try {
11+
console.log('🚀 Registering webhook...');
12+
13+
const webhook = await admin.webhooks.set({
14+
url: 'https://example.com/hooks/linkedapi',
15+
payloadMode: 'fat',
16+
});
17+
console.log(`✅ Webhook ${webhook.id}${webhook.url}`);
18+
19+
// Verify the endpoint end-to-end without waiting for a real event.
20+
await admin.webhooks.sendTest();
21+
22+
const deliveries = await admin.webhooks.deliveries();
23+
console.log(`📬 Recent deliveries: ${deliveries.length}`);
24+
for (const delivery of deliveries) {
25+
console.log(` ${delivery.eventType}${delivery.status} (attempts: ${delivery.attempts})`);
26+
}
27+
} catch (error) {
28+
if (error instanceof LinkedApiError) {
29+
console.error('🚨 Linked API Error:', error.message);
30+
} else {
31+
console.error('💥 Unknown error:', error);
32+
}
33+
}
34+
}
35+
36+
// 2. Receive events. Capture the RAW body so the payload is parsed exactly as delivered.
37+
function startReceiver(): void {
38+
const app = express();
39+
app.use(express.raw({ type: 'application/json' }));
40+
41+
app.post('/hooks/linkedapi', (req, res) => {
42+
const event = parseWebhookEvent(req.body as Buffer);
43+
44+
switch (event.type) {
45+
case 'workflow.completed':
46+
console.log(`Workflow ${event.data.workflowId} finished: ${event.data.status}`);
47+
console.log('Result:', event.data.result);
48+
break;
49+
case 'account.reconnectionRequired':
50+
console.log(`Account ${event.data.accountId} needs reconnection — pause campaigns.`);
51+
break;
52+
case 'webhook.test':
53+
console.log('Test event received:', event.data.message);
54+
break;
55+
default:
56+
console.log(`Event ${event.type}`);
57+
}
58+
59+
// Acknowledge fast (2xx). Non-2xx makes Linked API retry with backoff.
60+
res.sendStatus(200);
61+
});
62+
63+
app.listen(3000, () => console.log('👂 Listening for webhooks on :3000'));
64+
}
65+
66+
void manageWebhook();
67+
startReceiver();

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.0.5",
3+
"version": "2.1.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/admin/admin-webhooks.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
HttpClient,
3+
LinkedApiError,
4+
TDeleteWebhookParams,
5+
TLinkedApiErrorType,
6+
TReplayWebhookDeliveryParams,
7+
TSetWebhookParams,
8+
TSetWebhookPayloadModeParams,
9+
TWebhookDelivery,
10+
TWebhookSubscription,
11+
} from '../types';
12+
13+
/**
14+
* Manage the client's registered outbound webhook and inspect recent deliveries.
15+
*
16+
* A client may hold at most one active webhook. It receives every event Linked API emits
17+
* (workflow lifecycle + account status changes); filter by the event `type` on your receiver.
18+
*
19+
* @see {@link https://linkedapi.io/docs/ Linked API Documentation}
20+
*/
21+
export class AdminWebhooks {
22+
constructor(private readonly httpClient: HttpClient) {}
23+
24+
public async set(params: TSetWebhookParams): Promise<TWebhookSubscription> {
25+
const response = await this.httpClient.post<{ webhook: TWebhookSubscription }>(
26+
'/admin/webhook.set',
27+
params,
28+
);
29+
if (response.success && response.result) {
30+
return response.result.webhook;
31+
}
32+
throw new LinkedApiError(
33+
(response.error?.type ?? 'httpError') as TLinkedApiErrorType,
34+
response.error?.message ?? 'Failed to set webhook',
35+
);
36+
}
37+
38+
public async get(): Promise<Array<TWebhookSubscription>> {
39+
const response = await this.httpClient.post<{ webhooks: Array<TWebhookSubscription> }>(
40+
'/admin/webhook.get',
41+
);
42+
if (response.success && response.result) {
43+
return response.result.webhooks;
44+
}
45+
throw new LinkedApiError(
46+
(response.error?.type ?? 'httpError') as TLinkedApiErrorType,
47+
response.error?.message ?? 'Failed to get webhooks',
48+
);
49+
}
50+
51+
public async setPayloadMode(params: TSetWebhookPayloadModeParams): Promise<TWebhookSubscription> {
52+
const response = await this.httpClient.post<{ webhook: TWebhookSubscription }>(
53+
'/admin/webhook.setPayloadMode',
54+
params,
55+
);
56+
if (response.success && response.result) {
57+
return response.result.webhook;
58+
}
59+
throw new LinkedApiError(
60+
(response.error?.type ?? 'httpError') as TLinkedApiErrorType,
61+
response.error?.message ?? 'Failed to set webhook payload mode',
62+
);
63+
}
64+
65+
public async delete(params: TDeleteWebhookParams): Promise<void> {
66+
const response = await this.httpClient.post('/admin/webhook.delete', params);
67+
if (response.success) {
68+
return;
69+
}
70+
throw new LinkedApiError(
71+
(response.error?.type ?? 'httpError') as TLinkedApiErrorType,
72+
response.error?.message ?? 'Failed to delete webhook',
73+
);
74+
}
75+
76+
public async deliveries(): Promise<Array<TWebhookDelivery>> {
77+
const response = await this.httpClient.post<{ deliveries: Array<TWebhookDelivery> }>(
78+
'/admin/webhook.deliveries',
79+
);
80+
if (response.success && response.result) {
81+
return response.result.deliveries;
82+
}
83+
throw new LinkedApiError(
84+
(response.error?.type ?? 'httpError') as TLinkedApiErrorType,
85+
response.error?.message ?? 'Failed to get webhook deliveries',
86+
);
87+
}
88+
89+
public async replayDelivery(params: TReplayWebhookDeliveryParams): Promise<void> {
90+
const response = await this.httpClient.post('/admin/webhook.replayDelivery', params);
91+
if (response.success) {
92+
return;
93+
}
94+
throw new LinkedApiError(
95+
(response.error?.type ?? 'httpError') as TLinkedApiErrorType,
96+
response.error?.message ?? 'Failed to replay webhook delivery',
97+
);
98+
}
99+
100+
public async sendTest(): Promise<void> {
101+
const response = await this.httpClient.post('/admin/webhook.sendTest');
102+
if (response.success) {
103+
return;
104+
}
105+
throw new LinkedApiError(
106+
(response.error?.type ?? 'httpError') as TLinkedApiErrorType,
107+
response.error?.message ?? 'Failed to send test webhook',
108+
);
109+
}
110+
}

src/admin/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AdminAccounts } from './admin-accounts';
55
import { buildAdminHttpClient } from './admin-http-client';
66
import { AdminLimits } from './admin-limits';
77
import { AdminSubscription } from './admin-subscription';
8+
import { AdminWebhooks } from './admin-webhooks';
89

910
/**
1011
* LinkedApiAdmin - Admin SDK for managing Linked API subscription, accounts, and limits.
@@ -25,12 +26,14 @@ import { AdminSubscription } from './admin-subscription';
2526
* const status = await admin.subscription.getStatus();
2627
* const { accounts } = await admin.accounts.getAll();
2728
* const { limits } = await admin.limits.getDefaults();
29+
* const webhook = await admin.webhooks.set({ url: 'https://example.com/hooks' });
2830
* ```
2931
*/
3032
export class LinkedApiAdmin {
3133
public readonly subscription: AdminSubscription;
3234
public readonly accounts: AdminAccounts;
3335
public readonly limits: AdminLimits;
36+
public readonly webhooks: AdminWebhooks;
3437

3538
constructor(config: TAdminConfig | HttpClient) {
3639
const httpClient =
@@ -39,5 +42,6 @@ export class LinkedApiAdmin {
3942
this.subscription = new AdminSubscription(httpClient);
4043
this.accounts = new AdminAccounts(httpClient);
4144
this.limits = new AdminLimits(httpClient);
45+
this.webhooks = new AdminWebhooks(httpClient);
4246
}
4347
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,3 +1206,4 @@ export type {
12061206
export * from './types';
12071207
export * from './operations';
12081208
export * from './core/operation';
1209+
export * from './webhooks';

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from './responses';
55
export * from './workflow-in-progress-response.type';
66
export * from './workflow-started-response.type';
77
export * from './workflows';
8+
export * from './webhooks';
89
export * from './actions/index';
910
export * from './http-client';
1011
export * from './admin/index';

src/types/webhooks.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
export type TWebhookPayloadMode = 'thin' | 'fat';
2+
3+
export type TWebhookEventType =
4+
| 'workflow.created'
5+
| 'workflow.started'
6+
| 'workflow.completed'
7+
| 'account.active'
8+
| 'account.reconnectionRequired'
9+
| 'account.frozen'
10+
| 'account.deleted'
11+
| 'webhook.test';
12+
13+
export type TWebhookDeliveryStatus = 'pending' | 'delivering' | 'success' | 'failed';
14+
15+
export type TWorkflowWebhookStatus = 'pending' | 'running' | 'completed' | 'failed';
16+
17+
export type TAccountWebhookStatus = 'active' | 'reconnection_required' | 'frozen' | 'deleted';
18+
19+
export interface TWebhookSubscription {
20+
id: string;
21+
url: string;
22+
payloadMode: TWebhookPayloadMode;
23+
isActive: boolean;
24+
createdAt: string;
25+
}
26+
27+
export interface TWebhookDelivery {
28+
id: string;
29+
eventType: TWebhookEventType;
30+
eventId: string;
31+
status: TWebhookDeliveryStatus;
32+
attempts: number;
33+
responseStatusCode: number | null;
34+
lastError: string | null;
35+
createdAt: string;
36+
updatedAt: string;
37+
}
38+
39+
export interface TSetWebhookParams {
40+
url: string;
41+
payloadMode?: TWebhookPayloadMode;
42+
}
43+
44+
export interface TSetWebhookPayloadModeParams {
45+
id: string;
46+
payloadMode: TWebhookPayloadMode;
47+
}
48+
49+
export interface TDeleteWebhookParams {
50+
id: string;
51+
}
52+
53+
export interface TReplayWebhookDeliveryParams {
54+
deliveryId: string;
55+
}
56+
57+
interface TWebhookEventBase {
58+
id: string;
59+
createdAt: string;
60+
}
61+
62+
export interface TWorkflowWebhookEvent extends TWebhookEventBase {
63+
type: 'workflow.created' | 'workflow.started' | 'workflow.completed';
64+
data: {
65+
workflowId: string;
66+
accountId: string;
67+
status: TWorkflowWebhookStatus;
68+
// Present only on workflow.completed delivered in `fat` payload mode; in `thin` mode fetch the
69+
// result via the workflow API by workflowId.
70+
result?: unknown;
71+
};
72+
}
73+
74+
export interface TAccountWebhookEvent extends TWebhookEventBase {
75+
type: 'account.active' | 'account.reconnectionRequired' | 'account.frozen' | 'account.deleted';
76+
data: {
77+
accountId: string;
78+
status: TAccountWebhookStatus;
79+
};
80+
}
81+
82+
export interface TWebhookTestEvent extends TWebhookEventBase {
83+
type: 'webhook.test';
84+
data: {
85+
message: string;
86+
};
87+
}
88+
89+
export type TWebhookEvent = TWorkflowWebhookEvent | TAccountWebhookEvent | TWebhookTestEvent;

src/webhooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './parse-webhook-event';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { TWebhookEvent } from '../types';
2+
3+
/**
4+
* Parse a raw webhook request body into a typed Linked API event.
5+
*
6+
* Pass the raw HTTP body (string or Buffer) exactly as received. Discriminate the returned event on
7+
* its `type` field to narrow `data`.
8+
*
9+
* @throws Error when the body is not valid JSON or is missing the `id` / `type` envelope fields.
10+
*
11+
* @example
12+
* ```typescript
13+
* const event = parseWebhookEvent(rawBody);
14+
* if (event.type === 'workflow.completed') {
15+
* console.log(event.data.workflowId, event.data.result);
16+
* }
17+
* ```
18+
*/
19+
export function parseWebhookEvent(rawBody: string | Buffer): TWebhookEvent {
20+
const text = typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');
21+
22+
let parsed: unknown;
23+
try {
24+
parsed = JSON.parse(text);
25+
} catch {
26+
throw new Error('Invalid webhook payload: body is not valid JSON');
27+
}
28+
29+
if (
30+
typeof parsed !== 'object' ||
31+
parsed === null ||
32+
typeof (parsed as { id?: unknown }).id !== 'string' ||
33+
typeof (parsed as { type?: unknown }).type !== 'string'
34+
) {
35+
throw new Error('Invalid webhook payload: missing "id" or "type"');
36+
}
37+
38+
return parsed as TWebhookEvent;
39+
}

0 commit comments

Comments
 (0)