Automating Jira and Confluence with the REST API and a PAT
Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.
Key takeaways
- A personal access token authenticates a script as you, with your permissions, which means a Jira API script can never see more than you can see in the browser and every action it takes is attributed to your account in the audit log.
- The single highest-value Jira automation for an analyst is generating the requirements traceability matrix from JQL and issue links, because it replaces a day of manual copying with a query that is correct every time it runs.
- Confluence pages are created and updated through the same REST pattern as Jira issues, with the body sent as storage format HTML and a version number that must be incremented on every update or the request is rejected.
- Every Jira or Confluence script needs pagination, a non-200 check, and a dry run mode, because an unpaginated query silently returns the first 50 results and a write script without a dry run is one typo away from editing 300 issues.
A personal access token plus about forty lines of Python turns Jira and Confluence from screens you click into systems you query. The three automations that pay for themselves on any delivery are generating the requirements traceability matrix from issue links, publishing specifications to Confluence from Markdown, and pulling ticket state into your own notes. The token authenticates as you, with your permissions, so nothing a script does is something you could not do by hand.
Analysts spend a genuinely surprising amount of a programme copying data between tools. The traceability matrix is the clearest example: requirements in Jira, test cases in Jira or a test tool, defects in Jira, and a spreadsheet in the middle that somebody rebuilds by hand before every steering committee and that is stale by the time it is presented. That spreadsheet is a query. It was always a query. The only reason it is manual is that nobody on the team treated the tracker as an API.
Both Jira and Confluence expose complete REST APIs, and the authentication is a token you can create yourself in about a minute. This is the most accessible piece of real automation available to an analyst, and it needs no more Python than scripting checks already requires. The wider technical toolkit this sits inside is covered in The Technical Skills Guide for BAs.
What is a personal access token and why not a password?
A personal access token is a long random string that authenticates an API request as your user account. It matters for four reasons, and each one is a reason a security team prefers it.
It carries exactly your permissions, no more. A PAT cannot read a project you cannot read, so the blast radius of a leaked analyst token is bounded by your own access. It is individually revocable: if it leaks, you revoke that token and nothing else about your account changes, whereas a leaked password means a reset and a scramble. It can be given an expiry, which forces rotation on a schedule instead of leaving a credential valid for years. And it is attributable: every issue transition and page edit made with your token appears in the audit log as your account, which is exactly what you want in a regulated environment where “who changed this requirement” is a question with consequences.
The mechanics differ slightly by deployment, and this is the detail that costs people an afternoon.
| Atlassian Cloud | Jira and Confluence Data Center | |
|---|---|---|
| Credential | API token | Personal access token |
| Created at | id.atlassian.com, API tokens | Your profile, Personal Access Tokens |
| Sent as | HTTP basic auth: email as user, token as password | Authorization: Bearer <token> |
| Base path | /rest/api/3 (Jira), /wiki/rest/api (Confluence) | /rest/api/2 (Jira), /rest/api (Confluence) |
Test the credential before writing anything else. One call tells you whether authentication, base URL, and network path all work.
# Atlassian Cloud
curl -s -u "$JIRA_EMAIL:$JIRA_TOKEN" \
"https://yourcompany.atlassian.net/rest/api/3/myself" | jq .displayName
# Data Center
curl -s -H "Authorization: Bearer $JIRA_PAT" \
"https://jira.yourbank.internal/rest/api/2/myself" | jq .displayName
If that returns your name, everything else in this article will work. If it returns a 401, the credential is wrong; a 403 usually means a proxy or an IP allowlist. Note that the token is read from an environment variable in both cases and never typed into a file, for reasons covered in API keys, PATs, and OAuth tokens.
How do you query Jira from Python?
Everything starts with JQL against the search endpoint. This helper handles the two things beginners’ scripts always miss, pagination and error checking.
import os
import requests
BASE = "https://yourcompany.atlassian.net"
AUTH = (os.environ["JIRA_EMAIL"], os.environ["JIRA_TOKEN"])
def search(jql, fields):
"""Return every issue matching jql, paging through the full result set."""
issues, start = [], 0
while True:
r = requests.get(
f"{BASE}/rest/api/3/search",
auth=AUTH,
params={"jql": jql, "fields": ",".join(fields),
"startAt": start, "maxResults": 100},
timeout=30,
)
r.raise_for_status()
page = r.json()
issues.extend(page["issues"])
start += len(page["issues"])
if start >= page["total"] or not page["issues"]:
return issues
reqs = search(
'project = PAY AND issuetype = Requirement AND fixVersion = "R2026.1"',
["summary", "status", "issuelinks", "priority"],
)
print(f"{len(reqs)} requirements in scope")
Three details carry their weight. raise_for_status() turns a silent failure into a loud one, which matters because an unauthenticated Jira search returns an empty result set rather than an error in some configurations, and an empty matrix looks exactly like a matrix with no gaps. The pagination loop exists because the API returns 50 results by default: a script without it reports on the first 50 requirements and quietly omits the rest, which is the most dangerous class of bug in analyst tooling, since the output looks complete. And requesting only the fields you need keeps a query over 800 issues fast instead of transferring the full issue body for each.
How do you generate the traceability matrix automatically?
This is the automation to build first. A requirements traceability matrix is a join between requirements, the tests that verify them, and the defects raised against them, and Jira already holds all three plus the links between them.
import csv
TEST_LINKS = {"is tested by", "tests"}
DEFECT_LINKS = {"is caused by", "causes", "relates to"}
def linked(issue, kinds, issuetype):
"""Keys of linked issues of a given type, following either link direction."""
out = []
for link in issue["fields"].get("issuelinks", []):
name = link["type"]["inward"] if "inwardIssue" in link else link["type"]["outward"]
other = link.get("inwardIssue") or link.get("outwardIssue")
if name in kinds and other["fields"]["issuetype"]["name"] == issuetype:
out.append(other["key"])
return out
with open("rtm.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["Requirement", "Summary", "Status", "Tests", "Defects", "Gap"])
for issue in reqs:
tests = linked(issue, TEST_LINKS, "Test")
defects = linked(issue, DEFECT_LINKS, "Bug")
w.writerow([
issue["key"],
issue["fields"]["summary"],
issue["fields"]["status"]["name"],
" ".join(tests),
" ".join(defects),
"NO TEST COVERAGE" if not tests else "",
])
The Gap column is the reason to run this weekly rather than before a steering committee. A requirement with no linked test is either untested or unlinked, and both are findings you want in week three rather than in the week before user acceptance testing. On an ISO 20022 migration this surfaces the specific requirements everyone assumes somebody covered: the reason code handling rules, the structured address validation, and the truncation behaviour on the MT to MX boundary.
Run it against a real project and expect the first result to be uncomfortable. On one payments release the script found 41 requirements with no linked test out of 260, and about half turned out to be tested but never linked, which is a process finding rather than a coverage finding, and just as worth having. The document templates that this data feeds into are in Real-World BA Deliverables.
How do you publish a page to Confluence?
Confluence stores page bodies in storage format, an XHTML dialect with Confluence-specific macro tags. Creating a page is a POST; updating one is a PUT with an incremented version.
CONF = "https://yourcompany.atlassian.net/wiki"
def publish(space, title, html, parent_id=None):
"""Create the page, or update it in place if the title already exists."""
found = requests.get(
f"{CONF}/rest/api/content",
auth=AUTH,
params={"spaceKey": space, "title": title, "expand": "version"},
timeout=30,
).json()["results"]
body = {
"type": "page",
"title": title,
"space": {"key": space},
"body": {"storage": {"value": html, "representation": "storage"}},
}
if found:
page = found[0]
body["version"] = {"number": page["version"]["number"] + 1}
r = requests.put(f"{CONF}/rest/api/content/{page['id']}",
auth=AUTH, json=body, timeout=30)
else:
if parent_id:
body["ancestors"] = [{"id": parent_id}]
r = requests.post(f"{CONF}/rest/api/content",
auth=AUTH, json=body, timeout=30)
r.raise_for_status()
return f"{CONF}{r.json()['_links']['webui']}"
The version number is the part that trips everyone. Confluence rejects a PUT whose version is not exactly one higher than the current one, which is optimistic locking: if a colleague edited the page after you read it, your write fails instead of silently destroying their edit. Do not work around it by fetching and incrementing blindly in a retry loop, because that is exactly the overwrite the check exists to prevent.
For a table, storage format is close enough to plain HTML that generating it is unremarkable:
rows = "".join(
f"<tr><td>{i['key']}</td><td>{i['fields']['summary']}</td>"
f"<td>{i['fields']['status']['name']}</td></tr>"
for i in reqs
)
html = (
"<p>Generated from Jira. Do not edit by hand.</p>"
"<table><tbody>"
"<tr><th>Key</th><th>Requirement</th><th>Status</th></tr>"
f"{rows}</tbody></table>"
)
print(publish("PAY", "R2026.1 Requirements Traceability", html))
That “do not edit by hand” line is not decoration. A generated page that people edit becomes a page that loses its edits on the next run, and the fastest way to lose trust in an automation is to have it overwrite somebody’s careful correction. Either the page is generated or it is authored, and it says which at the top.
The natural authoring flow is the one described in the Obsidian second brain: draft in Markdown where linking is cheap, convert to storage format, publish. The vault holds the working version, Confluence holds the record.
What are the safety rules for write scripts?
Reading is harmless. Writing is where an analyst script can do real damage, and the damage is always the same shape: a JQL query that matched more issues than you expected, run at speed.
Dry run first, always. Every write script takes a flag, defaults to printing what it would do, and only acts when told to.
import sys
APPLY = "--apply" in sys.argv
for issue in issues:
if not APPLY:
print(f"WOULD transition {issue['key']} to Ready for Test")
continue
r = requests.post(
f"{BASE}/rest/api/3/issue/{issue['key']}/transitions",
auth=AUTH, json={"transition": {"id": TRANSITION_ID}}, timeout=30,
)
r.raise_for_status()
print(f"moved {issue['key']}")
Read the dry run output before applying. Check the count against what you expected, and check three or four keys individually in the browser. A JQL clause that omits a project filter is the classic way to touch a thousand issues in another team’s project.
Never bulk edit somebody else’s field. Adding a comment, transitioning your own team’s issues, or updating a label you own is reasonable. Rewriting descriptions or reassigning across a project is a conversation, not a script, and the audit log will name you either way.
Keep the token out of the code. It lives in an environment variable, the script reads it with os.environ, and the repository contains a .env.example with the variable names and no values. A committed token is an incident, and search engines index public repositories quickly.
Which automations are actually worth building?
Not everything should be scripted. The test is whether the task is repetitive, rule-based, and currently done by copying. Four pass that test on almost every delivery.
The traceability matrix, as above, run weekly with the gap column as the output that matters. A scope change report: query issues added to or removed from the fix version since a date and post the delta as a comment on the release ticket, which turns scope creep from a feeling into a list. Ticket stubs into your notes, one Markdown file per issue with key, status, and summary as frontmatter, so your domain notes can link to the ticket that taught you something. A specification publish step, Markdown to Confluence, so the version people read is the version you wrote.
What is not worth building is a two-way sync between Jira and anything else. Every one of them fails the same way: the two sides drift, someone edits the wrong copy, and you spend more time reconciling than the automation ever saved. One direction, one source of truth, regenerate rather than merge.
The takeaway
A personal access token authenticates a script as you with exactly your permissions, expires, revokes individually, and shows up in the audit log, which is why it is the right credential for analyst automation and a password never is. With it, Jira and Confluence become queryable systems rather than screens.
Build the traceability matrix generator first, because it replaces a manual day with a query and its gap column finds untested requirements while there is still time to test them. Add a Confluence publish step so specifications flow from your drafts to the system of record in one direction. Then hold the line on the safety rules: paginate every query, check every status code, dry run every write, and keep the token in the environment rather than the file.
Start with The Technical Skills Guide for BAs for the scripting and API foundations, and Real-World BA Deliverables for the matrix and specification templates these scripts populate, or browse everything at The Tech BA Toolkit.
Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.
Tags: Jira, Confluence, Automation, Python, Business Analysis
About the author
Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.
Related articles
- Obsidian as an Analyst's Second Brain: The Vault That Survives a Payments Programme How to build an Obsidian vault for analyst work: folder structure, atomic notes per ISO 20022 message and reason code, daily investigation logs, and Jira sync.
- API Keys, PATs, and OAuth Tokens: The Analyst's Guide to Credentials The difference between an API key, a personal access token, and an OAuth token, how to scope and rotate them, and where they belong across an analyst toolchain.
- The Requirements Traceability Matrix: From Requirement to Test, Proven What a requirements traceability matrix is, how to build one, and why it proves every requirement is designed, built, and tested. With a payments example.
- Scripting Checks in Python: Automate What You Repeat How an analyst uses Python to automate repetitive checks: calling APIs, comparing files, querying data, and chaining requests. Small scripts, large leverage.
Newsletter
Subscribe
Practical, no-fluff playbooks for technical analysts who analyze, code, test, and support. New articles straight to your inbox.
No spam. Unsubscribe anytime.