Creating a Webhook for JIRA

I am trying to create a Webhook that will be processed once a JIRA issue has a status of Closed. I believe I understand how to set up the calling of the webhook from within JIRA, but my issue is with the webhook code itself. I am very new to webhooks, so I started looking online. I found a tutorial from Microsoft that uses ASP.Net packages from NuGet, so I grabbed the bitbucket package and used that as a starting point. I believe I have my server set up ok, and I have published the example to my server. However, it doesn’t seem to be triggered from JIRA. I have not really modified the code from the package but I will include it. I set the breakpoint as the tutorial said it should but it doesn’t break. I added a statement to have it open a different webpage just to see if it would do that but to no avail. The code as it exists looks something like this:

using Microsoft.AspNet.WebHooks;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Threading.Tasks;

namespace Closed_WO.WebHookHandlers
{
    public class BitbucketWebHookHandler : WebHookHandler
    {
        public override Task ExecuteAsync(string receiver, WebHookHandlerContext context)
        {
			// make sure we're only processing the intended type of hook
			if("bitbucket".Equals(receiver, System.StringComparison.CurrentCultureIgnoreCase))
			{
				// todo: replace this placeholder functionality with your own code
				string action = context.Actions.First();
				JObject incoming = context.GetDataOrDefault<JObject>();
                //test to see if this works or not
                System.Diagnostics.Process.Start("http://www.mysite.com");

			}
            
            return Task.FromResult(true);
        }
    }
}

My uri is https://closed_wo.mysite.com/api/webhooks/incoming/bitbucket

In my JIRA webhook, I added a /${issue.key} at the end of the uri in the hopes of having the key passed back.

I am sure that there is a lot I am missing. But I can’t seem to find anything that is really putting it all together for me.

Hopefully someone can steer me in the right direction with a good example or two.

The first thing I do when working with webhooks from anything is to have a good look at the payload and understand how to configure it with what I need. I use RequestBin but there must be dozens of good tools out there. Of course, this assumes your JIRA instance can reach a web tool like that. All JIRA Cloud instances can, but self-hosted instances can be configured to limit network traffic.

Assuming you can see your webhook, you might want to use something like webscript so that you don’t have to worry about configuring and running your own server. Then again, that might depend on what you want to do with the webhook.

Even if you want to run the server locally, I would follow similar steps. Namely, first create a server that just echos to console whatever payload is sent to the URL. Then, start adding logic to it. From the code you provided, it looks like it’s trying to filter to make sure the webhook comes from Bitbucket, but you mentioned you’re trying to send from JIRA. That would be a problem. :wink:

First, thank you for responding. I had no idea that it is possible that JIRA may not be allowing the webhook message to go out. I will check on that since I believe we are self-hosted. I saw nothing on the web that mentioned that.

In regards to Bitbucket, as I mentioned I was using that as a template for trying to get the webhook receiver going. I was hoping that since it was an Altassian product it might be the same, but you indicate that is not the case. So my question then is where do I find out what the JIRA webbook looks like? I assumed that by my setting the URI to have bitbucket at the end it was just an indicator of what to process - I have that on my JIRA webhook so I thought that it was something that would be parsed just so it knew what to do with it when I hit my receiver. Is that not correct? How do I set up my receiver to get the webhook data from JIRA? As I said this is all new to me so I am a bit lost on how to set this all up.

EDIT - I set up the requestbin and changed the URL in the webhook. I added the /${issue.key} to see what it would return. All I got on in the requestbin was “ok” - nothing else. What am I missing? Why is nothing else coming out of JIRA?

I set up the requestbin and changed the URL in the webhook. I added the /${issue.key} to see what it would return. All I got on in the requestbin was “ok” - nothing else. What am I missing? Why is nothing else coming out of JIRA?

RequestBin is very picky about URLs. Appending to the path won’t work. But you can use a query parameter. For example, I just configured to send a webhoook on every issue update to http://requestb.in/1hieuil1?issueId=${issue.id}. When I changed an issue, here’s what I got:

So my question then is where do I find out what the JIRA webbook looks like?

The JIRA docs on webhooks have examples, you just have to open some of the sections that say “Show me” (sorry for the poor doc design, we’re switching to having all code examples in visible blocks, not hidden). But, for reference, here’s what I received in RequestBin:

