← All articles

Jira Webhooks: Setup, Events, JQL Filters, Retries and Signatures

Webhooker Team 22 min read
Flat illustration of Jira webhook filtering and delivery: a board of issue cards on the left throws cards into a funnel that stands for a JQL filter, only one card comes out of the funnel onto a cable that runs through the blue Webhooker pulse node, a stopwatch above the cable stands for the retry delay, and a row of envelopes reaches a server with a checkmark.

Jira webhooks are HTTP POST requests that Jira sends to a URL you register when something changes: an issue is created, updated or deleted, a comment is added, a sprint starts, a version is released. Each request carries the event name in the JSON body as webhookEvent (for example jira:issue_updated), a unique id in the X-Atlassian-Webhook-Identifier header, and, if you set a secret, an HMAC-SHA256 signature in X-Hub-Signature. Jira Cloud treats only HTTP 200 as success. On 408, 409, 425, 429, any 5xx, a timeout or a refused connection, it retries up to five times, 5 to 15 minutes apart.

Two things make Jira different from the other providers we have covered. Admin webhooks can be narrowed with a JQL query, so the filtering happens in Jira instead of in your code. And Jira Cloud gives administrators no delivery log: there is no page listing what was sent and what failed. This guide covers the three ways Jira sends webhooks, how to create one in the UI and through the API, what arrives in a delivery, which events exist, how to verify the signature, and how to build a receiver that does not lose events. Every Jira fact links to Atlassian’s documentation, checked on 26 October 2026. It is about Jira Cloud unless a section says Data Center. If your code also lives on GitHub or GitLab, the GitHub webhooks guide and the GitLab webhooks guide cover those platforms the same way.

Admin webhook, app webhook or Automation: which one do you need?

Jira can send an HTTP request on an event in three ways, and they differ more than the shared name suggests.

MethodWho sets it upFilteringSignatureBest for
Admin webhookA Jira admin, in Settings > System > WebHooks or through the admin REST APIJQL on issue, comment, worklog and attachment eventsX-Hub-Signature, if a secret is setYour own service that should hear about everything in a site, or one project
App webhook (Connect or OAuth 2.0)An app, declared in its descriptor or registered through /rest/api/3/webhookA restricted JQL subsetJWT in Authorization for OAuth 2.0 apps; Connect apps use the app’s shared secretMarketplace and internal apps that act for users
Automation Send web requestAnyone who can edit an Automation flowThe flow’s own trigger and conditionsNone built in; you add your own headerA request in a custom shape to a third-party API

The webhooks overview documents the first two. App webhooks registered through the REST API expire after 30 days unless the app calls PUT /rest/api/3/webhook/refresh, and an OAuth 2.0 app may register at most five per user per site (a Connect app, 100 per site). Connect is on its way out: Atlassian no longer accepts new Connect apps on the Marketplace and builds new features on Forge.

For your own backend, use an admin webhook. It never expires, it supports a secret, and an admin can see it in the UI. The rest of this guide is mostly about admin webhooks; Automation has its own section near the end.

How do you create a Jira webhook?

In the admin UI

You need the Administer Jira global permission. The steps: select Settings (the cog) > System, then under Advanced select WebHooks and Create a WebHook. The fields:

A webhook can also be disabled from its details panel without deleting it, which is the safe way to pause one. A deleted webhook is gone, along with its secret.

With the REST API

Admin webhooks have their own, older REST resource, /rest/webhooks/1.0/webhook, separate from the /rest/api/3/webhook resource that apps use. On Jira Cloud, authenticate as an admin with an email address and an API token:

curl --request POST \
  --user "$JIRA_EMAIL:$JIRA_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "deploy-tracker",
    "description": "Issue changes in project OPS",
    "url": "https://example.com/hooks/jira",
    "events": ["jira:issue_created", "jira:issue_updated", "comment_created"],
    "filters": { "issue-related-events-section": "project = OPS" },
    "excludeBody": false,
    "secret": "<long random string>"
  }' \
  "https://your-site.atlassian.net/rest/webhooks/1.0/webhook"

