The Linear Client SDK for interacting with the Linear GraphQL API
Secure webhook handling system with signature verification, event parsing, and type-safe event handlers for integrating with Linear's webhook system.
Main client for processing and validating Linear webhooks with secure signature verification.
/**
* Client for handling Linear webhook requests with helpers
*/
class LinearWebhookClient {
/**
* Creates a new LinearWebhookClient instance
* @param secret - The webhook signing secret from Linear webhook settings
*/
constructor(secret: string);
/**
* Creates a webhook handler function that can process Linear webhook requests
* @returns A webhook handler function with event registration capabilities
*/
createHandler(): LinearWebhookHandler;
/**
* Verify a webhook signature without processing the payload
* @param rawBody - Raw request body as Buffer
* @param signature - Signature from linear-signature header
* @param timestamp - Optional timestamp for replay protection
* @returns Whether the signature is valid
*/
verify(rawBody: Buffer, signature: string, timestamp?: number): boolean;
/**
* Parse and verify webhook payload data
* @param rawBody - Raw request body as Buffer
* @param signature - Signature from linear-signature header
* @param timestamp - Optional timestamp for replay protection
* @returns Parsed and verified webhook payload
*/
parseData(rawBody: Buffer, signature: string, timestamp?: number): LinearWebhookPayload;
}Universal webhook handler supporting both Fetch API and Node.js HTTP environments.
/**
* Webhook handler with event registration capabilities
* Supports both Fetch API and Node.js HTTP interfaces
*/
interface LinearWebhookHandler {
/**
* Handle Fetch API requests (for modern runtimes, Cloudflare Workers, etc.)
* @param request - Fetch API Request object
* @returns Promise resolving to Response object
*/
(request: Request): Promise<Response>;
/**
* Handle Node.js HTTP requests (for Express, Next.js API routes, etc.)
* @param request - Node.js IncomingMessage
* @param response - Node.js ServerResponse
* @returns Promise resolving when response is sent
*/
(request: IncomingMessage, response: ServerResponse): Promise<void>;
/**
* Register an event handler for specific webhook event types
* @param eventType - The webhook event type to listen for
* @param handler - Function to handle the event
*/
on<T extends LinearWebhookEventType>(
eventType: T,
handler: LinearWebhookEventHandler<Extract<LinearWebhookPayload, { type: T }>>
): void;
/**
* Register a wildcard event handler that receives all webhook events
* @param eventType - Use "*" to listen for all webhook events
* @param handler - Function to handle any webhook event
*/
on(eventType: "*", handler: LinearWebhookEventHandler<LinearWebhookPayload>): void;
/**
* Remove an event handler for specific webhook event types
* @param eventType - The webhook event type to stop listening for
* @param handler - The specific handler function to remove
*/
off<T extends LinearWebhookEventType>(
eventType: T,
handler: LinearWebhookEventHandler<Extract<LinearWebhookPayload, { type: T }>>
): void;
/**
* Remove a wildcard event handler that receives all webhook events
* @param eventType - Use "*" to stop listening for all webhook events
* @param handler - The specific wildcard handler function to remove
*/
off(eventType: "*", handler: LinearWebhookEventHandler<LinearWebhookPayload>): void;
/**
* Remove all event listeners for a given event type or all events
* @param eventType - Optional specific event type to clear
*/
removeAllListeners(eventType?: LinearWebhookEventType): void;
}
/**
* Event handler function type for webhook events
*/
type LinearWebhookEventHandler<T extends LinearWebhookPayload> = (payload: T) => void | Promise<void>;Usage Examples:
import { LinearWebhookClient } from "@linear/sdk/webhooks";
import type { IncomingMessage, ServerResponse } from "http";
// Initialize webhook client with signing secret
const webhookClient = new LinearWebhookClient("your-webhook-signing-secret");
// Create handler for Fetch API (Cloudflare Workers, Deno, etc.)
const handler = webhookClient.createHandler();
// Register event handlers
handler.on("Issue", async (payload) => {
console.log("Issue event:", payload.action, payload.data.title);
if (payload.action === "create") {
console.log("New issue created:", payload.data.id);
// Send notification, update external system, etc.
}
});
handler.on("Comment", async (payload) => {
console.log("Comment event:", payload.action, payload.data.body);
});
// Use with Fetch API
export default {
async fetch(request: Request): Promise<Response> {
if (request.method === "POST" && request.url.endsWith("/webhooks")) {
return await handler(request);
}
return new Response("Not found", { status: 404 });
}
};
// Use with Node.js HTTP (Express example)
import express from "express";
const app = express();
app.use(express.raw({ type: "application/json" }));
app.post("/webhooks", async (req: IncomingMessage, res: ServerResponse) => {
await handler(req, res);
});Complete enumeration of Linear webhook event types and their payloads.
/**
* Union type of all Linear webhook event types
*/
type LinearWebhookEventType =
| "Issue"
| "Comment"
| "Project"
| "ProjectUpdate"
| "Cycle"
| "User"
| "Team"
| "Attachment"
| "IssueLabel"
| "Document"
| "Initiative"
| "InitiativeUpdate"
| "Reaction"
| "Customer"
| "CustomerNeed"
| "AuditEntry"
| "IssueSLA"
| "OAuthApp"
| "AppUserNotification"
| "PermissionChange"
| "AgentSessionEvent";
/**
* Union type of all webhook payload types
*/
type LinearWebhookPayload =
| EntityWebhookPayloadWithIssueData
| EntityWebhookPayloadWithCommentData
| EntityWebhookPayloadWithProjectData
| EntityWebhookPayloadWithProjectUpdateData
| EntityWebhookPayloadWithCycleData
| EntityWebhookPayloadWithUserData
| EntityWebhookPayloadWithTeamData
| EntityWebhookPayloadWithAttachmentData
| EntityWebhookPayloadWithIssueLabelData
| EntityWebhookPayloadWithDocumentData
| EntityWebhookPayloadWithInitiativeData
| EntityWebhookPayloadWithInitiativeUpdateData
| EntityWebhookPayloadWithReactionData
| EntityWebhookPayloadWithCustomerData
| EntityWebhookPayloadWithCustomerNeedData
| EntityWebhookPayloadWithAuditEntryData
| EntityWebhookPayloadWithUnknownEntityData
| AppUserNotificationWebhookPayloadWithNotification
| AppUserTeamAccessChangedWebhookPayload
| IssueSlaWebhookPayload
| OAuthAppWebhookPayload
| AgentSessionEventWebhookPayload;
/**
* Base webhook payload structure for entity events
*/
interface EntityWebhookPayloadWithEntityData<T = unknown> {
/** Webhook event action */
action: "create" | "update" | "remove";
/** Entity data */
data: T;
/** Webhook event type */
type: LinearWebhookEventType;
/** Organization ID */
organizationId: string;
/** Webhook delivery timestamp */
createdAt: DateTime;
/** Webhook delivery attempt number */
webhookTimestamp: number;
}
/**
* Issue webhook payload
*/
interface EntityWebhookPayloadWithIssueData extends EntityWebhookPayloadWithEntityData<Issue> {
type: "Issue";
data: Issue;
}
/**
* Comment webhook payload
*/
interface EntityWebhookPayloadWithCommentData extends EntityWebhookPayloadWithEntityData<Comment> {
type: "Comment";
data: Comment;
}
/**
* Project webhook payload
*/
interface EntityWebhookPayloadWithProjectData extends EntityWebhookPayloadWithEntityData<Project> {
type: "Project";
data: Project;
}
/**
* Team webhook payload
*/
interface EntityWebhookPayloadWithTeamData extends EntityWebhookPayloadWithEntityData<Team> {
type: "Team";
data: Team;
}
/**
* Unknown entity webhook payload for entities that don't have specific typed data
*/
interface EntityWebhookPayloadWithUnknownEntityData {
action: "create" | "update" | "remove";
type: LinearWebhookEventType;
organizationId: string;
createdAt: DateTime;
data: Record<string, unknown>;
}
/**
* App user team access changed webhook payload
*/
interface AppUserTeamAccessChangedWebhookPayload {
type: "PermissionChange";
action: "create" | "update" | "remove";
organizationId: string;
createdAt: DateTime;
data: {
userId: string;
teamId: string;
access: string;
};
}
/**
* Issue SLA webhook payload
*/
interface IssueSlaWebhookPayload {
type: "IssueSLA";
action: "create" | "update" | "remove";
organizationId: string;
createdAt: DateTime;
data: {
issueId: string;
slaBreachedAt?: DateTime;
};
}
/**
* OAuth app webhook payload
*/
interface OAuthAppWebhookPayload {
type: "OAuthApp";
action: "create" | "update" | "remove";
organizationId: string;
createdAt: DateTime;
data: {
appId: string;
name: string;
status: string;
};
}
/**
* Agent session event webhook payload
*/
interface AgentSessionEventWebhookPayload {
type: "AgentSessionEvent";
action: "create" | "update" | "remove";
organizationId: string;
createdAt: DateTime;
data: {
sessionId: string;
userId: string;
event: string;
metadata: Record<string, unknown>;
};
}Constants for webhook processing and header validation.
/** Header name containing the webhook signature */
const LINEAR_WEBHOOK_SIGNATURE_HEADER = "linear-signature";
/** Field name in payload containing the webhook timestamp */
const LINEAR_WEBHOOK_TS_FIELD = "webhookTimestamp";Multi-Event Handler:
import { LinearWebhookClient } from "@linear/sdk/webhooks";
const webhookClient = new LinearWebhookClient("your-secret");
const handler = webhookClient.createHandler();
// Handle multiple issue actions
handler.on("Issue", async (payload) => {
const { action, data: issue } = payload;
switch (action) {
case "create":
await notifyTeam(`New issue: ${issue.title}`, issue);
await createJiraTicket(issue);
break;
case "update":
if (issue.completedAt) {
await sendCompletionMetrics(issue);
}
break;
case "remove":
await cleanupExternalResources(issue.id);
break;
}
});
// Handle project lifecycle
handler.on("Project", async (payload) => {
if (payload.action === "create") {
await setupProjectResources(payload.data);
} else if (payload.action === "update" && payload.data.completedAt) {
await generateProjectReport(payload.data);
}
});Error Handling in Webhooks:
const handler = webhookClient.createHandler();
handler.on("Issue", async (payload) => {
try {
await processIssueEvent(payload);
} catch (error) {
console.error("Failed to process issue webhook:", error);
// Log to monitoring service
await logWebhookError("Issue", payload.data.id, error);
// Don't throw - webhook should return 200 to avoid retries
}
});
// Graceful error handling wrapper
function withErrorHandling<T extends LinearWebhookPayload>(
handler: LinearWebhookEventHandler<T>
): LinearWebhookEventHandler<T> {
return async (payload) => {
try {
await handler(payload);
} catch (error) {
console.error(`Webhook handler error for ${payload.type}:`, error);
// Optionally report to error tracking service
}
};
}
handler.on("Comment", withErrorHandling(async (payload) => {
await processCommentEvent(payload);
}));Manual Verification (for custom implementations):
import { LinearWebhookClient } from "@linear/sdk/webhooks";
const webhookClient = new LinearWebhookClient("your-secret");
// Custom webhook endpoint
app.post("/custom-webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["linear-signature"] as string;
const rawBody = req.body;
// Verify signature
if (!webhookClient.verify(rawBody, signature)) {
return res.status(401).json({ error: "Invalid signature" });
}
// Parse payload
try {
const payload = webhookClient.parseData(rawBody, signature);
// Process based on event type
switch (payload.type) {
case "Issue":
handleIssueEvent(payload);
break;
case "Comment":
handleCommentEvent(payload);
break;
default:
console.log("Unhandled event type:", payload.type);
}
res.status(200).json({ success: true });
} catch (error) {
console.error("Webhook processing error:", error);
res.status(400).json({ error: "Invalid webhook payload" });
}
});Wildcard Event Handler:
import { LinearWebhookClient } from "@linear/sdk/webhooks";
const webhookClient = new LinearWebhookClient("your-secret");
const handler = webhookClient.createHandler();
// Listen for all webhook events using wildcard
handler.on("*", async (payload) => {
console.log(`Received ${payload.type} event:`, {
action: payload.action,
organizationId: payload.organizationId,
timestamp: payload.createdAt
});
// Log all events for analytics
await logWebhookEvent(payload.type, payload.action, payload.organizationId);
// Route to different handlers based on event type
switch (payload.type) {
case "Issue":
case "Comment":
case "Project":
await sendToSlack(payload);
break;
case "User":
case "Team":
await syncWithCRM(payload);
break;
default:
await logUnhandledEvent(payload);
}
});
// You can still use specific handlers alongside wildcard handlers
// Specific handlers will receive events in addition to wildcard handlers
handler.on("Issue", async (payload) => {
// This will be called for Issue events along with the wildcard handler
await updateJiraIntegration(payload);
});Best Practices:
Signature Verification Process:
Linear webhooks use HMAC-SHA256 signatures for verification. The SDK handles this automatically, but the verification process includes:
webhookTimestamp field in payloadInstall with Tessl CLI
npx tessl i tessl/npm-linear--sdk