401 Authorization Error when trying to get Confluence Server Id

I may be going about this the wrong way, but I am trying to build a multi-app forge app, where a confluence page is created when an action occurs in Jira. I looked at the “cross app” example tutorial but it requires a hard-coded server id and has no indication of how to get the linked confluence server id in a real world app.

I have the following permissions in manifest.yml:

permissions:
  scopes:
    - read:jira-work
    - write:page:confluence
    - storage:app
    - manage:jira-configuration
    - manage:jira-webhook

I get a 401 error with response of { code: 401, message: 'Unauthorized; scope does not match' }. I have researched but can not find an alternative scope that may work here.

I can confirm the REST endpoint works and returns expected JSON response with the confluence server id as needed, using CURL on the CLI. On the surface this looks like a scope/perms error, but I can figure out what the correct scope should be.

Is this simply not possible in forge and forge is displaying an incorrect error (i.e. there is no scope that makes this possible and it’s not a scope problem), is it something I have missed, or is there an alternative working way to get dynamically get the correct server id for confluence? Or can one post and create pages without the server id of the linked confluence instance?

This is the code I am using in order to try to get the server ID of the linked confluence server:

async function configureLinkedConfluence() {
  const response = await api.asApp().requestJira(route`/rest/applinks/2.0/listApplicationlinks`, {
    method: 'GET',
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch application links: ${response.status} ${response.statusText}`);
  }

  const responseBody = await response.json();
  const appLinks = responseBody.list;

  // Find the Confluence server ID
  const confluenceLink = appLinks.find(link => link.application.typeId === 'confluence');
  if (!confluenceLink) {
    throw new Error('No linked Confluence site found in Jira application links.');
  }

  const serverId = confluenceLink.application.id;
  await storage.set('linkedConfluenceServerId', serverId);
  console.log(`Linked Confluence URL stored: ${serverId}`);

  return serverId;
}

Hello @DavidR,
For your app to be able to call Jira REST API from Confluence, your app has to be deployed and installed in both Confluence and Jira.

Hope that helps :wink: !

Sorry, that doesn’t help. I want to call a confluence api from Jira. My app is installed on both products.

I need to create a confluence page FROM Jira. In order to do that, it seems I need to know the server ID of the linked confluence instance. The example tutorial from atlassian had the api call with a hard coded server id which does not work in the real world (you can’t ask every marketplace user to look up their sever id manually)

Why not? How many companies using Jira CLOUD still have Confluence SERVER in use and need to link to multiple instances of it? It’s a discontinued, unsupported product.

It works with OAuth 2.0? What documentation about that non-public, internal endpoint describes that?

It’s not returning XML? Can you provide a sample of that JSON response you got via cURL?

You’ve installed a Forge app on Confluence SERVER??

The app link API is not ‘public’. As an app you can’t call it.

Also, the Marketplace does not support cross product apps. You can either distribute it via installation links or you have to split your app into two parts, but that means your Jira app can’t create a page directly.

1 Like

Sounds like there’s a mix up of understanding here.

I am talking about Jira CLOUD and Confluence CLOUD.

It appears to create a page using the “forge api” addPage in confluence you need to add server id which indicates which cloud instance the Forge Cloud app is in. Could be wrong here, but I am going on the docs and the atlassian provided tutorial.

Again I use the phrase “server id” because that is what the attribute is called in the forge addPage api when being called from Jira forge app.

And again to clarify I understand the limitations of cross product forge apps. However there is literally an example of what I am trying to do here, by atlassian. However their example has a hard coded server id.

Future repliers, PLEASE read the above tutorial to understand clearer.

I am talking about cloud only, with a forge app installed on both. And, for example I have used other marketplace apps that work together without this type of configuration (Jira charts for confluence, and script runner as two examples)

I just read that tutorial, and the thing called addPage() isn’t an “api”, but it looks to me to be a function, which isn’t called by a “Jira forge app” but is called by a MACRO, which uses a statically declared constant called jiraServerId for the sole purpose of declaring an AC parameter in the macro’s definition (the equivalent of the macro’s header).

That jiraServerId constant doesn’t play any role in the requestConfluence() method called by the macro, which creates the page in Confluence. This matches with the documentation about that method, which also makes no mention of any requirement for using a server ID.

Your results may differ.

I think the tutorial you are following is very wrong or completely outdated (thanks Atlassian).

Firstly, if you create a Jira macro in Confluence, and then inspect the storage format, you will not find any serverId but instead a cloudId, which is the identifier used by Atlassian to identify a site.
The cloudId is available in most, if not all, Forge context.

Secondly, to create a Confluence page from Jira (on the same site), you “just” need the right permissions in your app:

...
permissions:
  scopes:
...
    #    Confluence
    - write:page:confluence
...

Then install your app in both Confluence and Jira.

The call to create the page looks like (code from a POC I made a few month ago):

  export async function createPage(payload: CreatePagePayload, context: ActionContext): Promise<string> {
      console.log("createPage", payload, context);
      const { content, title } = payload;
  
      if (content == null || content === "") {
          return "Missing content";
      }
      if (title == null || title === "") {
          return "Missing title";
      }
  
      // Convert the article to a Confluence compatible format
      const convertResponse = await api.asUser().requestConfluence(route`/wiki/rest/api/contentbody/convert/storage`, {
          method: "POST",
          headers: {
              Accept: "application/json",
              "Content-Type": "application/json",
          },
          body: JSON.stringify({
              value: payload.content,
              representation: "editor",
          }),
      });
  
      console.log(`convertResponse: ${convertResponse.status} ${convertResponse.statusText}`);
      const confluenceContent = await convertResponse.json();
      // console.log("confluenceContent", confluenceContent);
  
      // Create page
      const pageBody = {
          spaceId: SPACE_ID,
          status: "current",
          title,
          parentId: PARENT_PAGE_ID,
          body: {
              representation: "storage",
              value: confluenceContent.value,
          },
      };
  
      const pageResponse = await api.asUser().requestConfluence(route`/wiki/api/v2/pages`, {
          method: "POST",
          headers: {
              Accept: "application/json",
              "Content-Type": "application/json",
          },
          body: JSON.stringify(pageBody),
      });
  
      console.log(`pageResponse: ${pageResponse.status} ${pageResponse.statusText}`);
      const pageResult = await pageResponse.json();
      console.log(pageResult);
  
      const pageUrl = pageResult._links.base + pageResult._links.webui;
  
      return pageUrl;
  }

Thanks @SilvreLestang . This is the most promising so far! As long as the cloudId is the same on Jira as it is confluence, I think this will solve it.

I have paused this path for now, and am tracking an alternate path that doiesn’t use confluence for now - but will circle back when I can and confirm!

Really appreciated!