Creating Atlassian Confluence pages with Postman

John Wheeler
9 min readJun 15, 2020

I’ve been using Confluence now for a little over 2 years and though it has it’s shortcomings, it does a decent job at helping me organize content. It also provides a good API that allows me to do things the software can’t do natively.

One of the things I find myself doing is creating a lot of similar content. I tend to use page properties often and I have a table of page properties to track different attributes of a document.

To create and update these pages I use postman. Postman is the missing tool in a traditional office productivity suite. If you can build a spreadsheet in google sheets, then you can use postman.

As I continue build our security program I want to map controls back to specific frameworks. This article focuses on loading pages into Confluence that represent ISO27001 Annex A controls. I used the spreadsheet here to create the Confluence pages.

Setup Atlassian

Getting an API token for Confluence is pretty easy. Follow these steps to create the token.

  1. Navigate here
  2. Click “Create API token”
  3. Name the token (I usually name it the created date)
  4. Copy the token to a safe location.

I use Lastpass to store the token for later retrieval. Once you have a token you need to configure postman.

Setting Up Postman

If you don’t have Postman installed, start by visiting the postman site and download the latest version. Once you have postman installed I recommend creating an environment.

Redacted Atlassian Evnironment

I named my environment “Jira” but the name is arbitrary. Additionally, I chose the name “jira_user” and “jira_token” but these are arbitrary as well. “jira_token” is the token you obtained from the previous steps above. Finally, I recommend you create a “jira_host” that represents your Confluence instance. The Confluence API uses basic authentication so configure that in Postman below. Once you’ve created an environment you need to test your credentials.

Testing Credentials

Now that you have you’re credentials in an environment you can test API access by requesting a list of your spaces.

Confluence credential test

Postman uses double angle brackets to replace values with environment variables. So the values defined in the previous step can be used here. You can see in the box labeled “1” above, I’ve typed in “{{jira_user}}” and box “2” above I’ve type in “{{jira_token}}”. I’ve also replaced the hostname portion of the test URL I’m using with “{{jira_host}}”. I chose to test with the “space” endpoint, but you could choose any Confluence endpoint you like. Note that I’ve selected “Basic Auth” as the authentication type. Once you’ve entered the URL, username, and token click send and you should see a status of 200 as in box “4” above and some output shown in arrow “5”.

Common errors are are your credentials, mistyped user name or password. Be sure that your token doesn’t have a leading or trailing space.

Creating a Template Page

To create bulk pages in Confluence, I use a standard Confluence page as a template. Though I’d like to use a confluence template, there was no api method to create a page from template at the time of this writing. I spent time building a working page with the data that would be part of the page and settled on the following layout.

Confluence editor of template page

The content is fairly straight forward. I have an equally divided page with page properties on the left and a few meta data items on the right including a warning that page is auto generated, a table of contents, and a citation table. Finally the bottom contains a JQL query to show company controls related to this control. Here is what the page looks like rendered.

Rendered page in Confluence

It took a bit of trial an error to finalize on this format. A few things to note:

  • The JQL query to reference a control on the bottom of the page overlaps the citations table. I wanted to ensure that I was properly tagging controls as they were documented and implemented. My goal is to tag controls with the appropriate ISO control and have them show on this page to understand coverage and gaps.
  • The citations table will likely include more than just controls as processes and procedures may also reference this control.
  • The page name is a concatenation of the control reference and the control name.
  • The only data that will change on each page is the data in the page properties table, the JQL query, and the page labels.

Further changes to this page can be made to the template and then the pages can be auto generated following the process below.

Get the template using postman

Now that we have a page, we can see what it looks like in JSON format.

Postman GET of template page

This is the content endpoint, specifically we are requesting /wiki/rest/api/content/search with the following query parameters

  • expand = body.storage,metadata.properties,history.nextVersion,version,metadata.labels,space
  • cql = title=”A.8.1.3 Acceptable use of assets” and space=SEC and type=page

Use the same authentication and instance name from our test above. This query is important because we’ll use the actual content to build the page later with Postman. You can find the actual content if you scroll down in the response section till you see the following

Content body of Confluence page

looking at the “body” attribute under “storage” then “value” you can see the actual content on that page. This is one complete line in the response. I’ve copied it below. You can see where the text I’ve placed is in the template and the various sections in the page.

