Linear Webhooks
Reference: https://linear.app/developers/webhooks
Signature verification
Linear signs every delivery with HMAC-SHA256:
Linear-Signature: <hex digest>
Verify in constant time:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyLinearSignature(rawBody: Buffer, signature: string, secret: string): boolean {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signature, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
Always read the raw body bytes, not the parsed JSON. Express:
app.use("/linear/webhook", express.raw({ type: "application/json" }));
Replay protection
Each delivery has a webhookTimestamp field in the JSON body (Unix ms). Reject events older than 5 minutes:
if (Math.abs(Date.now() - body.webhookTimestamp) > 5 * 60_000) reject();
Idempotency
Linear may re-deliver. Each event has:
delivery.id— unique per delivery (use this!)data.id— entity ID
Store seen delivery.id in Redis with 7-day TTL; ignore duplicates.
Resource types
Issue, IssueLabel, Comment, Cycle, Project, ProjectUpdate, Initiative, InitiativeUpdate, Customer, CustomerNeed, Reaction, Attachment, Document.
Subscribe selectively — fewer types means smaller event volume.
Action types
create | update | remove. Some resources support more; consult the schema.
Body shape
{
"action": "update",
"actor": { "id": "...", "name": "..." },
"createdAt": "2026-04-30T12:00:00.000Z",
"data": { /* the resource */ },
"type": "Issue",
"url": "https://linear.app/...",
"webhookTimestamp": 1714478400000,
"webhookId": "...",
"delivery": { "id": "..." }
}
Re-fetch on demand
Don't trust webhook payload state for reads. Linear may send out-of-order events. After receiving an Issue update, re-fetch via GraphQL using the id to get the canonical state.
Dead-Letter Queue
Implementation in lib/webhook-dlq.ts:
- After 3 failed processings, write to DLQ table with: delivery ID, payload, error, attempts
/linear:webhook dlqlists;/linear:webhook replay --since 24hretries from DLQ- Alert (Slack / PagerDuty) when DLQ depth > 10
Local testing
Use ngrok http 3000 and set the public URL as the webhook URL. Linear has no built-in test-replay UI; use webhookTest mutation if available, or the DLQ replay path.
Webhook security checklist
- HTTPS only
- Signature verified before any body parsing beyond raw read
- 5-minute timestamp window
-
delivery.ididempotency - Re-fetch authoritative state via GraphQL
- DLQ with bounded retry
- Webhook secret rotated yearly