The same resource takes GET to list webhooks, and PUT or DELETE on /rest/webhooks/1.0/webhook/{webhookId}. The response includes isSigned: true when a secret is set; the secret itself is never returned. To remove a secret, send null or an empty string on update. Connect apps cannot use this resource.

What does a Jira webhook delivery look like?

An issue moving from To Do to In Progress, as it arrives at your endpoint with a secret configured (header values and body shortened):

POST /hooks/jira HTTP/1.1
Host: example.com
Content-Type: application/json
X-Atlassian-Webhook-Identifier: 4mL7Y2qPk9
X-Atlassian-Webhook-Flow: Primary
X-Hub-Signature: sha256=9b1f0c6d...e41a

{
  "timestamp": 1792999800123,
  "webhookEvent": "jira:issue_updated",
  "issue_event_type_name": "issue_generic",
  "user": {
    "accountId": "5b10ac8d82e05b22cc7d4ef5",
    "accountType": "atlassian",
    "displayName": "Jane Doe"
  },
  "issue": {
    "id": "10042",
    "key": "OPS-128",
    "fields": {
      "summary": "Rotate the payment API key",
      "status": { "name": "In Progress" },
      "project": { "key": "OPS" }
    }
  },
  "changelog": {
    "id": "10331",
    "items": [
      { "field": "status", "fromString": "To Do", "toString": "In Progress" }
    ]
  }
}

The payload section describes the parts:

The headers are fewer than on other platforms:

HeaderWhat it carriesUse it for
X-Atlassian-Webhook-IdentifierOne id per webhook, unique within the site and the same across retriesDeduplication
X-Atlassian-Webhook-RetryThe retry number. Absent on the first attemptSpotting retries in your logs
X-Atlassian-Webhook-FlowPrimary for direct actions, Secondary for bulk and cascading changesKnowing which delivery target applies
X-Hub-Signaturesha256=<hex HMAC>, only if a secret is setAuthentication

One warning from the same page: webhooks are sent “without a specific user context”, so all issues are available to them. An admin webhook with an empty JQL filter sends issues from restricted projects too. If the receiver posts to a shared channel, narrow the JQL or filter on your side.

Which Jira webhook events can you subscribe to?

The full event list is long. The ones most integrations use:

AreawebhookEvent valuesTypical use
Issuesjira:issue_created, jira:issue_updated, jira:issue_deletedSyncing to another tracker, deploy gates, chat notifications
Commentscomment_created, comment_updated, comment_deletedChat-ops, mirroring discussion
Worklogsworklog_created, worklog_updated, worklog_deletedTime tracking and billing
Attachmentsattachment_created, attachment_deletedVirus scanning, archiving
Issue linksissuelink_created, issuelink_deletedDependency graphs
Sprintssprint_created, sprint_started, sprint_closed, sprint_updated, sprint_deletedReports, release notes
Versionsjira:version_created, jira:version_released, jira:version_unreleased, jira:version_updated, jira:version_moved, jira:version_deletedRelease automation, changelogs
Projectsproject_created, project_updated, project_deleted, project_archived, and othersProvisioning
Usersuser_created, user_updated, user_deletedAccount sync

There are also events for issue properties, issue types, filters, boards and system settings. Three behaviours from the known issues list tend to cost a day of debugging:

Does a status change send its own event?

No. A workflow transition arrives as jira:issue_updated, with the status change in changelog.items (field: "status"). To act on “moved to Done”, check the changelog, not just the event name:

def moved_to_status(payload: dict, target_status: str) -> bool:
    if payload.get("webhookEvent") != "jira:issue_updated":
        return False
    changelog_items = payload.get("changelog", {}).get("items", [])
    return any(
        item.get("field") == "status" and item.get("toString") == target_status
        for item in changelog_items
    )

Workflow post functions offer a second route: a Trigger a Webhook post function on a transition fires an admin webhook only for that transition. Two caveats from Atlassian: a post function on the Create transition does not fire (use jira:issue_created), and a webhook that has both events ticked and a post function fires twice for one transition, so untick its events.

How do JQL filters work?

The JQL field limits issue, comment, worklog, attachment and issue property events to issues that match. project = OPS AND issuetype = Bug is a common one. The filter runs for every event, so Atlassian warns that slow JQL, for example on some custom fields, delays delivery. Sprint, version, issue link and most other events ignore the filter and arrive for every board and project, so filter those in your receiver.

