Linear Webhook Integration
Linear webhooks provide real-time notifications for issue tracking, project updates, and team collaboration events. Perfect for automating workflows, syncing with other tools, and building custom integrations.
Quick Start
- Get your Unhook URL:
https://unhook.sh/wh_YOUR_ID
- Configure in Linear Settings: linear.app/settings/api
- Start receiving events locally:
unhook listen
Configuration Steps
1. Access Linear Webhook Settings
Navigate to Linear Settings:
2. Create a New Webhook
- Click “New webhook”
- Enter webhook details:
- Label: Give your webhook a descriptive name
- URL: Enter your Unhook URL:
https://unhook.sh/wh_YOUR_ID
- Select Resource types to subscribe to
3. Select Resource Types
Choose the resources you want to monitor:
Issue Events
- Issue - All issue-related events
- Created, updated, deleted
- Status changes
- Assignment changes
- Priority updates
- Label additions/removals
Project & Cycle Events
- Project - Project lifecycle events
- Cycle - Sprint/cycle management
- ProjectUpdate - Project status updates
- Milestone - Milestone tracking
Team Collaboration
- Comment - Issue comments
- Reaction - Emoji reactions
- IssueRelation - Issue linking
- Attachment - File attachments
Workflow & Organization
- WorkflowState - Workflow state changes
- Team - Team updates
- User - User account changes
- Label - Label management
- Template - Issue templates
- Secret: Copy the webhook secret for signature verification
- Enabled: Toggle to activate the webhook
- Click “Create webhook”
Event Payload Structure
Linear uses a consistent event structure:
{
"action": "create",
"actor": {
"id": "user_id",
"name": "John Doe",
"email": "john@example.com"
},
"createdAt": "2023-11-20T10:00:00.000Z",
"data": {
// Resource-specific data
},
"type": "Issue",
"url": "https://linear.app/team/issue/TEAM-123",
"webhookId": "webhook_id",
"webhookTimestamp": 1700478000000
}
Event Examples
Issue Created
{
"action": "create",
"actor": {
"id": "80e102b0-1234-5678-9abc-def012345678",
"name": "Jane Smith"
},
"createdAt": "2023-11-20T10:00:00.000Z",
"data": {
"id": "issue_id",
"identifier": "TEAM-123",
"title": "Fix authentication bug",
"description": "Users are unable to login with SSO",
"priority": 1,
"state": {
"id": "state_id",
"name": "In Progress",
"type": "started"
},
"assignee": {
"id": "assignee_id",
"name": "John Developer"
},
"creator": {
"id": "creator_id",
"name": "Jane Smith"
},
"team": {
"id": "team_id",
"key": "TEAM",
"name": "Engineering"
},
"labels": [
{
"id": "label_id",
"name": "bug",
"color": "#ff0000"
}
],
"estimate": 3,
"dueDate": "2023-11-25"
},
"type": "Issue",
"url": "https://linear.app/company/issue/TEAM-123"
}
Issue Status Changed
{
"action": "update",
"actor": {
"id": "80e102b0-1234-5678-9abc-def012345678",
"name": "John Developer"
},
"createdAt": "2023-11-20T14:30:00.000Z",
"data": {
"id": "issue_id",
"identifier": "TEAM-123",
"title": "Fix authentication bug",
"state": {
"id": "new_state_id",
"name": "Done",
"type": "completed"
},
"previousState": {
"id": "old_state_id",
"name": "In Progress",
"type": "started"
}
},
"type": "Issue",
"updatedFrom": {
"state": {
"id": "old_state_id",
"name": "In Progress"
}
}
}
{
"action": "create",
"actor": {
"id": "user_id",
"name": "Sarah QA"
},
"createdAt": "2023-11-20T15:00:00.000Z",
"data": {
"id": "comment_id",
"body": "I've tested the fix and it's working well. Ready for deployment.",
"issue": {
"id": "issue_id",
"identifier": "TEAM-123",
"title": "Fix authentication bug"
},
"user": {
"id": "user_id",
"name": "Sarah QA"
}
},
"type": "Comment",
"url": "https://linear.app/company/issue/TEAM-123#comment_id"
}
Webhook Security
Verify Webhook Signatures
Linear signs webhooks using HMAC-SHA256:
const crypto = require('crypto');
function verifyLinearWebhook(payload, signature, secret) {
const hash = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return `sha256=${hash}` === signature;
}
// Express middleware
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['linear-signature'];
const secret = process.env.LINEAR_WEBHOOK_SECRET;
if (!verifyLinearWebhook(req.body, signature, secret)) {
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(req.body);
// Process the webhook
console.log(`Linear ${event.type} ${event.action}:`, event.data);
res.status(200).send('OK');
});
Best Practices
1. Handle All Actions
Process create, update, and remove actions:
async function handleLinearWebhook(event) {
const { type, action, data } = event;
switch (type) {
case 'Issue':
switch (action) {
case 'create':
await handleIssueCreated(data);
break;
case 'update':
await handleIssueUpdated(data, event.updatedFrom);
break;
case 'remove':
await handleIssueDeleted(data);
break;
}
break;
case 'Comment':
if (action === 'create') {
await handleCommentAdded(data);
}
break;
// Handle other resource types...
}
}
2. Track Changes with updatedFrom
Use the updatedFrom
field to track what changed:
function handleIssueUpdate(issue, updatedFrom) {
if (updatedFrom?.state) {
console.log(`Status changed from ${updatedFrom.state.name} to ${issue.state.name}`);
notifyStatusChange(issue);
}
if (updatedFrom?.assignee) {
console.log(`Assignee changed to ${issue.assignee?.name || 'Unassigned'}`);
notifyAssignmentChange(issue);
}
if (updatedFrom?.priority !== undefined) {
console.log(`Priority changed from ${updatedFrom.priority} to ${issue.priority}`);
}
}
3. Implement Idempotency
Handle potential duplicate events:
const processedEvents = new Map();
async function processWebhook(event) {
const eventKey = `${event.webhookId}-${event.webhookTimestamp}`;
if (processedEvents.has(eventKey)) {
console.log(`Duplicate event ${eventKey} skipped`);
return;
}
processedEvents.set(eventKey, Date.now());
// Clean up old entries periodically
if (processedEvents.size > 10000) {
const cutoff = Date.now() - 3600000; // 1 hour
for (const [key, timestamp] of processedEvents) {
if (timestamp < cutoff) {
processedEvents.delete(key);
}
}
}
await handleLinearWebhook(event);
}
4. Use Linear SDK for Additional Context
Fetch additional data when needed:
const { LinearClient } = require('@linear/sdk');
const linear = new LinearClient({
apiKey: process.env.LINEAR_API_KEY
});
async function enrichIssueData(issueId) {
const issue = await linear.issue(issueId);
const comments = await issue.comments();
const attachments = await issue.attachments();
return {
issue: issue,
comments: comments.nodes,
attachments: attachments.nodes
};
}
Common Use Cases
Sync with External Systems
async function syncToExternalSystem(event) {
if (event.type === 'Issue' && event.action === 'create') {
const externalTicket = await createExternalTicket({
title: event.data.title,
description: event.data.description,
priority: mapLinearPriority(event.data.priority),
linearId: event.data.id,
linearUrl: event.url
});
// Store mapping for future updates
await storeMapping(event.data.id, externalTicket.id);
}
}
Auto-assign Based on Labels
async function autoAssignIssues(event) {
if (event.type === 'Issue' && event.action === 'update') {
const labels = event.data.labels || [];
for (const label of labels) {
const assignee = getAssigneeForLabel(label.name);
if (assignee && !event.data.assignee) {
await linear.issueUpdate(event.data.id, {
assigneeId: assignee.id
});
}
}
}
}
Notify on High Priority Issues
async function notifyHighPriorityIssues(event) {
if (event.type === 'Issue' && event.action === 'create') {
if (event.data.priority <= 1) { // Urgent or High
await sendSlackNotification({
text: `🚨 High priority issue created: ${event.data.identifier}`,
issue: event.data,
url: event.url
});
}
}
}
Testing Webhooks
Manual Testing
- Create a test issue in Linear
- Update the issue status
- Add comments or labels
- Verify webhooks are received
Using Linear API
// Create test issue via API
const issue = await linear.issueCreate({
teamId: 'team_id',
title: 'Test webhook issue',
description: 'Testing webhook integration'
});
// Update to trigger webhook
await linear.issueUpdate(issue.issue.id, {
stateId: 'new_state_id'
});
Environment Variables
# Linear API Key (for enriching webhook data)
LINEAR_API_KEY=lin_api_xxxxxxxxxxxxx
# Webhook Secret (from webhook configuration)
LINEAR_WEBHOOK_SECRET=your_webhook_secret
# Optional: Team ID for API operations
LINEAR_TEAM_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Common Issues
Missing Events
- Ensure all required resource types are selected
- Check webhook is enabled
- Verify URL is accessible
Signature Verification Failures
- Use the exact secret from Linear
- Ensure using raw request body
- Check for proper header name (
linear-signature
)
Rate Limits
- Linear API has rate limits (check headers)
- Implement exponential backoff
- Cache frequently accessed data
Useful Links
Support
Need help with Linear webhooks?
Responses are generated using AI and may contain mistakes.