{
	"timestamp": 1490819671963,
	"webhookEvent": "jira:issue_updated",
	"issue_event_type_name": "issue_updated",
	"user": {
		"self": "https://devpartisan.atlassian.net/rest/api/2/user?username=admin",
		"name": "admin",
		"key": "admin",
		"emailAddress": "ibuchanan@atlassian.com",
		"avatarUrls": {
			"48x48": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=48",
			"24x24": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=24",
			"16x16": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=16",
			"32x32": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=32"
		},
		"displayName": "Ian Buchanan  [Administrator]",
		"active": true,
		"timeZone": "America/Chicago"
	},
	"issue": {
		"id": "10010",
		"self": "https://devpartisan.atlassian.net/rest/api/2/issue/10010",
		"key": "TIS-11",
		"fields": {
			"issuetype": {
				"self": "https://devpartisan.atlassian.net/rest/api/2/issuetype/10002",
				"id": "10002",
				"description": "The sub-task of the issue",
				"iconUrl": "https://devpartisan.atlassian.net/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype",
				"name": "Sub-task",
				"subtask": true,
				"avatarId": 10316
			},
			"parent": {
				"id": "10009",
				"key": "TIS-10",
				"self": "https://devpartisan.atlassian.net/rest/api/2/issue/10009",
				"fields": {
					"summary": "As a developer, I can update story and task status with drag and drop (click the triangle at far left of this story to show sub-tasks)",
					"status": {
						"self": "https://devpartisan.atlassian.net/rest/api/2/status/3",
						"description": "This issue is being actively worked on at the moment by the assignee.",
						"iconUrl": "https://devpartisan.atlassian.net/images/icons/statuses/inprogress.png",
						"name": "In Progress",
						"id": "3",
						"statusCategory": {
							"self": "https://devpartisan.atlassian.net/rest/api/2/statuscategory/4",
							"id": 4,
							"key": "indeterminate",
							"colorName": "yellow",
							"name": "In Progress"
						}
					},
					"priority": {
						"self": "https://devpartisan.atlassian.net/rest/api/2/priority/3",
						"iconUrl": "https://devpartisan.atlassian.net/images/icons/priorities/medium.svg",
						"name": "Medium",
						"id": "3"
					},
					"issuetype": {
						"self": "https://devpartisan.atlassian.net/rest/api/2/issuetype/10000",
						"id": "10000",
						"description": "A user story. Created by JIRA Software - do not edit or delete.",
						"iconUrl": "https://devpartisan.atlassian.net/images/icons/issuetypes/story.svg",
						"name": "Story",
						"subtask": false
					}
				}
			},
			"timespent": null,
			"project": {
				"self": "https://devpartisan.atlassian.net/rest/api/2/project/10000",
				"id": "10000",
				"key": "TIS",
				"name": "Teams in Space",
				"avatarUrls": {
					"48x48": "https://devpartisan.atlassian.net/secure/projectavatar?avatarId=10324",
					"24x24": "https://devpartisan.atlassian.net/secure/projectavatar?size=small&avatarId=10324",
					"16x16": "https://devpartisan.atlassian.net/secure/projectavatar?size=xsmall&avatarId=10324",
					"32x32": "https://devpartisan.atlassian.net/secure/projectavatar?size=medium&avatarId=10324"
				}
			},
			"fixVersions": [{
				"self": "https://devpartisan.atlassian.net/rest/api/2/version/10001",
				"id": "10001",
				"name": "Version 2.0",
				"archived": false,
				"released": false,
				"releaseDate": "2016-09-15"
			}],
			"aggregatetimespent": null,
			"resolution": null,
			"customfield_10310": null,
			"customfield_10311": null,
			"customfield_10312": null,
			"customfield_10027": null,
			"customfield_10302": null,
			"customfield_10303": null,
			"customfield_10304": null,
			"customfield_10305": null,
			"customfield_10306": null,
			"customfield_10307": null,
			"customfield_10308": null,
			"resolutiondate": null,
			"customfield_10309": null,
			"workratio": -1,
			"lastViewed": "2017-03-29T15:34:13.929-0500",
			"watches": {
				"self": "https://devpartisan.atlassian.net/rest/api/2/issue/TIS-11/watchers",
				"watchCount": 0,
				"isWatching": false
			},
			"created": "2016-09-04T14:47:29.278-0500",
			"customfield_10021": ["com.atlassian.greenhopper.service.sprint.Sprint@af21bd[id=1,rapidViewId=1,state=ACTIVE,name=Sample Sprint 2,goal=<null>,startDate=2016-09-01T02:45:31.321-05:00,endDate=2016-09-15T03:05:31.322-05:00,completeDate=<null>,sequence=1]"],
			"customfield_10022": "0|i00027:",
			"priority": {
				"self": "https://devpartisan.atlassian.net/rest/api/2/priority/4",
				"iconUrl": "https://devpartisan.atlassian.net/images/icons/priorities/low.svg",
				"name": "Low",
				"id": "4"
			},
			"customfield_10100": null,
			"customfield_10023": null,
			"customfield_10024": [],
			"customfield_10300": null,
			"customfield_10025": null,
			"customfield_10301": null,
			"customfield_10026": null,
			"labels": [],
			"customfield_10016": null,
			"customfield_10017": null,
			"timeestimate": null,
			"aggregatetimeoriginalestimate": null,
			"versions": [],
			"issuelinks": [],
			"assignee": {
				"self": "https://devpartisan.atlassian.net/rest/api/2/user?username=admin",
				"name": "admin",
				"key": "admin",
				"emailAddress": "ibuchanan@atlassian.com",
				"avatarUrls": {
					"48x48": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=48",
					"24x24": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=24",
					"16x16": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=16",
					"32x32": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=32"
				},
				"displayName": "Ian Buchanan  [Administrator]",
				"active": true,
				"timeZone": "America/Chicago"
			},
			"updated": "2017-03-29T15:34:31.796-0500",
			"status": {
				"self": "https://devpartisan.atlassian.net/rest/api/2/status/3",
				"description": "This issue is being actively worked on at the moment by the assignee.",
				"iconUrl": "https://devpartisan.atlassian.net/images/icons/statuses/inprogress.png",
				"name": "In Progress",
				"id": "3",
				"statusCategory": {
					"self": "https://devpartisan.atlassian.net/rest/api/2/statuscategory/4",
					"id": 4,
					"key": "indeterminate",
					"colorName": "yellow",
					"name": "In Progress"
				}
			},
			"components": [],
			"timeoriginalestimate": null,
			"description": null,
			"customfield_10010": null,
			"customfield_10011": null,
			"customfield_10012": null,
			"customfield_10013": null,
			"customfield_10014": "Not started",
			"timetracking": {},
			"customfield_10005": null,
			"customfield_10006": null,
			"customfield_10007": null,
			"customfield_10008": null,
			"attachment": [],
			"customfield_10009": null,
			"aggregatetimeestimate": null,
			"summary": "Update task status by dragging and dropping from column to column >> Try dragging this task to \"Done\"",
			"creator": {
				"self": "https://devpartisan.atlassian.net/rest/api/2/user?username=admin",
				"name": "admin",
				"key": "admin",
				"emailAddress": "ibuchanan@atlassian.com",
				"avatarUrls": {
					"48x48": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=48",
					"24x24": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=24",
					"16x16": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=16",
					"32x32": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=32"
				},
				"displayName": "Ian Buchanan  [Administrator]",
				"active": true,
				"timeZone": "America/Chicago"
			},
			"subtasks": [],
			"reporter": {
				"self": "https://devpartisan.atlassian.net/rest/api/2/user?username=admin",
				"name": "admin",
				"key": "admin",
				"emailAddress": "ibuchanan@atlassian.com",
				"avatarUrls": {
					"48x48": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=48",
					"24x24": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=24",
					"16x16": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=16",
					"32x32": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=32"
				},
				"displayName": "Ian Buchanan  [Administrator]",
				"active": true,
				"timeZone": "America/Chicago"
			},
			"customfield_10000": null,
			"aggregateprogress": {
				"progress": 0,
				"total": 0
			},
			"customfield_10001": null,
			"customfield_10320": null,
			"customfield_10200": null,
			"customfield_10003": "{}",
			"customfield_10313": null,
			"customfield_10314": null,
			"customfield_10315": null,
			"environment": null,
			"customfield_10316": null,
			"customfield_10317": null,
			"customfield_10318": null,
			"duedate": null,
			"customfield_10319": null,
			"progress": {
				"progress": 0,
				"total": 0
			},
			"comment": {
				"comments": [{
					"self": "https://devpartisan.atlassian.net/rest/api/2/issue/10010/comment/10002",
					"id": "10002",
					"author": {
						"self": "https://devpartisan.atlassian.net/rest/api/2/user?username=admin",
						"name": "admin",
						"key": "admin",
						"emailAddress": "ibuchanan@atlassian.com",
						"avatarUrls": {
							"48x48": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=48",
							"24x24": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=24",
							"16x16": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=16",
							"32x32": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=32"
						},
						"displayName": "Ian Buchanan  [Administrator]",
						"active": true,
						"timeZone": "America/Chicago"
					},
					"body": "To Do to In Progress 3 days 21 hours 8 minutes ago",
					"updateAuthor": {
						"self": "https://devpartisan.atlassian.net/rest/api/2/user?username=admin",
						"name": "admin",
						"key": "admin",
						"emailAddress": "ibuchanan@atlassian.com",
						"avatarUrls": {
							"48x48": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=48",
							"24x24": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=24",
							"16x16": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=16",
							"32x32": "https://secure.gravatar.com/avatar/7581132d18239f9356e14a82c84da336?d=mm&s=32"
						},
						"displayName": "Ian Buchanan  [Administrator]",
						"active": true,
						"timeZone": "America/Chicago"
					},
					"created": "2016-09-04T14:47:29.277-0500",
					"updated": "2016-09-04T14:47:29.277-0500"
				}],
				"maxResults": 1,
				"total": 1,
				"startAt": 0
			},
			"votes": {
				"self": "https://devpartisan.atlassian.net/rest/api/2/issue/TIS-11/votes",
				"votes": 0,
				"hasVoted": false
			},
			"worklog": {
				"startAt": 0,
				"maxResults": 20,
				"total": 0,
				"worklogs": []
			}
		}
	},
	"changelog": {
		"id": "11400",
		"items": [{
			"field": "priority",
			"fieldtype": "jira",
			"from": "3",
			"fromString": "Medium",
			"to": "4",
			"toString": "Low"
		}]
	}
}

