Jira API returns 400 all the time in the installed lifecycle callback

I’m developing a Connect app and I would like to call Jira API from callbacks. In this case, I’m trying to PUT a migration webhook in the installed lifecycle callback. But I also tried different GET methods like getting all registered webhooks or getting a project by project key. Yet in all those cases, Jira returns 400 with no specific message. This opens many questions in my head:

  • Can I call Jira API in the installed callback? Edit: No, I cannot (my last post has a solution).
  • Is 400 returned when JWT is incorrectly created?
  • Is it a problem with headers?
  • Is my request wrong?

Maybe the answer is simple but just for more information, this is my request (variable jwt is my custom script, method fetch is from a cross-fetch library):

let jwtToken = jwt.generateToken(
  const bodyData = '{"endpoints": ["https://my-cloud-app.com/migration-event"]}';
  const response = await fetch("https://my-jira.atlassian.net/rest/atlassian- connect/1/migration/webhook", 
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "JWT " + jwtToken
    body: bodyData

The JWT token is generated using the Atlassian Javascript library atlassian-jwt (variable atlassianJwt in the code). The input is the HTTP method, URL, and shared secret received in the installed callback. This is how it’s generated:

const generateToken = (method, url, secret) => {
  const now = moment().utc();
  const request = atlassianJwt.fromMethodAndUrl(method, url);

  const tokenData = {
    "iss": "cz.closeit.atlassian.subtasks-navigation",
    "iat": now.unix(),
    "exp": now.add(5, 'minutes').unix(),
    "qsh": atlassianJwt.createQueryStringHash(request)
  return atlassianJwt.encodeSymmetric(tokenData, secret, "HS256");

The 400 is not very specific so to get this status, even JWT could be bad I guess. But in that case, I would expect the 401. At this point, I just have no clue where the problem is so any help would be appreciated.

Thank you.

1 Like

Hi @ludekN,

One small point, in your code you have a space atlassian- connect between atlassian- and connect.

Otherwise I can’t see an issue with the code. As an idea, print the jwt to the console and check it with a tool like https://jwt.io/ (although it does mean potentially sharing your secret.)

Then try creating a curl comment and run it with curl --verbose to see all the headers to make sure it’s all correct. The HTTP 400 means something in the query is malformed.


1 Like

Hello @jrichards,
thank you for your reply. The space is just a mistake that happened when inserting the code to the post.

I tried the curl and also Postman. Both requests are successful, so JWT must be correct. It’s just the node-fetch and Axios that somehow fail to execute the request correctly. Or they do something differently.

Update: After a lot of testing, I realized that I probably cannot call Jira API in the installed callback. I was calling the Jira API in the installed controller (AWS lambda function) just after the code that was saving shared secret to the database. But at this point, the plugin installation isn’t finished yet (the app key is probably not registered in the cloud Jira yet). I tried to call Jira API from a different function AFTER the successful installation and it returned 200 with the correct response.

So the solution for me and future readers with the same problem is this - do not call Jira API in your installed handler. Just validate the JWT from Jira, save the shared secret to the database and return 200. This will finish the installation. You can then trigger your code (for example using the enabled lifecycle callback) that calls Jira API (for example registration of cloud migration webhook).

1 Like

Hi @ludekN,

Ah, now I understand. Yeah, don’t use /installed because nothing is ready yet. I recommend using /enabled, which is what you’ve found. I see now you stated you were using installed and my brain didn’t connect with my long term memory storage about that.

In the documentation we do suggest the enabled event

Use the migration/webhook endpoint to register your webhooks after your app has received the enabled lifecycle event.

Anyway, good to hear the magic worked.