0
# Webhook Processing
1
2
Secure webhook handling system with signature verification, event parsing, and type-safe event handlers for integrating with Linear's webhook system.
3
4
## Capabilities
5
6
### LinearWebhookClient
7
8
Main client for processing and validating Linear webhooks with secure signature verification.
9
10
```typescript { .api }
11
/**
12
* Client for handling Linear webhook requests with helpers
13
*/
14
class LinearWebhookClient {
15
/**
16
* Creates a new LinearWebhookClient instance
17
* @param secret - The webhook signing secret from Linear webhook settings
18
*/
19
constructor(secret: string);
20
21
/**
22
* Creates a webhook handler function that can process Linear webhook requests
23
* @returns A webhook handler function with event registration capabilities
24
*/
25
createHandler(): LinearWebhookHandler;
26
27
/**
28
* Verify a webhook signature without processing the payload
29
* @param rawBody - Raw request body as Buffer
30
* @param signature - Signature from linear-signature header
31
* @param timestamp - Optional timestamp for replay protection
32
* @returns Whether the signature is valid
33
*/
34
verify(rawBody: Buffer, signature: string, timestamp?: number): boolean;
35
36
/**
37
* Parse and verify webhook payload data
38
* @param rawBody - Raw request body as Buffer
39
* @param signature - Signature from linear-signature header
40
* @param timestamp - Optional timestamp for replay protection
41
* @returns Parsed and verified webhook payload
42
*/
43
parseData(rawBody: Buffer, signature: string, timestamp?: number): LinearWebhookPayload;
44
}
45
```
46
47
### Webhook Handler
48
49
Universal webhook handler supporting both Fetch API and Node.js HTTP environments.
50
51
```typescript { .api }
52
/**
53
* Webhook handler with event registration capabilities
54
* Supports both Fetch API and Node.js HTTP interfaces
55
*/
56
interface LinearWebhookHandler {
57
/**
58
* Handle Fetch API requests (for modern runtimes, Cloudflare Workers, etc.)
59
* @param request - Fetch API Request object
60
* @returns Promise resolving to Response object
61
*/
62
(request: Request): Promise<Response>;
63
64
/**
65
* Handle Node.js HTTP requests (for Express, Next.js API routes, etc.)
66
* @param request - Node.js IncomingMessage
67
* @param response - Node.js ServerResponse
68
* @returns Promise resolving when response is sent
69
*/
70
(request: IncomingMessage, response: ServerResponse): Promise<void>;
71
72
/**
73
* Register an event handler for specific webhook event types
74
* @param eventType - The webhook event type to listen for
75
* @param handler - Function to handle the event
76
*/
77
on<T extends LinearWebhookEventType>(
78
eventType: T,
79
handler: LinearWebhookEventHandler<Extract<LinearWebhookPayload, { type: T }>>
80
): void;
81
82
/**
83
* Register a wildcard event handler that receives all webhook events
84
* @param eventType - Use "*" to listen for all webhook events
85
* @param handler - Function to handle any webhook event
86
*/
87
on(eventType: "*", handler: LinearWebhookEventHandler<LinearWebhookPayload>): void;
88
89
/**
90
* Remove an event handler for specific webhook event types
91
* @param eventType - The webhook event type to stop listening for
92
* @param handler - The specific handler function to remove
93
*/
94
off<T extends LinearWebhookEventType>(
95
eventType: T,
96
handler: LinearWebhookEventHandler<Extract<LinearWebhookPayload, { type: T }>>
97
): void;
98
99
/**
100
* Remove a wildcard event handler that receives all webhook events
101
* @param eventType - Use "*" to stop listening for all webhook events
102
* @param handler - The specific wildcard handler function to remove
103
*/
104
off(eventType: "*", handler: LinearWebhookEventHandler<LinearWebhookPayload>): void;
105
106
/**
107
* Remove all event listeners for a given event type or all events
108
* @param eventType - Optional specific event type to clear
109
*/
110
removeAllListeners(eventType?: LinearWebhookEventType): void;
111
}
112
113
/**
114
* Event handler function type for webhook events
115
*/
116
type LinearWebhookEventHandler<T extends LinearWebhookPayload> = (payload: T) => void | Promise<void>;
117
```
118
119
**Usage Examples:**
120
121
```typescript
122
import { LinearWebhookClient } from "@linear/sdk/webhooks";
123
import type { IncomingMessage, ServerResponse } from "http";
124
125
// Initialize webhook client with signing secret
126
const webhookClient = new LinearWebhookClient("your-webhook-signing-secret");
127
128
// Create handler for Fetch API (Cloudflare Workers, Deno, etc.)
129
const handler = webhookClient.createHandler();
130
131
// Register event handlers
132
handler.on("Issue", async (payload) => {
133
console.log("Issue event:", payload.action, payload.data.title);
134
135
if (payload.action === "create") {
136
console.log("New issue created:", payload.data.id);
137
// Send notification, update external system, etc.
138
}
139
});
140
141
handler.on("Comment", async (payload) => {
142
console.log("Comment event:", payload.action, payload.data.body);
143
});
144
145
// Use with Fetch API
146
export default {
147
async fetch(request: Request): Promise<Response> {
148
if (request.method === "POST" && request.url.endsWith("/webhooks")) {
149
return await handler(request);
150
}
151
return new Response("Not found", { status: 404 });
152
}
153
};
154
155
// Use with Node.js HTTP (Express example)
156
import express from "express";
157
158
const app = express();
159
160
app.use(express.raw({ type: "application/json" }));
161
162
app.post("/webhooks", async (req: IncomingMessage, res: ServerResponse) => {
163
await handler(req, res);
164
});
165
```
166
167
### Webhook Event Types
168
169
Complete enumeration of Linear webhook event types and their payloads.
170
171
```typescript { .api }
172
/**
173
* Union type of all Linear webhook event types
174
*/
175
type LinearWebhookEventType =
176
| "Issue"
177
| "Comment"
178
| "Project"
179
| "ProjectUpdate"
180
| "Cycle"
181
| "User"
182
| "Team"
183
| "Attachment"
184
| "IssueLabel"
185
| "Document"
186
| "Initiative"
187
| "InitiativeUpdate"
188
| "Reaction"
189
| "Customer"
190
| "CustomerNeed"
191
| "AuditEntry"
192
| "IssueSLA"
193
| "OAuthApp"
194
| "AppUserNotification"
195
| "PermissionChange"
196
| "AgentSessionEvent";
197
198
/**
199
* Union type of all webhook payload types
200
*/
201
type LinearWebhookPayload =
202
| EntityWebhookPayloadWithIssueData
203
| EntityWebhookPayloadWithCommentData
204
| EntityWebhookPayloadWithProjectData
205
| EntityWebhookPayloadWithProjectUpdateData
206
| EntityWebhookPayloadWithCycleData
207
| EntityWebhookPayloadWithUserData
208
| EntityWebhookPayloadWithTeamData
209
| EntityWebhookPayloadWithAttachmentData
210
| EntityWebhookPayloadWithIssueLabelData
211
| EntityWebhookPayloadWithDocumentData
212
| EntityWebhookPayloadWithInitiativeData
213
| EntityWebhookPayloadWithInitiativeUpdateData
214
| EntityWebhookPayloadWithReactionData
215
| EntityWebhookPayloadWithCustomerData
216
| EntityWebhookPayloadWithCustomerNeedData
217
| EntityWebhookPayloadWithAuditEntryData
218
| EntityWebhookPayloadWithUnknownEntityData
219
| AppUserNotificationWebhookPayloadWithNotification
220
| AppUserTeamAccessChangedWebhookPayload
221
| IssueSlaWebhookPayload
222
| OAuthAppWebhookPayload
223
| AgentSessionEventWebhookPayload;
224
225
/**
226
* Base webhook payload structure for entity events
227
*/
228
interface EntityWebhookPayloadWithEntityData<T = unknown> {
229
/** Webhook event action */
230
action: "create" | "update" | "remove";
231
/** Entity data */
232
data: T;
233
/** Webhook event type */
234
type: LinearWebhookEventType;
235
/** Organization ID */
236
organizationId: string;
237
/** Webhook delivery timestamp */
238
createdAt: DateTime;
239
/** Webhook delivery attempt number */
240
webhookTimestamp: number;
241
}
242
243
/**
244
* Issue webhook payload
245
*/
246
interface EntityWebhookPayloadWithIssueData extends EntityWebhookPayloadWithEntityData<Issue> {
247
type: "Issue";
248
data: Issue;
249
}
250
251
/**
252
* Comment webhook payload
253
*/
254
interface EntityWebhookPayloadWithCommentData extends EntityWebhookPayloadWithEntityData<Comment> {
255
type: "Comment";
256
data: Comment;
257
}
258
259
/**
260
* Project webhook payload
261
*/
262
interface EntityWebhookPayloadWithProjectData extends EntityWebhookPayloadWithEntityData<Project> {
263
type: "Project";
264
data: Project;
265
}
266
267
/**
268
* Team webhook payload
269
*/
270
interface EntityWebhookPayloadWithTeamData extends EntityWebhookPayloadWithEntityData<Team> {
271
type: "Team";
272
data: Team;
273
}
274
275
/**
276
* Unknown entity webhook payload for entities that don't have specific typed data
277
*/
278
interface EntityWebhookPayloadWithUnknownEntityData {
279
action: "create" | "update" | "remove";
280
type: LinearWebhookEventType;
281
organizationId: string;
282
createdAt: DateTime;
283
data: Record<string, unknown>;
284
}
285
286
/**
287
* App user team access changed webhook payload
288
*/
289
interface AppUserTeamAccessChangedWebhookPayload {
290
type: "PermissionChange";
291
action: "create" | "update" | "remove";
292
organizationId: string;
293
createdAt: DateTime;
294
data: {
295
userId: string;
296
teamId: string;
297
access: string;
298
};
299
}
300
301
/**
302
* Issue SLA webhook payload
303
*/
304
interface IssueSlaWebhookPayload {
305
type: "IssueSLA";
306
action: "create" | "update" | "remove";
307
organizationId: string;
308
createdAt: DateTime;
309
data: {
310
issueId: string;
311
slaBreachedAt?: DateTime;
312
};
313
}
314
315
/**
316
* OAuth app webhook payload
317
*/
318
interface OAuthAppWebhookPayload {
319
type: "OAuthApp";
320
action: "create" | "update" | "remove";
321
organizationId: string;
322
createdAt: DateTime;
323
data: {
324
appId: string;
325
name: string;
326
status: string;
327
};
328
}
329
330
/**
331
* Agent session event webhook payload
332
*/
333
interface AgentSessionEventWebhookPayload {
334
type: "AgentSessionEvent";
335
action: "create" | "update" | "remove";
336
organizationId: string;
337
createdAt: DateTime;
338
data: {
339
sessionId: string;
340
userId: string;
341
event: string;
342
metadata: Record<string, unknown>;
343
};
344
}
345
```
346
347
### Webhook Constants
348
349
Constants for webhook processing and header validation.
350
351
```typescript { .api }
352
/** Header name containing the webhook signature */
353
const LINEAR_WEBHOOK_SIGNATURE_HEADER = "linear-signature";
354
355
/** Field name in payload containing the webhook timestamp */
356
const LINEAR_WEBHOOK_TS_FIELD = "webhookTimestamp";
357
```
358
359
### Advanced Webhook Patterns
360
361
**Multi-Event Handler:**
362
363
```typescript
364
import { LinearWebhookClient } from "@linear/sdk/webhooks";
365
366
const webhookClient = new LinearWebhookClient("your-secret");
367
const handler = webhookClient.createHandler();
368
369
// Handle multiple issue actions
370
handler.on("Issue", async (payload) => {
371
const { action, data: issue } = payload;
372
373
switch (action) {
374
case "create":
375
await notifyTeam(`New issue: ${issue.title}`, issue);
376
await createJiraTicket(issue);
377
break;
378
379
case "update":
380
if (issue.completedAt) {
381
await sendCompletionMetrics(issue);
382
}
383
break;
384
385
case "remove":
386
await cleanupExternalResources(issue.id);
387
break;
388
}
389
});
390
391
// Handle project lifecycle
392
handler.on("Project", async (payload) => {
393
if (payload.action === "create") {
394
await setupProjectResources(payload.data);
395
} else if (payload.action === "update" && payload.data.completedAt) {
396
await generateProjectReport(payload.data);
397
}
398
});
399
```
400
401
**Error Handling in Webhooks:**
402
403
```typescript
404
const handler = webhookClient.createHandler();
405
406
handler.on("Issue", async (payload) => {
407
try {
408
await processIssueEvent(payload);
409
} catch (error) {
410
console.error("Failed to process issue webhook:", error);
411
// Log to monitoring service
412
await logWebhookError("Issue", payload.data.id, error);
413
// Don't throw - webhook should return 200 to avoid retries
414
}
415
});
416
417
// Graceful error handling wrapper
418
function withErrorHandling<T extends LinearWebhookPayload>(
419
handler: LinearWebhookEventHandler<T>
420
): LinearWebhookEventHandler<T> {
421
return async (payload) => {
422
try {
423
await handler(payload);
424
} catch (error) {
425
console.error(`Webhook handler error for ${payload.type}:`, error);
426
// Optionally report to error tracking service
427
}
428
};
429
}
430
431
handler.on("Comment", withErrorHandling(async (payload) => {
432
await processCommentEvent(payload);
433
}));
434
```
435
436
**Manual Verification (for custom implementations):**
437
438
```typescript
439
import { LinearWebhookClient } from "@linear/sdk/webhooks";
440
441
const webhookClient = new LinearWebhookClient("your-secret");
442
443
// Custom webhook endpoint
444
app.post("/custom-webhook", express.raw({ type: "application/json" }), (req, res) => {
445
const signature = req.headers["linear-signature"] as string;
446
const rawBody = req.body;
447
448
// Verify signature
449
if (!webhookClient.verify(rawBody, signature)) {
450
return res.status(401).json({ error: "Invalid signature" });
451
}
452
453
// Parse payload
454
try {
455
const payload = webhookClient.parseData(rawBody, signature);
456
457
// Process based on event type
458
switch (payload.type) {
459
case "Issue":
460
handleIssueEvent(payload);
461
break;
462
case "Comment":
463
handleCommentEvent(payload);
464
break;
465
default:
466
console.log("Unhandled event type:", payload.type);
467
}
468
469
res.status(200).json({ success: true });
470
} catch (error) {
471
console.error("Webhook processing error:", error);
472
res.status(400).json({ error: "Invalid webhook payload" });
473
}
474
});
475
```
476
477
**Wildcard Event Handler:**
478
479
```typescript
480
import { LinearWebhookClient } from "@linear/sdk/webhooks";
481
482
const webhookClient = new LinearWebhookClient("your-secret");
483
const handler = webhookClient.createHandler();
484
485
// Listen for all webhook events using wildcard
486
handler.on("*", async (payload) => {
487
console.log(`Received ${payload.type} event:`, {
488
action: payload.action,
489
organizationId: payload.organizationId,
490
timestamp: payload.createdAt
491
});
492
493
// Log all events for analytics
494
await logWebhookEvent(payload.type, payload.action, payload.organizationId);
495
496
// Route to different handlers based on event type
497
switch (payload.type) {
498
case "Issue":
499
case "Comment":
500
case "Project":
501
await sendToSlack(payload);
502
break;
503
case "User":
504
case "Team":
505
await syncWithCRM(payload);
506
break;
507
default:
508
await logUnhandledEvent(payload);
509
}
510
});
511
512
// You can still use specific handlers alongside wildcard handlers
513
// Specific handlers will receive events in addition to wildcard handlers
514
handler.on("Issue", async (payload) => {
515
// This will be called for Issue events along with the wildcard handler
516
await updateJiraIntegration(payload);
517
});
518
```
519
520
## Webhook Security
521
522
**Best Practices:**
523
524
1. **Always verify signatures** before processing webhook payloads
525
2. **Use HTTPS endpoints** to protect webhook data in transit
526
3. **Implement replay protection** using webhook timestamps
527
4. **Handle errors gracefully** to avoid unnecessary retries
528
5. **Rate limit webhook endpoints** to prevent abuse
529
6. **Log webhook events** for debugging and audit purposes
530
531
**Signature Verification Process:**
532
533
Linear webhooks use HMAC-SHA256 signatures for verification. The SDK handles this automatically, but the verification process includes:
534
535
1. Extract timestamp from `webhookTimestamp` field in payload
536
2. Create signature string from timestamp and raw body
537
3. Compute HMAC-SHA256 hash using webhook secret
538
4. Compare computed signature with provided signature
539
5. Optional: Check timestamp to prevent replay attacks