"<ac:layout><ac:layout-section ac:type=\"two_equal\"><ac:layout-cell><h2>Definition</h2><p><br /></p><ac:structured-macro ac:name=\"details\" ac:schema-version=\"1\" ac:macro-id=\"ad531b4d-a4a1-4000-b69e-0e097244798b\"><ac:rich-text-body><p><br /></p><table data-layout=\"default\"><colgroup><col /><col /></colgroup><tbody><tr><th><p><strong>Domain</strong></p></th><td><p>Asset management</p></td></tr><tr><th><p><strong>Sub Domain</strong></p></th><td><p>Responsibility for assets</p></td></tr><tr><th><p><strong>Control Name</strong></p></th><td><p>Acceptable use of assets</p></td></tr><tr><th><p><strong>Control Reference</strong></p></th><td><p>A.8.1.3</p></td></tr><tr><th><p><strong>Control Description</strong></p></th><td><p>Rules for the acceptable use of information and of assets associated with information and information processing facilities shall be identified, documented and implemented.</p></td></tr></tbody></table><p class=\"auto-cursor-target\"><br /></p></ac:rich-text-body></ac:structured-macro><p class=\"auto-cursor-target\"><br /></p></ac:layout-cell><ac:layout-cell><p class=\"auto-cursor-target\"><br /></p><ac:structured-macro ac:name=\"note\" ac:schema-version=\"1\" ac:macro-id=\"19b3a146-341a-472e-bdaa-45febcbd438b\"><ac:rich-text-body><p>This page is auto generated. Changes may be overwritten.</p></ac:rich-text-body></ac:structured-macro><ac:structured-macro ac:name=\"toc\" ac:schema-version=\"1\" ac:macro-id=\"5afbd337-9992-439b-8028-051f4b94eeb0\" /><ac:structured-macro ac:name=\"qc-doc-citation-macro\" ac:schema-version=\"1\" ac:macro-id=\"d6d39a49-f9fa-47f0-b0c5-c01ee4d5ae1f\" /></ac:layout-cell></ac:layout-section><ac:layout-section ac:type=\"single\"><ac:layout-cell><h2>Controls</h2><ac:structured-macro ac:name=\"detailssummary\" ac:schema-version=\"3\" ac:macro-id=\"ffcbafc6-5f99-4374-adca-de476bff6fd1\"><ac:parameter ac:name=\"headings\">Control Requirements, Control Response, Updated</ac:parameter><ac:parameter ac:name=\"cql\">label = &quot;tsc&quot; and label = &quot;A_8_1_3&quot; and space = currentSpace ( )</ac:parameter></ac:structured-macro></ac:layout-cell></ac:layout-section></ac:layout>"

The Algorithm

Now that I can get the template body from Confluence I need to figure out how to create the content. The Confluence API has a method for both creating and updating content. Since I want to be able to update these in the future I wanted to ensure I could both create the content as well as update it. Below is the algorithm I used to create the pages.

Page creation algorithm

Postman Collection

I’ve build a postman collection that captures this flow here. Much of the logic is in the first search request. You can download this collection and import it into Postman.

Postman collection

The first request searches for a page from a postman runner iteration. If it finds a page with this name it updates it, otherwise it creates a new page.

The search request has a post script that does most of the work after the request had returned results. I’ll break this up into a few sections

Top half of test function

After the test function definition, the response is extracted from the search. The “html” variable is the content from our template search in the beginning. The next section loops over the keys from the current object. The iteration data is is that spreadsheet above. If you look at the column names on that sheet, you’ll find that they match the table properties names. I use the table header to find a similar value in the source html, if I find it, I replace the page properties value with the value from the iteration. If not, the value is ignored. The whole purpose of this code is to populate the table in the Confluence doc withe the data from the google sheet.

The next section of code updates the label in the CQL query and then determines if it needs to create a page or update a page.

Bottom half of test function

Confluence doesn’t allow periods in labels so I needed to replace them with underscores. Once that’s done, I replace a known CQL lablel with the page specific label. Finally, I determine if I need to create or update a page. If I update the page, I need to increment it’s version number and set the id of the page I’m going to update. I also set the “setNextRequest” to the name of the update function in my collection “update confluence page”. Otherwise I “setNextRequest” to “create confluence page”. Both update and create require the body from the html we generated in the beginning of the test function.

Both the POST and PUT function are pretty simple they both just create/update the page and don’t post process the response.

Postman Runner

The last step in the process is creating the pages with Postman Runner. Postman runner basically lets you iterate over a collection. Though the intent of runner is to iterate over a collection with test data to test an API, we’ll be using it for data loading.

To start runner, click the runner button at the top left of the postman window. Once Collection Runner or Runner has started, find the collection “Add ISO Controls”. This will change the display and provide some additional options.

Postman Collection Runner

Select the environment we setup in the beginning, download a csv of the controls in the googlesheets file, then click “Run Add ISO Cont…”.

Ready to Create Pages in Confluence

This will create 114 pages (the iterations number).

Looking Ahead

I’ve used this design pattern for a few cases where I wanted to import or mass create pages in Confluence. One thing i found is that the API can be very finiky on the input data and some of the errors from the API can be very cryptic. A couple of basic observations:

  • Postman has/had an issue with quotes in text. It would fail to send data with properly escaped quotes for CSV text. I can’t remember if that is still an issue, but be aware, the data you are importing may require some sanitizaion. Do this in the google sheet first.
  • newlines, carriage returns, non-printable characters. All of these can cause issues with import. If you are having issues creating pages, check for non-printable characters.
  • I would always test with a few samples before creating a large number of pages. For this post, I think I tested on one page a few times, deleting the page after each test, then testing on 5 pages, then 5 more, etc. Pages are easy to recreate, but deleting 114 pages can be time consuming. Test on small batches.
  • I found in a more recent use case that my regular expression was not as accurate as I’d hoped. Ill post another article on a better crafted regex for updating a page properties table.
  • I could do a better job of error handling. I largely ignore errors in the code. It wouldn’t be too difficult to add code to halt execution for a failure.
  • I’ve used a similar pattern for Jira. I’ll find time to write about how to do this for Jira as well.

--

--

John Wheeler

Security professional, Mac enthusiast, writing code when I have to.