Guide to Linear Webhooks: Features and Best Practices
Linear webhooks notify your application when issues, comments, projects, and cycles change. If you're syncing Linear with another system, triggering automations, or mirroring issues, webhooks are how you react to changes without polling the API.
This guide covers how Linear webhooks work, the events you'll handle, how to verify the Linear-Signature, and the best practices for production.
What are Linear webhooks?
Linear webhooks are JSON POSTs delivered over HTTPS to a URL you register in your workspace settings. Each is signed with a Linear-Signature header: an HMAC-SHA256 over the raw request body, hex-encoded. Unlike GitHub, Linear does not prefix the value with sha256=, it's a bare hex string. The payload also carries a webhookTimestamp field (Unix milliseconds) so you can reject stale deliveries, which gives you replay protection out of the box.
Linear webhook features
| Feature | Details |
|---|---|
| Configuration | Workspace settings > API > Webhooks (or per OAuth app) |
| Signature header | Linear-Signature (bare hex, no sha256= prefix) |
| Signature scheme | HMAC-SHA256 (hex) over the raw body, keyed with the signing secret |
| Event header | Linear-Event (the entity type), Linear-Delivery (delivery UUID) |
| Replay protection | webhookTimestamp body field (Unix ms); reject if more than ~1 minute off |
| Delivery | Retried on failure; view and re-deliver from the webhook's page |
| SDK | None for verification (the @linear/sdk is GraphQL-focused); verify manually |
Common events
Linear identifies the entity type in the Linear-Event header and the payload's top-level type field. Data-change events carry an action of create, update (with an updatedFrom object of previous values), or remove.
| Event | Fires when |
|---|---|
Issue | An issue is created, updated, or removed |
Comment | A comment changes |
IssueLabel | A label changes |
Project / ProjectUpdate | A project or project update changes |
Cycle | A cycle changes |
Reaction, Document, Initiative, InitiativeUpdate | Those entities change |
Customer, CustomerRequest, User | Those entities change |
IssueSLA | Uses SLA-specific actions: set, highRisk, breached |
OAuthAppRevoked | Fires once when your app's authorization is revoked |
Note that IssueSLA doesn't use create/update/remove, it uses set/highRisk/breached, and OAuthAppRevoked fires once with neither.
Setting up Linear webhooks
Go to Workspace settings > API > Webhooks > Create new webhook (or, for an OAuth app, Settings > API > Applications > your app > Webhooks). Set a label, an HTTPS URL, and the resource types you want. The signing secret is displayed only once, copy it immediately into LINEAR_WEBHOOK_SECRET. If you lose it, delete and recreate the webhook to get a new one (this is also how you rotate).
Securing Linear webhooks
Verify against the raw body before parsing, compute an HMAC-SHA256 with your signing secret, hex-encode it, and compare in constant time. Then check that webhookTimestamp is within about a minute of now to reject replays.
const crypto = require("crypto");
const SECRET = process.env.LINEAR_WEBHOOK_SECRET;
const TIMESTAMP_TOLERANCE_MS = 60 * 1000;
function verify(rawBody, signatureHeader) {
if (!signatureHeader) return false;
const expected = crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
try {
return crypto.timingSafeEqual(
Buffer.from(signatureHeader, "hex"),
Buffer.from(expected, "hex")
);
} catch {
return false;
}
}
app.post("/webhooks/linear", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.headers["linear-signature"])) {
return res.sendStatus(401);
}
const payload = JSON.parse(req.body.toString("utf8"));
if (Math.abs(Date.now() - payload.webhookTimestamp) > TIMESTAMP_TOLERANCE_MS) {
return res.sendStatus(401); // stale or replayed
}
res.sendStatus(200); // acknowledge fast
processQueue.add(payload); // dedupe on Linear-Delivery, async
});
The same check in Python:
import hashlib
import hmac
import os
SECRET = os.environ["LINEAR_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, signature_header: str) -> bool:
if not signature_header:
return False
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature_header, expected)
Linear webhook limitations and pain points
The signature is bare hex with no prefix
The Problem: Linear-Signature is a bare hex string. Code that strips a sha256= prefix (as GitHub uses) or decodes base64 never matches.
Why It Happens: Linear hex-encodes the HMAC and doesn't prefix it.
Workarounds:
- Use the header value as-is, hex, over the raw body.
How Hookdeck Can Help: Hookdeck verifies the signature at the edge, so your app receives pre-verified events without matching header formats.
webhookTimestamp is in milliseconds
The Problem: The replay check compares webhookTimestamp to now, but it's in milliseconds, not seconds. Comparing against a seconds clock rejects everything.
Why It Happens: Linear reports the timestamp in Unix milliseconds.
Workarounds:
- Compare against
Date.now()(ms), and use an absolute skew so you also reject far-future timestamps.
How Hookdeck Can Help: Hookdeck enforces freshness at the edge, so your app receives events that are already within a sane window.
The secret is shown once
The Problem: Linear reveals the signing secret only at creation. Lose it and there's no way to view it again.
Why It Happens: Linear stores the secret write-only after creation.
Workarounds:
- Copy it into
LINEAR_WEBHOOK_SECRETimmediately; recreate the webhook to rotate.
How Hookdeck Can Help: Hookdeck holds the secret and verifies centrally, so it lives in one place.
Retries mean duplicates
The Problem: Linear retries failed deliveries, so the same event can arrive more than once. The signature proves authenticity, not uniqueness.
Why It Happens: At-least-once delivery with retries.
Workarounds:
- Dedupe on the
Linear-DeliveryUUID and make handlers idempotent.
How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, so retries don't double-process. See our guide to webhook idempotency.
Best practices
Verify HMAC-SHA256 over the raw body
Compute the hex HMAC over the raw body with your signing secret and compare against Linear-Signature in constant time.
Enforce the timestamp window
Reject deliveries whose webhookTimestamp is more than about a minute from now.
Dedupe on Linear-Delivery
Persist the delivery UUID and skip repeats so retries are safe.
Acknowledge fast, process asynchronously
Return 200 quickly and defer work to a queue. See why to process webhooks asynchronously.
Make Linear webhooks production-ready
Hookdeck verifies Linear-Signature, deduplicates, and durably queues every event
Conclusion
Linear webhooks are verified with a Linear-Signature HMAC-SHA256 over the raw body, hex-encoded with no prefix, and carry a webhookTimestamp (milliseconds) for replay protection. Verify over the raw body, enforce the timestamp window, dedupe on Linear-Delivery, and handle the broad set of entity events with their per-type actions.
Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.
Get started with Hookdeck for free and handle Linear webhooks reliably in minutes.