# Linear Webhooks

## When to Use This Skill

* Setting up Linear webhook handlers
* Debugging Linear signature verification failures
* Validating the `Linear-Signature` HMAC-SHA256 header
* Handling Linear `Issue`, `Comment`, `Project`, `Cycle`, `IssueLabel`, or `IssueSLA` events
* Reacting to `create`, `update`, and `remove` actions on Linear entities
* Rejecting stale webhook deliveries via the `webhookTimestamp` field

## Essential Code (USE THIS)

### Linear Signature Verification (JavaScript)

Linear signs each webhook with HMAC-SHA256 over the raw request body, hex-encoded, sent in the `Linear-Signature` header. Linear has no first-party Node SDK helper for verifying webhooks, so manual verification is the recommended approach.

```javascript
const crypto = require('crypto');

function verifyLinearWebhook(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;

  // HMAC-SHA256(rawBody, secret) → hex
  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;
  }
}

// Reject deliveries older than 1 minute (replay protection)
function isFreshTimestamp(webhookTimestamp) {
  if (typeof webhookTimestamp !== 'number') return false;
  const skewMs = Math.abs(Date.now() - webhookTimestamp);
  return skewMs <= 60 * 1000;
}

```

### Express Webhook Handler

```javascript
const express = require('express');
const app = express();

// CRITICAL: Use express.raw() - Linear signs the raw body
app.post('/webhooks/linear',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['linear-signature'];
    const event = req.headers['linear-event'];      // e.g. "Issue", "Comment"
    const delivery = req.headers['linear-delivery']; // UUID for idempotency

    if (!verifyLinearWebhook(req.body, signature, process.env.LINEAR_WEBHOOK_SECRET)) {
      return res.status(400).send('Invalid signature');
    }

    const payload = JSON.parse(req.body.toString());

    // Linear requires rejecting deliveries older than 1 minute
    if (!isFreshTimestamp(payload.webhookTimestamp)) {
      return res.status(400).send('Stale webhook');
    }

    console.log(`Linear ${event} ${payload.action} (delivery: ${delivery})`);

    switch (event) {
      case 'Issue':
        console.log(`Issue ${payload.action}:`, payload.data?.title);
        break;
      case 'Comment':
        console.log(`Comment ${payload.action} on issue ${payload.data?.issueId}`);
        break;
      case 'Project':
        console.log(`Project ${payload.action}:`, payload.data?.name);
        break;
      case 'IssueSLA':
        console.log(`SLA event on issue ${payload.issueData?.id}`);
        break;
      default:
        console.log(`Unhandled Linear event: ${event}`);
    }

    res.status(200).send('OK');
  }
);

```

### Python Signature Verification (FastAPI)

```python
import hmac
import hashlib
import time

def verify_linear_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header, expected)

def is_fresh_timestamp(webhook_timestamp_ms: int) -> bool:
    if not isinstance(webhook_timestamp_ms, int):
        return False
    now_ms = int(time.time() * 1000)
    return abs(now_ms - webhook_timestamp_ms) <= 60_000

```

> For complete working examples with tests, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/linear-webhooks/examples/express/) - Full Express implementation
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/linear-webhooks/examples/nextjs/) - Next.js App Router implementation
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/linear-webhooks/examples/fastapi/) - Python FastAPI implementation

## Common Linear-Event Header Values

| `Linear-Event` | Triggered When |
| --- | --- |
| `Issue` | Issue created, updated, or removed |
| `Comment` | Comment created, updated, or removed |
| `IssueLabel` | Label created, updated, or removed |
| `Project` | Project created, updated, or removed |
| `ProjectUpdate` | Project update posted |
| `Cycle` | Cycle created, updated, or removed |
| `Reaction` | Reaction added or removed |
| `Document` | Document created, updated, or removed |
| `Initiative` | Initiative created, updated, or removed |
| `InitiativeUpdate` | Initiative update posted |
| `Customer` | Customer record changed |
| `CustomerRequest` | Customer request created/updated |
| `User` | User changed |
| `IssueSLA` | SLA `set`, `highRisk`, or `breached` for an issue |
| `OAuthAppRevoked` | OAuth app permissions revoked |

> For the full event reference, see [Linear's webhook documentation](https://linear.app/developers/webhooks).

## Common Action Values

Data change events (`Issue`, `Comment`, `Project`, …) send one of:

| `action` | Meaning |
| --- | --- |
| `create` | Entity created |
| `update` | Entity updated (`updatedFrom` contains previous values) |
| `remove` | Entity deleted |

`IssueSLA` and `OAuthAppRevoked` use event-specific actions (e.g. `set`, `highRisk`, `breached`).

## Important Headers

| Header | Description |
| --- | --- |
| `Linear-Signature` | HMAC-SHA256 of raw body, hex encoded |
| `Linear-Event` | Entity type (e.g. `Issue`, `Comment`, `Project`) |
| `Linear-Delivery` | UUID v4 unique to the delivery — use for idempotency |
| `Content-Type` | `application/json; charset=utf-8` |
| `User-Agent` | `Linear-Webhook` |

## Environment Variables

```bash
LINEAR_WEBHOOK_SECRET=your_webhook_secret   # Shown once when the webhook is created in Linear

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 linear --path /webhooks/linear

```

Use the printed Hookdeck URL as the webhook URL when creating the webhook in Linear's API settings.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/linear-webhooks/references/overview.md) - Linear webhook concepts and event types
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/linear-webhooks/references/setup.md) - Configuring a webhook in Linear
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/linear-webhooks/references/verification.md) - Signature verification details and gotchas