Skip to main content

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! 🙌
Finding your API endpoint

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

  1. Go to SettingsSystemWebHooks (under Advanced)
  2. Click Create a WebHook
  3. 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
  4. Click Create

Jira Server/Data Center

  1. Go to AdministrationSystemWebHooks
  2. Click Create a WebHook
  3. Configure URL and events
  4. 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

EventTriggerCommon Use Case
jira:issue_createdNew issue createdNotifications, auto-assign
jira:issue_updatedIssue modifiedSync external systems
jira:issue_deletedIssue removedCleanup references
comment_createdComment addedMention notifications
comment_updatedComment editedAudit trail
sprint_startedSprint beginsTeam notifications
sprint_closedSprint endsReports, velocity
worklog_createdTime loggedTime tracking sync
issuelink_createdIssues linkedDependency tracking
project_createdNew projectSetup automation
user_createdNew user addedOnboarding 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
Jira Cloud vs Server

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.

Jira Webhooks FAQ

Common questions about handling Jira webhooks

How do I create a Jira webhook?
In Jira Cloud: Settings → System → WebHooks → Create a WebHook. Enter your URL, select events to subscribe to, and optionally add a JQL filter to limit which issues trigger the webhook. Click Create to save.
What is JQL filtering in Jira webhooks?
JQL (Jira Query Language) filters limit which issues trigger your webhook. For example, 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?
The 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?
Jira may retry webhooks if it doesn't receive a timely response. Implement idempotency by tracking processed webhook IDs or using a combination of timestamp and issue key. Return 2xx quickly to acknowledge receipt.
How do I authenticate responses back to Jira?
Use Basic auth with your email and an API token (not your password). Create an API token at id.atlassian.com → Security → API tokens. Use the header: Authorization: Basic base64(email:token).
What events should I subscribe to?
Only subscribe to events you need. Common choices: 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?
Use JQL filters to reduce events, respond to webhooks immediately (return 200), queue heavy processing for async handling, and implement rate limiting. Store webhook payloads and process them in background jobs.
How do I test Jira webhooks?
With Codehooks.io, just deploy and you're live in seconds - no tunneling needed. Run 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.