How do I set up my receiver to get the webhook data from JIRA?

Although I’ve done a fair bit of C#, it’s been a couple years. For that matter, I never wrote a WebHookHandler. However, I can’t help but wonder if adding the issue to the path was a problem for your code, just like it was for RequestBin. Removing the conditional and all assumptions about the shape of the context, maybe something like the following will start to yield more insights (sorry, I don’t have an environment for running this, so consider it more psuedo-code.)

public class JiraHandler : WebHookHandler
{
    public override Task ExecuteAsync(string receiver, WebHookHandlerContext context)
    {
        Console.Write(receiver);
        JObject data = context.GetDataOrDefault<JObject>();
        Console.Write(data);
  
        return Task.FromResult(true);
    }
}

Once we get this worked out, I hope you’ll post your working source code as an open-source project. It would be a great help to have such a project for reference!

Thank you for your response. I did get something into RequestBin, but it is only the headers. The Raw Body part says none - which is the package data that I need to get the key out. One other thing that I seem to have stumbled on is that there seems to be add-ons needed to send to a secured site (https). Is that true? If so I will have our admin guys do that, since in the future I will want it to go to our secured site.

Is there something in the configuration of the webhook where I tell it to include the body? I see where it says “Exclude” but that is unchecked, so I would assume it would include the body.