App webhooks get a stricter subset: only the issueKey, project, issuetype, status, priority, assignee and reporter clauses, indexed issue properties, and the operators =, !=, IN and NOT IN.

URL variables

An admin webhook URL can contain variables that Jira fills in per event, such as {issue.key}, {project.key}, {comment.id}, {sprint.id} and {version.id}:

https://example.com/hooks/jira/{project.key}/{issue.key}

Combined with Exclude body, this gives a lightweight design: Jira tells your service which issue changed, and the service fetches the current state through the REST API. That keeps issue data out of logs on the way and always gives you the latest version, which helps with the ordering problem described below. Variables only work for events about their own entity, so {sprint.id} is empty on an issue event.

How does Jira handle failed deliveries?

The retry policy is short and has one rule that surprises people:

ConditionWhat Jira does
Your endpoint answers 200Delivered. Any other status, including 201, 202 and 204, counts as unsuccessful
408, 409, 425, 429 or 5xx, a timeout, or a failed connectionRetries up to 5 times, each 5 to 15 minutes (randomised) after the previous attempt
Any other 4xx, such as 400, 401 or 404Not retried
No successful response for 30 minutesOnly one attempt per webhook from then on, until a delivery succeeds

Read the first row twice. A framework that answers 202 Accepted or 204 No Content by default is, by Atlassian’s definition, failing. Return 200 explicitly.

The last row is the one that loses events during outages. After half an hour of errors, Jira stops spending retries: each new event gets a single attempt, and if your service is still down, that event is not retried. Recovery is not automatic for the events in between.

Atlassian’s Jira Cloud documentation does not publish a response timeout. Figures such as “5 seconds to connect, 20 seconds to respond” circulate in third-party guides, but the official page only says that a timeout counts as a failure, so answer within a few seconds and do the work from a queue. What Atlassian does publish are delivery targets: Primary webhooks, from a direct user action, should arrive within 30 seconds; Secondary ones, from bulk edits and cascading changes, within 15 minutes. Concurrency is capped at 20 parallel requests per site and receiving host for primary webhooks and 10 for secondary ones. A bulk change to 2,000 issues is therefore 2,000 webhooks, queued behind those limits. Payloads over 25 MB are not delivered.

Where do you see failed Jira webhooks?

For admin webhooks, nowhere in the UI. Jira Cloud has no delivery log for them. The suggestion to add one has been open since 2014 with the status “Gathering Interest”. The only failure record is the Get failed webhooks operation, GET /rest/api/3/webhook/failed, which lists deliveries that exhausted their retries in roughly the last 72 hours, and only Connect apps may call it.

In practice that means your receiver has to be the log. Record the X-Atlassian-Webhook-Identifier, the event and the outcome of every request you accept. If you need to know whether Jira sent something at all, send the same events to a second endpoint that stores them, which is also what Atlassian’s own troubleshooting article does with a request bin.

Duplicates and ordering

Atlassian says plainly that some webhooks “might be delivered more than once”, for example when your 200 is lost on the way back. Deduplicate on X-Atlassian-Webhook-Identifier, which stays the same across retries. The idempotency keys guide shows a pattern that survives concurrent duplicates.

Ordering is not promised. With retries spaced 5 to 15 minutes apart, the event for “In Progress” can arrive after the one for “Done”. Compare issue.fields.updated with what you have stored and ignore older states, or fetch the issue fresh, as in the URL variables pattern above. The ordered delivery post covers both approaches.

How do you verify a Jira webhook signature?

When an admin webhook has a secret, Jira computes an HMAC-SHA256 over the raw request body with the secret as the key and sends it as X-Hub-Signature: sha256=<hex digest>. The documented steps: read the method from before the =, compute the HMAC of the raw body (as UTF-8 bytes), and compare in constant time. In Python with Flask:

import hashlib
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)

JIRA_WEBHOOK_SECRET = os.environ["JIRA_WEBHOOK_SECRET"].encode()


def jira_signature_is_valid(raw_body: bytes, signature_header: str) -> bool:
    method, _, received_digest = signature_header.partition("=")
    if method != "sha256" or not received_digest:
        return False
    expected_digest = hmac.new(JIRA_WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected_digest, received_digest)


