How to Receive Jira Webhooks with Examples
Jira webhooks notify your application when events occur in your Jira projects — issues are created, updated, transitioned, sprints start, and more. They're essential for automating workflows, syncing with external tools, and building custom integrations.
The problem? Jira webhook payloads are complex, events fire frequently, and you need proper filtering to avoid overwhelming your systems.
The solution? Deploy a production-ready Jira webhook handler with Codehooks.io in under 5 minutes.
Prerequisites
Before you begin, sign up for a free Codehooks.io account and install the CLI:
npm install -g codehooks
Quick Start
Create a Jira webhook handler:
coho create myjirahandler
You'll see output like:
Creating project: myjirahandler
Project created successfully!
Your API endpoints:
https://myjirahandler-a1b2.api.codehooks.io/dev
Then set up the project:
cd myjirahandler
npm install
Add your Jira webhook handler code to index.js:
import { app } from 'codehooks-js';
import { Datastore } from 'codehooks-js';
// Jira webhook endpoint
app.post('/jira/webhooks', async (req, res) => {
const { webhookEvent, issue, user, changelog, sprint } = req.body;
console.log(`Received Jira event: ${webhookEvent}`);
// Handle different Jira events
switch (webhookEvent) {
case 'jira:issue_created':
await handleIssueCreated(issue, user);
break;
case 'jira:issue_updated':
await handleIssueUpdated(issue, user, changelog);
break;
case 'jira:issue_deleted':
await handleIssueDeleted(issue, user);
break;
case 'sprint_started':
await handleSprintStarted(sprint);
break;
case 'sprint_closed':
await handleSprintClosed(sprint);
break;
case 'comment_created':
await handleCommentCreated(req.body);
break;
default:
console.log(`Unhandled event: ${webhookEvent}`);
}
res.json({ received: true });
});
async function handleIssueCreated(issue, user) {
console.log(`Issue created: ${issue.key} - ${issue.fields.summary}`);
console.log(`Created by: ${user.displayName}`);
console.log(`Project: ${issue.fields.project.name}`);
console.log(`Type: ${issue.fields.issuetype.name}`);
console.log(`Priority: ${issue.fields.priority?.name}`);
// Your business logic:
// - Send Slack notification
// - Create task in external system
// - Update dashboard
// - Auto-assign based on rules
}
async function handleIssueUpdated(issue, user, changelog) {
console.log(`Issue updated: ${issue.key}`);
console.log(`Updated by: ${user.displayName}`);
// Check what changed
if (changelog?.items) {
for (const change of changelog.items) {
console.log(`Field: ${change.field}`);
console.log(`From: ${change.fromString} → To: ${change.toString}`);
// Handle specific field changes
if (change.field === 'status') {
await handleStatusChange(issue, change);
} else if (change.field === 'assignee') {
await handleAssigneeChange(issue, change);
} else if (change.field === 'priority') {
await handlePriorityChange(issue, change);
}
}
}
}
async function handleStatusChange(issue, change) {
const { fromString, toString } = change;
console.log(`Status changed: ${fromString} → ${toString}`);
// Status-specific actions
if (toString === 'Done') {
// Issue completed
// - Update time tracking
// - Send completion notification
// - Trigger deployment if it's a deploy ticket
} else if (toString === 'In Progress') {
// Work started
// - Start time tracking
// - Notify team
} else if (toString === 'In Review') {
// Ready for review
// - Notify reviewers
// - Create PR reminder
}
}
async function handleAssigneeChange(issue, change) {
console.log(`Assignee changed: ${change.fromString || 'Unassigned'} → ${change.toString || 'Unassigned'}`);
// Notify new assignee
}
async function handlePriorityChange(issue, change) {
if (change.toString === 'Highest' || change.toString === 'Blocker') {
console.log(`High priority issue: ${issue.key}`);
// Send urgent notification
}
}
async function handleIssueDeleted(issue, user) {
console.log(`Issue deleted: ${issue.key} by ${user.displayName}`);
// Cleanup external references
}
async function handleSprintStarted(sprint) {
console.log(`Sprint started: ${sprint.name}`);
console.log(`Goal: ${sprint.goal}`);
console.log(`Start: ${sprint.startDate}`);
console.log(`End: ${sprint.endDate}`);
// Your business logic:
// - Send sprint kickoff notification
// - Update dashboards
// - Create sprint report template
}
async function handleSprintClosed(sprint) {
console.log(`Sprint closed: ${sprint.name}`);
// Your business logic:
// - Generate sprint report
// - Calculate velocity
// - Archive sprint data
}
async function handleCommentCreated(payload) {
const { comment, issue } = payload;
console.log(`New comment on ${issue.key} by ${comment.author.displayName}`);
console.log(`Comment: ${comment.body}`);
// Check for mentions or keywords
if (comment.body.includes('@devops')) {
// Notify DevOps team
}
}
export default app.init();
Deploy:
coho deploy
Project: myjirahandler-a1b2 Space: dev
Deployed Codehook successfully! 🙌
Run coho info to see your API endpoints and tokens. Your webhook URL will be:
https://myjirahandler-a1b2.api.codehooks.io/dev/jira/webhooks
Configure Jira Webhooks
Jira Cloud
- Go to Settings → System → WebHooks (under Advanced)
- Click Create a WebHook
- Enter:
- Name: Your webhook name
- URL:
https://myjirahandler-a1b2.api.codehooks.io/dev/jira/webhooks - Events: Select events to subscribe to
- JQL Filter (optional): Filter which issues trigger webhooks
- Click Create
Jira Server/Data Center
- Go to Administration → System → WebHooks
- Click Create a WebHook
- Configure URL and events
- Optionally add a JQL filter
Set Environment Variables
# If using Jira API for responses
coho set-env JIRA_API_TOKEN your-api-token --encrypted
coho set-env JIRA_EMAIL [email protected]
coho set-env JIRA_BASE_URL https://your-domain.atlassian.net
Common Jira Webhook Events
| Event | Trigger | Common Use Case |
|---|---|---|
jira:issue_created | New issue created | Notifications, auto-assign |
jira:issue_updated | Issue modified | Sync external systems |
jira:issue_deleted | Issue removed | Cleanup references |
comment_created | Comment added | Mention notifications |
comment_updated | Comment edited | Audit trail |
sprint_started | Sprint begins | Team notifications |
sprint_closed | Sprint ends | Reports, velocity |
worklog_created | Time logged | Time tracking sync |
issuelink_created | Issues linked | Dependency tracking |
project_created | New project | Setup automation |
user_created | New user added | Onboarding workflows |
Jira Webhook Signature Verification (HMAC)
Jira Cloud supports webhook signature verification using HMAC-SHA256. When you register a webhook with a secret, Jira signs the payload:
import { app } from 'codehooks-js';
import crypto from 'crypto';
const JIRA_WEBHOOK_SECRET = process.env.JIRA_WEBHOOK_SECRET;
// Verify Jira webhook signature
function verifyJiraWebhook(req) {
const signature = req.headers['x-hub-signature'];
if (!signature || !JIRA_WEBHOOK_SECRET) {
return false; // No signature or no secret configured
}
const body = req.rawBody;
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', JIRA_WEBHOOK_SECRET)
.update(body)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (e) {
return false;
}
}
app.post('/jira/webhooks', async (req, res) => {
// Verify signature if secret is configured
if (JIRA_WEBHOOK_SECRET && !verifyJiraWebhook(req)) {
console.error('Invalid Jira webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
res.json({ received: true });
});
export default app.init();
Set your webhook secret:
coho set-env JIRA_WEBHOOK_SECRET your-secret-here --encrypted
Signature verification is available in Jira Cloud. For Jira Server/Data Center, consider IP whitelisting or using Connect apps for authentication.
JQL Filtering
Use JQL (Jira Query Language) to filter which issues trigger your webhook:
# Only issues in PROJECT
project = PROJECT
# Only bugs
issuetype = Bug
# Only high priority issues
priority in (Highest, High)
# Combination
project = PROJECT AND issuetype = Bug AND priority = Highest
# Issues assigned to specific user
assignee = currentUser()
# Issues in specific sprint
sprint in openSprints()
# Recently updated
updated >= -1h
Pro tip: Use JQL filtering to reduce webhook traffic and focus on relevant events.
Slack Integration for Jira Events
Send Jira updates to Slack:
import { app } from 'codehooks-js';
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
app.post('/jira/webhooks', async (req, res) => {
const { webhookEvent, issue, user, changelog } = req.body;
let slackMessage = null;
switch (webhookEvent) {
case 'jira:issue_created':
slackMessage = {
text: `🆕 New issue created`,
attachments: [{
color: getTypeColor(issue.fields.issuetype.name),
title: `${issue.key}: ${issue.fields.summary}`,
title_link: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
fields: [
{ title: 'Type', value: issue.fields.issuetype.name, short: true },
{ title: 'Priority', value: issue.fields.priority?.name || 'None', short: true },
{ title: 'Reporter', value: user.displayName, short: true },
{ title: 'Project', value: issue.fields.project.name, short: true }
]
}]
};
break;
case 'jira:issue_updated':
const statusChange = changelog?.items?.find(i => i.field === 'status');
if (statusChange) {
slackMessage = {
text: `📋 Issue status changed`,
attachments: [{
color: getStatusColor(statusChange.toString),
title: `${issue.key}: ${issue.fields.summary}`,
title_link: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
fields: [
{ title: 'Status', value: `${statusChange.fromString} → ${statusChange.toString}`, short: true },
{ title: 'Updated by', value: user.displayName, short: true }
]
}]
};
}
break;
case 'comment_created':
const { comment } = req.body;
slackMessage = {
text: `💬 New comment on ${issue.key}`,
attachments: [{
color: '#7289da',
author_name: comment.author.displayName,
title: issue.fields.summary,
title_link: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
text: comment.body.slice(0, 200) + (comment.body.length > 200 ? '...' : '')
}]
};
break;
}
if (slackMessage) {
await fetch(SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(slackMessage)
});
}
res.json({ received: true });
});
function getTypeColor(type) {
const colors = {
'Bug': '#ff0000',
'Story': '#00ff00',
'Task': '#0099ff',
'Epic': '#6554c0',
'Sub-task': '#4fade6'
};
return colors[type] || '#7289da';
}
function getStatusColor(status) {
const colors = {
'Done': '#00ff00',
'In Progress': '#0099ff',
'To Do': '#cccccc',
'In Review': '#ffff00',
'Blocked': '#ff0000'
};
return colors[status] || '#7289da';
}
export default app.init();
Auto-Assignment Based on Rules
Automatically assign issues based on criteria:
import { app } from 'codehooks-js';
const JIRA_BASE_URL = process.env.JIRA_BASE_URL;
const JIRA_EMAIL = process.env.JIRA_EMAIL;
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
app.post('/jira/webhooks', async (req, res) => {
const { webhookEvent, issue } = req.body;
if (webhookEvent === 'jira:issue_created') {
// Auto-assign based on component or label
const assignee = determineAssignee(issue);
if (assignee && !issue.fields.assignee) {
await assignIssue(issue.key, assignee);
}
}
res.json({ received: true });
});
function determineAssignee(issue) {
const components = issue.fields.components?.map(c => c.name) || [];
const labels = issue.fields.labels || [];
// Assignment rules
const rules = {
'backend': 'backend-team-lead',
'frontend': 'frontend-team-lead',
'database': 'dba-team',
'security': 'security-team',
'urgent': 'on-call-engineer'
};
// Check components
for (const component of components) {
if (rules[component.toLowerCase()]) {
return rules[component.toLowerCase()];
}
}
// Check labels
for (const label of labels) {
if (rules[label.toLowerCase()]) {
return rules[label.toLowerCase()];
}
}
return null;
}
async function assignIssue(issueKey, assigneeId) {
const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');
await fetch(`${JIRA_BASE_URL}/rest/api/3/issue/${issueKey}/assignee`, {
method: 'PUT',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
accountId: assigneeId
})
});
}
export default app.init();
Handling High Volume Webhooks
For busy Jira instances, process webhooks asynchronously:
import { app, Datastore } from 'codehooks-js';
// Receive webhook and queue for processing
app.post('/jira/webhooks', async (req, res) => {
const conn = await Datastore.open();
// Store the webhook payload
const webhookId = `jira-${Date.now()}-${Math.random().toString(36).slice(2)}`;
await conn.insertOne('jira_webhooks', {
_id: webhookId,
payload: req.body,
receivedAt: new Date().toISOString(),
processed: false
});
// Queue for processing
await conn.enqueue('process-jira-webhook', { webhookId });
// Return immediately
res.json({ received: true, queued: webhookId });
});
// Process webhooks from queue
app.worker('process-jira-webhook', async (req, res) => {
const { webhookId } = req.body.payload;
const conn = await Datastore.open();
const webhook = await conn.getOne('jira_webhooks', webhookId);
if (!webhook) {
res.end();
return;
}
// Process the webhook
await processJiraEvent(webhook.payload);
// Mark as processed
await conn.updateOne('jira_webhooks', webhookId, {
$set: {
processed: true,
processedAt: new Date().toISOString()
}
});
res.end();
});
async function processJiraEvent(payload) {
// Your processing logic here
}
export default app.init();
Best Practices
1. Use JQL Filters
Reduce webhook volume by filtering to only relevant issues:
project = MYPROJECT AND issuetype in (Bug, Story)
2. Handle Retries
Jira may retry failed webhooks. Use idempotency:
const eventKey = `${req.body.timestamp}-${req.body.issue?.key}`;
// Check if already processed before handling
3. Respond Quickly
Return 2xx within a few seconds. Queue heavy processing for async handling.
4. Log Everything
Jira webhook debugging can be tricky. Log payloads during development.
Related Resources
- Jira Webhooks Documentation
- Jira REST API Reference
- JQL Syntax Reference
- All Codehooks Templates - Browse all templates
- Codehooks.io Quick Start
- Webhook Delivery System
Jira Webhooks FAQ
Common questions about handling Jira webhooks
How do I create a Jira webhook?
What is JQL filtering in Jira webhooks?
project = MYPROJECT AND issuetype = Bug only sends webhooks for bugs in MYPROJECT. This reduces traffic and focuses on relevant events.How do I know what changed in an issue_updated event?
changelog object contains an items array with each change. Each item has field (what changed), from/fromString (old value), and to/toString (new value). Check this array to see exactly what was modified.Why am I receiving duplicate Jira webhooks?
How do I authenticate responses back to Jira?
Authorization: Basic base64(email:token).What events should I subscribe to?
jira:issue_created and jira:issue_updated for issue tracking, sprint_started/sprint_closed for sprint automation, comment_created for mentions and notifications.How do I handle high-volume Jira instances?
How do I test Jira webhooks?
coho deploy to get a public URL instantly. Configure that URL in Jira's webhook settings and make changes in Jira to trigger test events. Check logs with coho logs.