Skip to content
Advanced Test Management for Jira

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.

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.

In the Developer Console:

  1. 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.
  2. 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.
  3. Permissions → Add Marketplace or custom app — the button on the right, not the Add on the Jira API row. In the dialog choose the site, Advanced Test Management for Jira, the environment, and the scopes you need.
  4. Also grant the Atlassian product scope read:forge-app:jira. Without it the call is rejected regardless of your app scopes.
  5. Authorization → set a callback URL.
  6. 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).
  7. Open that URL, consent, and exchange the returned code for a token.
Terminal window
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:

Terminal window
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.


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.


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.

Terminal window
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.

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.


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 "".

Scope: write:testcase:custom

Send any subset of preconditions, steps, testType, automationStatus, automationKey.

Terminal window
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 steps will not clear the test type.
  • steps replaces 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. An id you send is ignored, so a GET response can be edited and PUT straight back.
  • testType and automationStatus accept any casing and must be one of the values below, or "" to clear. automationKey is 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.


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.

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"]
}
]
}

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.

Terminal window
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 notes keeps the existing note. To clear one, send "notes": "".
  • To reset a row, send "status": "notRun" explicitly. Nothing is ever cleared by omission.
  • status is one of pass, fail, blocked, skipped, notRun, in any casing.
  • If any testCase is 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.

Scope: write:execution:custom

Terminal window
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 notRun you get 409 naming how many remain.
  • A completed run is read-only. Further writes, and completing again, return 409.
  • Completion is not reversible through this API.

Record a suite’s results and close the run:

Terminal window
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.


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.