or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

comments-attachments.mdcore-client.mderror-handling.mdindex.mdissue-management.mdpagination-connections.mdproject-management.mdteam-user-management.mdwebhook-processing.mdworkflow-cycle-management.md

webhook-processing.mddocs/

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