| name | linear-webhooks-events |
| description | Configure and handle Linear webhooks for real-time event processing.
Use when setting up webhooks, handling Linear events,
or building real-time integrations.
Trigger with phrases like "linear webhooks", "linear events",
"linear real-time", "handle linear webhook", "linear webhook setup".
|
| allowed-tools | Read, Write, Edit, Bash(ngrok:*), Grep |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Linear Webhooks & Events
Overview
Set up and handle Linear webhooks for real-time event notifications.
Prerequisites
- Linear workspace admin access
- Public endpoint for webhook delivery
- Webhook signing secret configured
Available Event Types
| Event Type | Description |
|---|
Issue | Issue created, updated, or removed |
IssueComment | Comment added or updated |
Project | Project changes |
Cycle | Cycle (sprint) changes |
Label | Label changes |
Reaction | Emoji reactions |
Instructions
Step 1: Create Webhook Endpoint
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
export const config = {
api: {
bodyParser: false,
},
};
async function getRawBody(req: NextApiRequest): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
function verifySignature(payload: string, signature: string): boolean {
const secret = process.env.LINEAR_WEBHOOK_SECRET!;
const hmac = crypto.createHmac("sha256", secret);
const expectedSignature = hmac.(payload).();
crypto.(
.(signature),
.(expectedSignature)
);
}
() {
(req. !== ) {
res.().({ : });
}
rawBody = (req);
signature = req.[] ;
(!signature || !(rawBody, signature)) {
res.().({ : });
}
event = .(rawBody);
(event);
res.().({ : });
}
Step 2: Event Processing Router
interface LinearWebhookPayload {
action: "create" | "update" | "remove";
type: string;
data: Record<string, unknown>;
createdAt: string;
organizationId: string;
webhookTimestamp: number;
webhookId: string;
}
type EventHandler = (data: Record<string, unknown>, action: string) => Promise<void>;
const handlers: Record<string, EventHandler> = {
Issue: handleIssueEvent,
IssueComment: handleCommentEvent,
Project: handleProjectEvent,
Cycle: handleCycleEvent,
};
export async function processLinearEvent(payload: LinearWebhookPayload) {
const handler = handlers[payload.type];
if (!handler) {
.();
;
}
{
(payload., payload.);
} (error) {
.(, error);
error;
}
}
() {
issue = data {
: ;
: ;
: ;
: { : };
: ;
: { : };
};
.();
(action) {
:
(issue);
;
:
(issue);
;
:
(issue.);
;
}
}
() {
comment = data {
: ;
: ;
: { : };
: { : };
};
.();
}
() {
.(, data);
}
() {
.(, data);
}
Step 3: Business Logic Handlers
import { sendSlackNotification } from "./slack";
import { syncToDatabase } from "./database";
async function onIssueCreated(issue: any) {
await syncToDatabase("issues", issue.id, issue);
if (issue.priority <= 2) {
await sendSlackNotification({
channel: "#engineering-alerts",
text: `New high-priority issue: ${issue.identifier} - ${issue.title}`,
});
}
}
async function onIssueUpdated(issue: any) {
await syncToDatabase("issues", issue.id, issue);
if (issue.state?.name === "Done") {
await celebrateCompletion(issue);
}
}
async function onIssueRemoved() {
(, issueId, );
}
() {
.();
}
Step 4: Register Webhook in Linear
import { LinearClient } from "@linear/sdk";
async function createWebhook() {
const client = new LinearClient({
apiKey: process.env.LINEAR_API_KEY!,
});
const result = await client.createWebhook({
url: "https://your-domain.com/api/webhooks/linear",
label: "My Integration Webhook",
teamId: "your-team-id",
resourceTypes: ["Issue", "IssueComment", "Project"],
});
if (result.success) {
const webhook = await result.webhook;
console.log("Webhook created:", webhook?.id);
console.log("Secret (save this!):", webhook?.secret);
}
}
Step 5: Local Development with ngrok
npm run dev
ngrok http 3000
Step 6: Idempotent Event Processing
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
export async function processIdempotent(
webhookId: string,
processor: () => Promise<void>
): Promise<boolean> {
const key = `webhook:${webhookId}`;
const exists = await redis.exists(key);
if (exists) {
console.log(`Webhook ${webhookId} already processed, skipping`);
return false;
}
await redis.setex(key, 86400, "processing");
try {
await processor();
await redis.setex(key, 86400, "completed");
return true;
} catch (error) {
redis.(key);
error;
}
}
(payload., () => {
(payload);
});
Error Handling
| Error | Cause | Solution |
|---|
Invalid signature | Wrong secret or tampering | Verify webhook secret |
Timeout | Processing too slow | Use async queue |
Duplicate events | Webhook retry | Implement idempotency |
Missing data | Partial event | Handle gracefully |
Resources
Next Steps
Optimize performance with linear-performance-tuning.