@app.post("/hooks/jira")
def receive_jira_webhook():
    raw_body = request.get_data()
    if not jira_signature_is_valid(raw_body, request.headers.get("X-Hub-Signature", "")):
        abort(401)

    webhook_id = request.headers.get("X-Atlassian-Webhook-Identifier", "")
    enqueue_for_processing(webhook_id, request.get_json())
    return "", 200

Atlassian publishes a test vector you can put in a unit test: the secret It's a Secret to Everybody and the body Hello World! give sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9.

The format matches GitHub’s, with one difference: GitHub sends SHA-256 in X-Hub-Signature-256 and keeps X-Hub-Signature for its legacy SHA-1 digest, while Jira puts SHA-256 in X-Hub-Signature. Code copied from a GitHub receiver needs the header name changed, or every Jira request fails. The GitHub signature guide explains the shared mechanics, and the general signature verification guide explains why you must sign the raw bytes, not re-serialised JSON.

Jira does not sign a timestamp, so a captured request stays valid if someone replays it. Deduplicating on X-Atlassian-Webhook-Identifier blocks exact replays; for the wider problem, see replay attacks and timestamp tolerance.

Two other checks are available. App webhooks for OAuth 2.0 apps carry a JWT in the Authorization header, signed with the app’s client secret. And Jira Cloud sends from Atlassian’s published IP ranges, which you can allowlist as an extra layer. They are shared by every Atlassian customer, so an IP match proves the request came from Atlassian, not from your site.

How is Jira Data Center different?

Jira Data Center has admin webhooks in the same System > WebHooks place, with the same events, JQL filters and URL variables, but the delivery engine is different. According to Atlassian’s Data Center troubleshooting article:

Jira Automation: Send web request and Incoming webhook

Automation is often the quicker path, and it works in both directions.

Send web request is an action in an Automation flow (Atlassian now calls rules “flows” and issues “work items” in parts of the UI). It sends a request to any URL with a body in Jira format, Automation format or a custom format built from smart values such as {{issue.key}}. You can add your own headers and mark a value as hidden, which masks it once the flow is saved. That is how you authenticate: put an API key in a hidden header, and check it on the receiving side. There is no signature. Anyone who can edit the flow can change the URL, so Atlassian recommends restricting who can edit flows that send data out.

The Incoming webhook trigger works the other way: it gives a flow a URL and a secret, and a POST to that URL runs the flow. The setup article says to send the secret in an X-Automation-Webhook-Token header, or, if the sender cannot set headers, to append it to the URL after a slash. The header is the safer choice, because URLs end up in logs. The payload is available in the flow as {{webhookData}}.

A common setup joins the two: an admin webhook or a flow in one Jira site posts to an Incoming webhook in another. If you point an admin webhook at an Automation URL, leave the admin webhook’s secret empty and use the URL form of the token, since admin webhooks cannot add custom headers.

How do you test Jira webhooks locally?

Jira Cloud only sends to public HTTPS URLs with a trusted certificate, so localhost needs something in front of it. Jira also has no Test button for admin webhooks: to fire one, you change an issue. Create a test project, narrow the webhook’s JQL to it (project = WHTEST), and edit issues there.

Test the failure path as well. Return 500 from your local receiver, note the time, and watch the retries arrive 5 to 15 minutes apart with X-Atlassian-Webhook-Retry set. Then return 202 and confirm that Jira treats it as a failure too. The webhook testing guide covers fixtures and replaying real payloads in automated tests.

Putting a gateway in front of Jira

We build Webhooker, so treat this section as a vendor describing its own product.

The weak spots above are an outage longer than 30 minutes, which reduces every event to a single attempt, and a Cloud admin who cannot see what was lost. A webhook gateway takes the receiving side off your service: it answers Jira at once, stores the event, and delivers it to your service on its own schedule.

