Gareth Wilson Gareth Wilson

Guide to Linear Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationWorkspace settings > API > Webhooks (or per OAuth app)
Signature headerLinear-Signature (bare hex, no sha256= prefix)
Signature schemeHMAC-SHA256 (hex) over the raw body, keyed with the signing secret
Event headerLinear-Event (the entity type), Linear-Delivery (delivery UUID)
Replay protectionwebhookTimestamp body field (Unix ms); reject if more than ~1 minute off
DeliveryRetried on failure; view and re-deliver from the webhook's page
SDKNone 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.

EventFires when
IssueAn issue is created, updated, or removed
CommentA comment changes
IssueLabelA label changes
Project / ProjectUpdateA project or project update changes
CycleA cycle changes
Reaction, Document, Initiative, InitiativeUpdateThose entities change
Customer, CustomerRequest, UserThose entities change
IssueSLAUses SLA-specific actions: set, highRisk, breached
OAuthAppRevokedFires 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_SECRET immediately; 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-Delivery UUID 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.