CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-linear--sdk

The Linear Client SDK for interacting with the Linear GraphQL API

Overview
Eval results
Files

webhook-processing.mddocs/

Webhook Processing

Secure webhook handling system with signature verification, event parsing, and type-safe event handlers for integrating with Linear's webhook system.

Capabilities

LinearWebhookClient

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;
}

Webhook Handler

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);
});

Webhook Event Types

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>;
  };
}

Webhook Constants

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";

Advanced Webhook Patterns

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);
});

Webhook Security

Best Practices:

  1. Always verify signatures before processing webhook payloads
  2. Use HTTPS endpoints to protect webhook data in transit
  3. Implement replay protection using webhook timestamps
  4. Handle errors gracefully to avoid unnecessary retries
  5. Rate limit webhook endpoints to prevent abuse
  6. Log webhook events for debugging and audit purposes

Signature Verification Process:

Linear webhooks use HMAC-SHA256 signatures for verification. The SDK handles this automatically, but the verification process includes:

  1. Extract timestamp from webhookTimestamp field in payload
  2. Create signature string from timestamp and raw body
  3. Compute HMAC-SHA256 hash using webhook secret
  4. Compare computed signature with provided signature
  5. Optional: Check timestamp to prevent replay attacks

Install with Tessl CLI

npx tessl i tessl/npm-linear--sdk

docs

comments-attachments.md

core-client.md

error-handling.md

index.md

issue-management.md

pagination-connections.md

project-management.md

team-user-management.md

webhook-processing.md

workflow-cycle-management.md

tile.json