With Webhooker you create a source and paste its ingest URL (https://app.webhooker.eu/in/<token>) into the webhook’s URL field. Webhooker answers 200 by default, which is the only status Jira accepts, so leave the source’s custom response status at 200 if you change the body. Every request is stored before delivery starts, and delivery to each destination gets up to six attempts over about five hours with exponential backoff. Events that exhaust them wait in a dead-letter queue until you replay them, one at a time or in bulk. Payloads and delivery history are kept for 14 days on the free plan and 30 or 90 days on paid plans, which gives a Jira Cloud site the delivery log it does not have. Filters on the body can route by webhookEvent or by project key, for example only jira:version_released to a release-notes service.

Three limits to know first. Webhooker does not yet verify Jira’s X-Hub-Signature: its GitHub preset reads X-Hub-Signature-256, and its generic HMAC check does not strip the sha256= prefix. Webhooker also delivers with its own headers rather than Jira’s, so the signature does not reach your service either. Until that changes, the unguessable ingest URL is the shared secret on the Jira side, and your service checks Webhooker’s own signature on the delivery. For Automation flows you can add an API key header and have the source check it at ingest. Second, because Jira’s headers are not forwarded, route on webhookEvent in the body, not on X-Atlassian-Webhook-Identifier; deduplicate on Webhooker’s event id or on issue.id plus timestamp. Third, request bodies are capped at 1 MB, against Jira’s 25 MB, so an issue with a very large description is rejected at ingest.

Jira payloads carry names, account ids and the text of issues and comments, often more sensitive than code events. Webhooker keeps ingest, storage and delivery inside the EU, and the free plan covers 10,000 events a month. To try it, create a free account, follow the quick start, and add a second admin webhook with a narrow JQL filter next to your existing one. For local work, the Webhooker CLI forwards events from a source to a port on your machine.

Frequently asked questions

How do I create a webhook in Jira?

With the Administer Jira permission, go to Settings > System > WebHooks > Create a WebHook. Enter a name and an HTTPS URL, set a secret, tick the events and, optionally, add a JQL filter for issue events. Through the API, send POST /rest/webhooks/1.0/webhook with name, url, events and optional filters and secret, authenticated as an admin.

Does Jira retry failed webhooks?

Yes, Jira Cloud retries up to five times, 5 to 15 minutes apart, when your endpoint returns 408, 409, 425, 429 or 5xx, times out or refuses the connection. Other 4xx codes are not retried. If your endpoint has not succeeded for 30 minutes, Jira makes only one attempt per new webhook until a delivery succeeds.

What is the Jira webhook timeout?

Atlassian does not publish a response timeout for Jira Cloud webhooks. Its documentation only says that a timed-out request is retried like a 5xx, and that primary webhooks should be delivered within 30 seconds of the event. Respond with 200 within a few seconds and process the event asynchronously.

Why does Jira treat my 202 response as a failure?

Jira Cloud counts only HTTP 200 as a successful delivery. Atlassian’s documentation says all other response codes should be treated as unsuccessful, so 201, 202 and 204 count as failures. Return 200 explicitly, even when you queue the event for later processing.

How do I verify a Jira webhook?

Set a secret on the admin webhook. Jira then sends X-Hub-Signature: sha256=<hex>, an HMAC-SHA256 of the raw body keyed with your secret. Compute the same HMAC over the unparsed bytes and compare with a constant-time function such as hmac.compare_digest. Note the header name: GitHub uses X-Hub-Signature-256 for the same scheme.

Where can I see Jira webhook delivery logs?

Jira Cloud has no delivery log for admin webhooks. Connect apps can call GET /rest/api/3/webhook/failed for deliveries that exhausted their retries in about the last 72 hours. Otherwise, log every request in your receiver or put a gateway in front that stores events. Jira Data Center records attempts in the AO_A0B856_HIST_INVOCATION database table.

How do I trigger a Jira webhook only for one project or status?

Add a JQL filter such as project = OPS AND status = Done to the admin webhook. It applies to issue, comment, worklog, attachment and issue property events; sprint and version events ignore it. To react to a status change, subscribe to jira:issue_updated and check changelog.items for field: "status".

Can a Jira webhook send a custom JSON body?

Admin webhooks cannot: they send Jira’s own payload, or no body with Exclude body. For a custom body, use the Automation Send web request action with a custom format built from smart values such as {{issue.key}} and {{issue.summary}}, and add your own headers there.