REST API
Read and write the test data this app owns, from outside Jira: test case steps and preconditions, and test run results.
Start here: do you actually need this API?
Section titled “Start here: do you actually need this API?”Most of what you might want is already available through Jira’s own REST API, because test artifacts are ordinary Jira issues. This API exists only for the parts Jira has no concept of.
| What you want to do | Use |
|---|---|
| Create, update or delete a Test Case, Set, Plan or Execution | Jira’s REST API — they are Jira issues |
| Set Test Type, Automation Status, Automation Key | Jira’s REST API (by customfield_* id — see note) |
| Link a test case to a requirement, or add cases to a set | Jira’s REST API — issue links (Covers, Includes) |
| Find or report on any of the above | JQL |
| Read or write test case steps and preconditions | This API |
| Read or record test run results | This API |
There is deliberately no create route and no delete route here. Jira already does both, and duplicating them would mean two ways to do one thing.
Note on custom fields via Jira’s API: address them by field id (
customfield_10308), not by display name. Names can collide with other fields on the site and the write is then rejected. The field must also be on the project’s screens, which the app’s setup handles.
Pushing CI results? There may be an easier path
Section titled “Pushing CI results? There may be an easier path”If all you want is to push automated test results, the app’s automation import webtrigger is
markedly simpler: generate a token in the app’s own UI, POST your results to one URL. No OAuth, no
Developer Console, no admin toggle, no token expiry. See the app’s Automation tab.
Choose this REST API when you need to read data back, or want per-row control over a run.
Two one-time steps. Neither is per-user or per-project.
1. A site admin enables app REST APIs
Section titled “1. A site admin enables app REST APIs”Atlassian admin → Apps → Connected apps → Advanced Test Management for Jira → App REST APIs → enable.
This is off by default and can be switched off again at any time, which immediately stops all API access. Until it is on, every request fails no matter how valid your token is.
2. Create an OAuth 2.0 (3LO) integration
Section titled “2. Create an OAuth 2.0 (3LO) integration”In the Developer Console:
- Create → OAuth 2.0 integration. This is a new, separate entry from the app itself — the settings are not inside the Advanced Test Management app.
- Access type: Resource-level. It restricts the token to the one site the user selects. Account-level would grant access to every Atlassian site on the account, which is more than any integration needs.
- Permissions →
Add Marketplace or custom app— the button on the right, not theAddon the Jira API row. In the dialog choose the site, Advanced Test Management for Jira, the environment, and the scopes you need. - Also grant the Atlassian product scope
read:forge-app:jira. Without it the call is rejected regardless of your app scopes. - Authorization → set a callback URL.
- Copy the authorization URL from the generator under the app’s entry — labelled “Marketplace App: Advanced Test Management for Jira”. ⚠️ Not the generic one at the top of the page (see Troubleshooting).
- Open that URL, consent, and exchange the returned
codefor a token.
curl -sS -X POST https://auth.atlassian.com/oauth/token \ -H 'Content-Type: application/json' \ -d '{"grant_type":"authorization_code","client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET","code":"THE_CODE","redirect_uri":"YOUR_CALLBACK_URL"}'Authorization codes expire in 300 seconds (5 minutes), and are single-use. If you see
authorization_code is invalid, get a fresh one.
⚠️ For CI, you must request offline_access
Section titled “⚠️ For CI, you must request offline_access”An access token is valid for one hour. That is fine while you explore the API by hand and useless for a build server — nothing can click a consent screen at 3am.
Add offline_access to the scopes in your authorization URL. The token response then includes a
refresh_token, which your integration exchanges for new access tokens unattended:
curl -sS -X POST https://auth.atlassian.com/oauth/token \ -H 'Content-Type: application/json' \ -d '{"grant_type":"refresh_token","client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET","refresh_token":"YOUR_REFRESH_TOKEN"}'Miss this and your integration will pass every test you run by hand, then stop working overnight.
Scopes
Section titled “Scopes”| Scope | Grants |
|---|---|
read:testcase:custom |
View the steps and preconditions recorded on test cases |
write:testcase:custom |
Create and change those steps and preconditions |
read:execution:custom |
View test run results |
write:execution:custom |
Record and update test run results, including completing a run |
Your existing Jira permissions still apply. Every request runs as you: Jira enforces your project permissions and any issue-level security, so a scope grants no access to a project or issue you could not already see. An issue you cannot see is reported as not found.
Grant only what an integration needs — a CI job that just records results needs
write:execution:custom, not the test case scopes.
Making a request
Section titled “Making a request”https://api.atlassian.com/svc/jira/{cloudId}/apps/{appId}_{environmentId}{path}cloudId identifies your Jira site; appId and environmentId come from the integration you set up.
Send the token as a bearer header.
curl -sS \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Accept: application/json" \ "https://api.atlassian.com/svc/jira/$CLOUD_ID/apps/${APP_ID}_${ENV_ID}/testCases/AN-123"Test management must be enabled for the project (project settings → Advanced Test Management). The
API honours the same per-project switch as the app’s screens, so it never exposes data the product
itself is hiding. A disabled project returns 409 notEnabled.
Issues are always addressed by key (AN-123), never numeric id — the key identifies the project,
so no project parameter is needed.
Errors
Section titled “Errors”Every error has the same shape:
{ "error": "notFound", "message": "No Test Case was found for AN-123." }| Status | error |
Meaning |
|---|---|---|
| 400 | badRequest |
Malformed JSON, an unknown field, an invalid value, or a bad path |
| 401 | — | Missing, expired or invalid token |
| 403 | — | Your token lacks the required scope, or app REST APIs are disabled for the site |
| 404 | notFound |
No such issue, it is not the expected type, or you cannot see it |
| 405 | methodNotAllowed |
Wrong HTTP method for the path |
| 409 | notEnabled |
Test management is not enabled for that project |
| 409 | conflict |
The run is completed, or cannot be completed yet |
404 is deliberately ambiguous. An issue you lack permission to see is indistinguishable from one that does not exist, so the API cannot be used to discover what exists.
Unknown fields are rejected rather than ignored. A typo like precondition for preconditions
returns 400 instead of silently doing nothing.
Test case steps and preconditions
Section titled “Test case steps and preconditions”GET /testCases/{issueKey}
Section titled “GET /testCases/{issueKey}”Scope: read:testcase:custom
{ "issueKey": "AN-123", "preconditions": "The user is signed in", "steps": [ { "id": "s1", "action": "Open Settings", "data": "admin@example.com", "expected": "Settings opens" }, { "id": "s2", "action": "Click Save", "expected": "A confirmation appears" } ], "testType": "Manual", "automationStatus": "Not Automated", "automationKey": "", "updatedAt": "2026-07-24T22:42:00.000Z"}Only app-owned data is returned. Summary, status, priority and assignee are Jira’s — read them from
Jira’s API so there is one source of truth. Every key above is always present; unset values are
null or "".
PUT /testCases/{issueKey}
Section titled “PUT /testCases/{issueKey}”Scope: write:testcase:custom
Send any subset of preconditions, steps, testType, automationStatus, automationKey.
curl -sS -X PUT \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ "$BASE/testCases/AN-123" \ -d '{ "preconditions": "The user is signed in", "steps": [ { "action": "Open Settings", "expected": "Settings opens" }, { "action": "Click Save", "data": "admin@example.com", "expected": "A confirmation appears" } ] }'- Fields you omit are left unchanged. Sending only
stepswill not clear the test type. stepsreplaces the whole list. Send every step you want to keep.- Do not send step
ids — they are assigned for you (s1,s2, …) in the order given. Anidyou send is ignored, so aGETresponse can be edited andPUTstraight back. testTypeandautomationStatusaccept any casing and must be one of the values below, or""to clear.automationKeyis free text.
| Field | Allowed values |
|---|---|
testType |
Manual, Automated, Exploratory, Performance, Security |
automationStatus |
Not Automated, Planned, Automated, Flaky, Deprecated |
A successful response is the updated record, in the same shape as GET. If it includes a warning, the
values were saved but could not be mirrored onto the Jira fields, so the app’s grids and reports may not
reflect them yet.
Test run results
Section titled “Test run results”Create the Test Execution itself in the app (or with Jira’s API). Its rows are fixed when it is created from a plan, set or selection — this API records results on existing rows and can neither add nor remove them.
GET /testExecutions/{issueKey}/results
Section titled “GET /testExecutions/{issueKey}/results”Scope: read:execution:custom
{ "issueKey": "AN-500", "source": "plan", "sourceIssueKey": "AN-400", "environment": "staging", "build": "1.4.2", "status": "Pass", "completed": false, "counts": { "pass": 2, "fail": 0, "blocked": 0, "skipped": 1, "notRun": 0, "total": 3 }, "results": [ { "testCase": "AN-123", "summary": "Valid login", "status": "pass", "notes": "Ran on Chrome 141", "executedBy": "5b10a2844c20165700ede21g", "executedAt": "2026-07-24T22:42:00.000Z", "defects": ["AN-900"] } ]}PUT /testExecutions/{issueKey}/results
Section titled “PUT /testExecutions/{issueKey}/results”Scope: write:execution:custom
PATCH is accepted as an alias with identical behaviour, for clients that cannot send PUT, or that
prefer PATCH. Use whichever suits you.
curl -sS -X PUT \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ "$BASE/testExecutions/AN-500/results" \ -d '{ "environment": "staging", "build": "1.4.2", "results": [ { "testCase": "AN-123", "status": "pass" }, { "testCase": "AN-124", "status": "fail", "notes": "Timed out after 30s" } ] }'- Rows you do not mention are left alone. Partial updates are the normal case, so a job reporting 3 of 50 results will not disturb the other 47.
- Omitting
noteskeeps the existing note. To clear one, send"notes": "". - To reset a row, send
"status": "notRun"explicitly. Nothing is ever cleared by omission. statusis one ofpass,fail,blocked,skipped,notRun, in any casing.- If any
testCaseis not part of this run, the whole request is rejected with 400 and nothing is saved. A job running a stale list of cases should fail loudly rather than half-succeed and look green.
executedBy and executedAt are stamped automatically from your token and the time of the call.
POST /testExecutions/{issueKey}/complete
Section titled “POST /testExecutions/{issueKey}/complete”Scope: write:execution:custom
curl -sS -X POST \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Accept: application/json" \ "$BASE/testExecutions/AN-500/complete"Completing a run finalises it and updates each test case’s latest result, which is what the Coverage, Traceability and Overview reports read.
- Every row must have a status first. While any row is still
notRunyou get409naming how many remain. - A completed run is read-only. Further writes, and completing again, return
409. - Completion is not reversible through this API.
A worked CI example
Section titled “A worked CI example”Record a suite’s results and close the run:
BASE="https://api.atlassian.com/svc/jira/$CLOUD_ID/apps/${APP_ID}_${ENV_ID}"; \curl -sS -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \ "$BASE/testExecutions/$RUN/results" \ -d '{"environment":"ci","build":"'"$GIT_SHA"'","results":[{"testCase":"AN-123","status":"pass"},{"testCase":"AN-124","status":"fail","notes":"see build log"}]}' \&& curl -sS -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Accept: application/json" \ "$BASE/testExecutions/$RUN/complete"Refresh your access token from a stored refresh token at the start of the job — see
offline_access.
Troubleshooting
Section titled “Troubleshooting”These are Atlassian Developer Console behaviours that cost real time. None is a fault in the app.
“The authorization URL doesn’t work / my custom scopes aren’t in it.”
The generic Authorization URL generator at the top of the Authorization page produces a URL that
cannot work for a Forge app: it omits developer-defined scopes and the required sns parameter.
Use the generator under the app’s own entry (“Marketplace App: …”), which includes both.
“The scope I need isn’t in the list.” A scope only appears once a deployed route uses it, and registration is not instant. Reload the page. If it still is not listed, the app version installed on that site may not yet expose it.
“Something went wrong” during authorization. Intermittent. Retry.
authorization_code is invalid.
Codes last 300 seconds and are single-use. Get a fresh one, copy only the value between code= and
the next &, and make sure redirect_uri in the exchange matches your callback URL exactly.
401 on every call with a valid-looking token.
Check you are sending only the access_token value, not the whole JSON response. Also confirm the
token has not passed its one-hour life.
403 although the scope is granted.
A site admin must have enabled App REST APIs for the app (setup step 1). It is off by default.
409 notEnabled.
Test management is not switched on for that project. A project admin enables it in project settings →
Advanced Test Management.
404 for an issue you are sure exists.
Either it is not the type the route expects, or your Jira account cannot see it — the two are reported
identically on purpose. Confirm you can open the issue in Jira as the same user.