Looking at what I did get in RequestBin, it appears the webhook is sending a GET instead of a POST:

Why is it sending a GET instead of a POST?

I don’t think that was a webhook. The User-Agent header suggests a browser. Notice the same header from my request where it has strings indicating it came from Atlassian JIRA. This would also explain why there’s a GET instead of POST. I think we still need to get the JIRA configuration right before we worry about code.

Do you have any JQL configured? If so, we should drop it for now. And, for now, let’s focus on the simple trigger of an issue update. As you’ve already indicated, make sure the exclude body option is “No”. Here’s what my configuration looks like:

If this still fails, I suspect there’s some network gateway between JIRA and RequestBin, like a web proxy or a firewall.

Excellent thought - I did have JQL in it. After removing the JQL I got the body in RequestBin. Thanks so much!

I am now working on creating the application to accept and read this. I am not sure which way to go. I started with the Bitbucket version to get some sample code, but then I found a MS page that gives examples of creating a basic WCF Web HTTP Service in VB.Net (I am better with VB than C#). I am thinking the more simple way is better. Once I get this going I will post my code here to share.

Well, I am still at a loss for how to make this really work. I can’t find a generic webhook example out there. If I try to use the bitbucket NuGet setup JIRA gives me errors when I try to send it out. There is no ability to set up secrets in the JIRA webhook, so I assume the non-secured is the best way to go. I know it works with the Requestbin setup, but I can’t seem to get anything else to work. I can’t find a custom Webhook receiver example that tells me exactly how to set it up. Everything I find points to GitHub, dropbox. or some other application which I don’t want to use.

Is Atlassian planning on making a NuGet for JIRA? I need support on this - I am getting pressure to get this working. I am willing to pay for support to get this done.

Please tell me what I need to do to get help. Surely someone has done things with JIRA webhooks since you have the ability to set them up. Help!!!

NOTE: One thing I have noticed is that I get these two errors when I try to have the hook sent:

JIRA Does Not Start after Configuring SSL Due to Unspecified TrustStore or Keystore Path
and
Unable to Connect to SSL Services due to PKIX Path Building Failed sun.security.provider.certpath.SunCertPathBuilderException

At first I had my website set to http thinking this would help. It didn’t so I changed it to https and I still get these errors. I talked to our JIRA admin guys here and they say that we are on the most current version and the add-ins the help talks about won’t work with the new version. They also said we are set up for SSL, so I am not sure why I am getting these errors.

Have you made any progress on this? I see that you are Inheriting from the WebHookHandler base class but it looks like you need to first implement the IWebHookReceiver to provide an implementation to actually process the incoming post request. https://github.com/aspnet/WebHooks/blob/master/src/Microsoft.AspNet.WebHooks.Receivers/WebHooks/IWebHookReceiver.cs