A webhook is a user-defined callback over HTTPS. You can use Jira webhooks to notify your app or web application when certain events occur in Jira. For example, you might want to alert your remote application when an issue has been updated or when sprint has been started. Using a webhook to do this means that your remote application doesn't have to periodically poll Jira (via the REST APIs) to determine whether changes have occurred.
A webhook in Jira is defined by the following information, which you need to provide when registering (i.e. creating) a new webhook:
/rest/webhooks/1.0/webhook
/rest/webhooks/1.0/webhook
. The receiving web server must have a valid SSL/TLS certificate, signed by a globally trusted certificate authority.("project = TEST and fixVersion = future")
Webhooks created in Jira Administration or registered by /rest/webhooks/1.0/webhook
are called admin webhooks in this documentation.
To register (i.e. create) a webhook, you can use any of the following methods:
sharedSecret
)The information in this section will help you configure a webhook, whether you are creating a new one or modifying an existing one.
The following events are available for Jira webhooks. The string in parentheses is the name of the webhookEvent
in the response.
If you only want your webhook events to fire for a specific set of issues, you can specify a JQL query in your webhook to send only events triggered by matching issues. For example, you could register for all Issue Deleted events on issues in the Test ("TEST") Project with the Performance component.
JQL filtering is only supported by a subset of webhook events. Refer to the event sections below for details.
Note, although the JQL that uses standard Jira fields is very performant, some custom fields, because of their implementation, can take multiple seconds to query. You should take this into account when using JQL for a webhook, as the query will be run with each event.
If you leave the JQL query empty, it'll match all issues from all projects.
This means the webhooks we'll send may reveal sensitive information.
app_access_to_objects_blocked
)app_access_to_objects_in_container_blocked
)jira:issue_created
)jira:issue_updated
)jira:issue_deleted
)JQL filtering supported.
issue_property_set
)issue_property_deleted
)JQL filtering supported.
worklog_created
)worklog_updated
)worklog_deleted
)JQL filtering supported.
comment_created
)comment_updated
)comment_deleted
)JQL filtering supported.
attachment_created
)attachment_deleted
)JQL filtering supported.
issuelink_created
)issuelink_deleted
)issuetype_created
)issuetype_updated
)issuetype_deleted
)project_created
)project_updated
)project_deleted
)project_soft_deleted
)project_restored_deleted
)project_archived
)project_restored_archived
)jira:version_released
)jira:version_unreleased
)jira:version_created
)jira:version_moved
)jira:version_updated
)jira:version_deleted
)jira:version_deleted
)
note, this is the same webhook event name as the 'deleted' event, but the response will include a mergedTo
property*filter_created
)filter_updated
)filter_deleted
)user_created
)user_updated
)user_deleted
)option_voting_changed
)option_watching_changed*
)option_unassigned_issues_changed
)option_subtasks_changed
)option_issuelinks_changed
)option_timetracking_changed
)option_timetracking_provider_changed
)sprint_created
)sprint_deleted
)sprint_updated
)sprint_started
)sprint_closed
)board_created
)board_updated
)board_deleted
)board_configuration_changed
)jira_expression_evaluation_failed
)Read more about monitoring Jira expressions here: Monitoring Jira expressions.
If you are unsure which events to register your webhook for, then the simplest approach is to register your webhook for all events for the relevant entity (e.g. all issue-related events). Then you won't need to update the webhook if you need to handle these events in future; you can just add code in your app or web application once you want to react to them.
You can append variables to the webhook URL when creating or updating a webhook. The variables are listed below:
{attachment.id}
{board.id}
{comment.id}
{destinationIssue.key}
{destinationIssue.id}
{destinationProject.key}
{destinationProject.id}
{issue.id}
{issue.key}
{issuetype.id}
{mergedVersion.id}
{modifiedUser.accountId}
{modifiedUser.key}
(deprecated){modifiedUser.name}
(deprecated){project.id}
{project.key}
{property.key}
{sourceIssue.key}
{sourceIssue.id}
{sourceProject.key}
{sourceProject.id}
{sprint.id}
{version.id}
{worklog.id}
You can use these variables to dynamically insert the value of the current issue key, sprint ID, project key, etc, into the webhook URL when it is triggered. Consider the following example:
Say you specified the URL for your webhook with the {issue.key}
variable, like this:
1 2https://service.example.com/webhook/{issue.key}
If an issue with the key EXA-1
triggers a webhook, then the URL that will be used is:
1 2https://service.example.com/webhook/EXA-1
Note that variables are only available to a webhook in the context of its registered events. For example, {issue.key}
and {issue.id}
are available to webhooks registered for events related to issues.
If a webhook is sent to its callback URL but fails, Jira Cloud will attempt to resend it up to five times. Each attempt following the failure is delayed with a randomized back-off algorithm and can happen between 5 and 15 minutes after the previous one.
Retries are attempted when any of the following are true:
This means that some webhooks might be delivered more than once (if the delivery acknowledgment fails).
Every webhook contains the X-Atlassian-Webhook-Identifier
header that provides an identifier for a webhook.
This identifier is unique within a Jira Cloud tenant and is the same across retries.
After you have processed a webhook, you can use the identifier to filter out retries.
The X-Atlassian-Webhook-Retry
header with the current retry count is included with webhooks that have been retried.
Monitor this header and cross-reference it with the callback server logs to stay on top of any unexpected reliability problems.
After we send a webhook and receive a failure, we log the time of failure. Every successful response from the recipient removes that log entry. However, if the recipient doesn’t respond successfully within 30 minutes, we only try to deliver a single attempt per webhook until we record a successful delivery.
Webhooks that are not delivered after the maximum number of retries can be found with the Get failed webhooks operation.
Each webhook contains X-Atlassian-Webhook-Flow
header with Primary
or Secondary
value.
All Primary
webhooks should be delivered within 30 seconds.
All Secondary
webhooks are a result of long-lasting bulk or cascade operation (bulk issue update, project deletion, issue deletion etc.).
For those webhooks, the expected delivery time requirements are relaxed, and the delivery should take no more than 15 minutes.
Note, in those cases, the top level webhook is transferred via Primary
flow, and all dependent webhooks are transferred using the Secondary
flow.
For example, when deleting an issue, the issue_deleted
event is transferred as Primary
but all dependent commend_deleted
, attachment_deleted
issuelink_deleted
etc. are Secondary
.
To make it easier for apps to process a large number of webhooks sent as a result of a sudden influx of events in Jira, a concurrency limiter is applied.
For Primary
webhooks, a default maximum number of 20
concurrent requests per tenant + webhook URL host
pair is allowed.
For Secondary
webhooks the default limit is 10
.
These limits can be changed without notice, depending on the tenant and the webhook host.
When integrating with Jira webhooks, it's important to understand that the scale of webhook traffic varies depending on the size of the Jira Cloud instance where the app is installed. Larger instances generate higher traffic, especially during batch operations or cascade effects, such as project or issue deletion, which can lead to significant traffic spikes.
Webhook recipients should ideally respond to webhook requests as quickly as possible and schedule their work on their end for further asynchronous processing (for example by enqueuing the message). This approach is crucial for maintaining high throughput, as it allows Jira to send subsequent webhooks without additional delay, working efficiently within the concurrency limits set by Jira Cloud.
Failing to consume webhook traffic quickly enough could result in recipient errors, which may exhaust the number of retries Jira attempts. This leads to missed webhooks and may degrade the user experience of the app. Therefore, your system must take traffic spikes into account and ensure reliable processing when these occur.
To trace the origin of a webhook, Connect apps can attach the additional X-Atlassian-Webhook-Trace
HTTP header with any value
consisting of a string of up to 1024 printable ASCII characters to a REST API request.
The header and its value are then attached to every webhook sent from Jira for the REST API request.
The app can use the webhook trace header to, for example:
In addition to declaring webhooks statically in the app descriptor, Connect and OAuth 2.0 apps can register webhooks dynamically using the REST API. This is useful where you need to observe issues based on how users define and amend the issues' content. For example, you may want to initiate other processing when an issue is given a certain label.
Limitations apply to the REST API in order to guarantee stability and performance of Jira Cloud for all apps and users.
The following subset of webhook events is supported:
jira:issue_created
jira:issue_updated
jira:issue_deleted
comment_created
comment_updated
comment_deleted
issue_property_set
issue_property_deleted
These are all events that allow JQL filtering. Webhooks for other events can be registered in the app descriptor.
Only a subset of JQL clauses is supported. This enables us to optimize event matching on the Jira side.
Supported clauses (left-hand side values):
issueKey
project
issuetype
status
priority
assignee
reporter
issue.property
(any indexed issue property can be queried)cf[id]
(for custom fields – currently only the ID of the Epic custom field is supported here)Supported operators: =
, !=
, IN
, and NOT IN
.
If you would like to use more clauses or operators, please contact Developer and Marketplace support.
In order to avoid sending webhooks no one listens to anymore, we assign each webhook an expiration date after which the webhook becomes inactive. The expiration period is 30 days from the time the webhook was created or refreshed using the Extend webhook life REST resource.
Webhooks are available for up to 3 months after they expire. It is not necessary to keep a Webhook alive during this period, you can refresh it when it is needed again.
To register webhooks through the REST API, make a POST
request to https://your-domain.atlassian.net/rest/api/2/webhook
. The registered URL must use the same base URL as the app. An example request body is shown below:
1 2{ "url": "https://your-app-base-url.com/webhook-received", "webhooks": [ { "jqlFilter": "project = PRJ AND status = Done", "events": ["jira:issue_created", "jira:issue_updated"] }, { "jqlFilter": "status = Done", "events": ["jira:issue_updated"] }, { "jqlFilter": "myClause = something", "events": ["jira:issue_deleted"] } ] }
This request will try to register three webhooks. The response will be a JSON array with a result for every webhook. In this case, we will likely succeed in registering the first two, while the third one will fail due to the unsupported JQL clause. The response will then look like this:
1 2[ { "createdWebhookId": 1000 }, { "createdWebhookId": 1001 }, { "errors": ["The clause myClause is unsupported"] } ]
To receive a registered webhook, an app must be granted all the scopes for all the events in the webhook. This table shows the scopes required for each event.
Webhook type | Required scopes |
---|---|
jira:issue_created | read:issue-details:jira |
jira:issue_updated | read:issue-details:jira |
jira:issue_deleted | read:issue-details:jira |
comment_created | read:comment.property:jira , read:comment:jira , read:epic:jira-software , read:group:jira , read:issue.property:jira , read:issue-type:jira , read:project:jira , read:project-role:jira , read:status:jira , and read:user:jira |
comment_updated | read:comment.property:jira , read:comment:jira , read:epic:jira-software , read:group:jira , read:issue.property:jira , read:issue-type:jira , read:project:jira , read:project-role:jira , read:status:jira , and read:user:jira |
comment_deleted | read:comment.property:jira , read:comment:jira , read:epic:jira-software , read:group:jira , read:issue.property:jira , read:issue-type:jira , read:project:jira , read:project-role:jira , read:status:jira , and read:user:jira |
issue_property_set | read:epic:jira-software ,read:issue.property:jira ,read:issue-type:jira ,read:project:jira ,read:status:jira , and read:user:jira |
issue_property_deleted | read:epic:jira-software ,read:issue.property:jira ,read:issue-type:jira ,read:project:jira ,read:status:jira , and read:user:jira |
To fetch webhooks registered through the REST API, make a GET
request to https://your-domain.atlassian.net/rest/api/2/webhook
.
This API uses pagination. The query parameters startAt
and maxResults
can be provided to specify the desired page, or the default values of 0
and 100
will be used.
To delete webhooks registered through the REST API, a DELETE
request can be made to https://your-domain.atlassian.net/rest/api/2/webhook
.
In your request, include the IDs of the webhooks to be deleted:
1 2{ "webhookIds": [1000, 1001] }
IDs corresponding to non-existent webhooks are ignored.
Webhooks registered with the REST API expire after 30 days. Because of that, it's necessary to periodically call the Extend webhook life API to keep them alive. Each call to the API extends the expiration date by another 30 days.
The format is the same as for webhooks declared in the Connect app descriptor,
but with an additional property specifying which webhooks were matched for the event.
For example, if an event happens in Jira that matches both JQL filters of the webhooks registered,
which were successfully registered and assigned the IDs of 1000
and 1001
,
the webhook data JSON will contain the additional matchedWebhookIds
root level property:
1 2{ ... usual webhook data ..., "matchedWebhookIds": [1000, 1001] }
An example Connect app that uses the REST API to register webhooks, fetch registered webhooks, delete them, and handle them, can be found here.
Webhooks for OAuth 2.0 apps are secured by bearer authentication. The token is present in the Authorization
header and is signed with the app's client secret.
Remember to verify the token to keep your integration secure. The use of a library to verify the token is recommended.
A list of suitable libraries can be found on the JWT website.
Example of a valid request body
1 2{ "name": "my first webhook via rest", "description": "description of my first webhook", "url": "https://www.example.com/webhooks", "events": [ "jira:issue_created", "jira:issue_updated" ], "filters": { "issue-related-events-section": "Project = JRA AND resolution = Fixed" }, "excludeBody" : false, "secret" : "G8j4166a5OkXRD4WbqV3" }
Where secret
is an optional string used to generate a signature
and verify incoming webhook payloads. You can’t retrieve your secret after you generate it - if you lose it, you have to get a new one.
Example of a valid response body
1 2{ "name": "my first webhook via rest", "description": "description of my first webhook", "url": "https://www.example.com/webhooks", "excludeBody": false, "events": [ "jira:issue_created", "jira:issue_updated" ], "filters": { "issue-related-events-section": "Project = JRA AND resolution = Fixed" }, "enabled": true, "self": "https://your-domain.atlassian.net/rest/webhooks/1.0/webhook/72", "lastUpdated": 1706692865788, "isSigned": true }
Where isSigned
is true if a secret
is defined.
To register a webhook using REST:
POST a webhook in JSON format to:
https://your-domain.atlassian.net/rest/webhooks/1.0/webhook
The response will return the webhook in JSON with additional information, including the user that created the webhook, the created timestamp, etc.
To update a webhook using REST:
https://your-domain.atlassian.net/rest/webhooks/1.0/webhook/{webhookId}
Pass null or an empty string to remove the secret
. Omitting the secret
field in a request body for PUT will cause it to stay intact.
To unregister (that is, delete) a webhook using REST:
Execute a DELETE to the following URL:
https://your-domain.atlassian.net/rest/webhooks/1.0/webhook/{webhookId}
The following would delete the webhook with an ID of 70:
1 2curl --user username:password \ -X DELETE \ -H "Content-Type: application/json" \ https://your-domain.atlassian.net/rest/webhooks/1.0/webhook/70
To query a webhook using REST:
To get all webhooks for a Jira instance, perform a GET with the following URL:
https://your-domain.atlassian.net/rest/webhooks/1.0/webhook.
1 2curl --user username:password \ -X GET \ -H "Content-Type: application/json" \ https://your-domain.atlassian.net/rest/webhooks/1.0/webhook
To get a specific webhook by its ID, perform a GET with the following URL:
https://your-domain.atlassian.net/rest/webhooks/1.0/webhook/{webhookId}
The following would get a webhook with an ID of 72:
1 2curl --user username:password \ -X GET \ -H "Content-Type: application/json" \ https://your-domain.atlassian.net/rest/webhooks/1.0/webhook/72
You can secure webhooks registered through the REST API or webhooks page by passing the secret
field.
In some instances, imported admin webhooks with a secret won’t be delivered until you rotate the secret. For more details, see our documentation:
About webhook deliveries
Once your server is configured to receive payloads, it will listen for any delivery that's sent to the endpoint you configured. For security reasons, you should only process deliveries from Jira Cloud. To ensure your server only processes deliveries from Jira Cloud, you need to:
Creating a secret token
You can create a new webhook with a secret token, or you can add a secret token to an existing webhook. When creating a secret token, you should choose a random string of text with high entropy. You can also use the “Generate secret” button to let us generate the secret for you. Make sure to record the secret somewhere secure - it can't be viewed or retrieved once the webhook is saved.
Updating a secret token
You can update the secret token of an existing webhook by editing the webhook. However, be aware that you’ll need to update any integrations using this secret.
Validating webhook deliveries
Jira Cloud will use your secret token to create a HMAC signature and include it in a X-Hub-Signature
header,
formatted as method=signature
, as defined by WebSub.
In your code that handles webhook deliveries, you should calculate the HMAC of the body using your secret token and the hash algorithm specified as the method
.
Then, compare the HMAC that Jira Cloud sent with the expected HMAC that you calculated, and ensure that they match.
For examples showing how to validate the hashes in various programming languages, see the Examples section.
There are a few important things to keep in mind when validating webhook payloads:
method
.Testing the webhook payload validation
You can use the following secret, payload, and method values to verify that your implementation is correct:
secret
: It's a Secret to Everybody
payload
: Hello World!
method
: sha256
If your implementation is correct, the signatures that you generate should match the following signature values:
signature: a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9
X-Hub-Signature
: sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9
You can use your programming language of choice to implement HMAC verification in your code.
Following are some examples showing how an implementation might look in various programming languages.
Note that these examples assume that Jira is using the sha256
hash algorithm. Jira might start using another method for the HMAC in the future. The example code here will start failing if this happens.
Java example
1 2import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HexFormat; public class Main { public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException { final String secret = "It's a Secret to Everybody"; final String payload = "Hello World!"; final String givenSignature = "sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9"; final SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); final Mac mac = Mac.getInstance("HmacSHA256"); mac.init(keySpec); final byte[] digest = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); final HexFormat hex = HexFormat.of(); final String calculatedSignature = "sha256=" + hex.formatHex(digest); if (!MessageDigest.isEqual(calculatedSignature.getBytes(), givenSignature.getBytes())) { System.out.println("Signatures do not match\nExpected signature:" + calculatedSignature + "\nActual: signature: " + givenSignature); } else { System.out.println("Signatures match"); } } }
Python example
1 2import hashlib import hmac secret = "It's a Secret to Everybody" payload = "Hello World!" given_signature = "sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9" hash_object = hmac.new( secret.encode("utf-8"), msg=payload.encode("utf-8"), digestmod=hashlib.sha256, ) calculated_signature = "sha256=" + hash_object.hexdigest() if not hmac.compare_digest(calculated_signature, given_signature): print( "Signatures do not match\nExpected signature:" f" {calculated_signature}\nActual: signature: {given_signature}" ) else: print("Signatures match")
Webhooks can be attached as a workflow post function. This makes it easy to trigger a webhook when an issue makes a workflow transition, for instance when it gets rejected from QA or when it gets resolved.
To add a webhook as a post function to a workflow:
Notes:
Webhooks will be run without a specific user context, for example, all issues will be available to the webhook, rather than having them scoped to a single user.
By default, a webhook will send a request with a JSON callback when it is triggered. If you don't want the JSON body sent, then you will need to select Exclude body when configuring the webhook.
Every callback contains the webhookEvent
ID, timestamp
, and information about the entity associated with the event (for example issue, project, or board). The callback can have additional information, depending on the type of event associated with it.
For example, issue-related events contain the issue_event_type_name
field. This field indicates the Jira event that triggered the callback.
This is the structure of a callback for an issue-related event:
1 2{ "timestamp", "webhookEvent", "issue_event_type_name", "user": { --> See User shape in table below }, "issue": { --> See Issue shape in table below }, "changelog" : { --> See Changelog shape in table below } }
Issue shape |
|
User shape |
|
Changelog shape |
|
Comment shape |
|
The returned fields depend on the webhook type.
The following is an example of the JSON sent in an issue update callback:
1 2{ "issue": { "id":"99291", "self":"https://your-domain.atlassian.net/rest/api/2/issue/99291", "key":"JRA-20002", "fields":{ "summary":"I feel the need for speed", "created":"2009-12-16T23:46:10.612-0600", "description":"Make the issue nav load 10x faster", "labels":["UI", "dialogue", "move"], "priority": { "self": "https://your-domain.atlassian.net/rest/api/2/priority/3", "iconUrl": "https://your-domain.atlassian.net/images/icons/priorities/minor.svg", "name": "Minor", "id": "3" }, } }, "user": { "self":"https://your-domain.atlassian.net/rest/api/2/user?accountId=99:27935d01-92a7-4687-8272-a9b8d3b2ae2e", "accoundId": "99:27935d01-92a7-4687-8272-a9b8d3b2ae2e", "accountType": "atlassian", "avatarUrls":{ "16x16":"https://your-domain.atlassian.net/secure/useravatar?size=small&avatarId=10605", "48x48":"https://your-domain.atlassian.net/secure/useravatar?avatarId=10605" }, "displayName":"Bryan Rollins [Atlassian]", "active" : "true", "timeZone": "Europe/Warsaw", }, "changelog": { "items": [ { "toString": "A new summary.", "to": null, "fromString": "What is going on here?????", "from": null, "fieldtype": "jira", "field": "summary" }, { "toString": "New Feature", "to": "2", "fromString": "Improvement", "from": "4", "fieldtype": "jira", "field": "issuetype" } ], "id": 10124 }, "timestamp": 1606480436302, "webhookEvent": "jira:issue_updated", "issue_event_type_name": "issue_generic" }
If a bulk operation triggers the webhook, the field bulkOperationMetaData
is added to the webhook payload. For example:
1 2{ ... usual webhook data ..., "bulkOperationMetaData": { "sendMail": false } }
If a webhook is triggered successfully, a response code of 200 is returned. All other response codes should be treated as an unsuccessful attempt to trigger the webhook.
issue_created
event instead.issue_deleted
webhooks.attachment_created
webhooks, but they are listed in attachment field in jira:issue_created
webhook’s body.Rate this page: