Adding and Removing Jira Labels via the REST API Without a Prior GET

Adding and Removing Jira Labels via the REST API Without a Prior GET

Read-modify-write is inefficient and causes conflicts when adding or removing labels via the Jira API. This post shows, with test results, how the Edit issue API's update operation does it safely in one request, no prior GET.
2026.08.31

This page has been translated by machine translation. View original

Hi there! I'm Yuji Nishimura from the Ops Team!

When adding or removing Jira labels via the API, specifying fields.labels replaces the entire array. For this reason, people tend to follow the steps "GET the current labels → merge them locally → PUT them back." This approach increases the number of requests, and if another update lands between the GET and the PUT, that change gets overwritten. In fact, my own application used this approach and issued 2 GETs + 3 PUTs per issue.

Using the update operation of the Edit issue API, I was able to apply just the label additions and removals on the server side in a single request without a prior GET, so I'll introduce it along with my verification results.

What we'll do

What you'll learn in this article:

  • How to add and remove labels without a prior GET using add / remove in update.labels
  • The difference between fields and update and when to use each
  • How to combine other fields (such as the due date) in the same request

The difference between fields and update

In the request body of Edit issue (PUT /rest/api/3/issue/{issueIdOrKey}), you can specify field edits in two ways: fields and update.

  • fields: A simple method that sets values as they are. Array fields are replaced entirely by the array you send
  • update: A method that specifies operations (verbs) such as set / add / remove / edit / copy. Adding or removing elements of an array field can be expressed as operations

With update.labels, you only send operations such as "add this label" and "remove this label," so other labels are unaffected even if you don't know the current value. The rule of thumb is to use fields when setting a value wholesale and update for partial updates of an array.

Note that the official reference explicitly states that the same field cannot be included in both fields and update ("Fields included in here cannot be included in update.").

Trying it out

Test environment

  • Jira Cloud (REST API v3)
  • macOS + curl
  • Authentication: Basic auth using an Atlassian account API token. Set your email address and API token in the environment variables JIRA_EMAIL / JIRA_API_TOKEN
  • Verification date: 2026-08-31

The issue used for verification already has four labels attached.

curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
  "https://your-domain.atlassian.net/rest/api/3/issue/ISSUE-1?fields=labels" | jq -c '.fields.labels'
["existing-a","existing-b","existing-c","existing-d"]

1. Adding a label

PUT with a single add in update.labels.

curl -s -o /dev/null -w "%{http_code}\n" \
  -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
  -X PUT -H "Content-Type: application/json" \
  "https://your-domain.atlassian.net/rest/api/3/issue/ISSUE-1" \
  -d '{"update":{"labels":[{"add":"new-label"}]}}'

The result was 204 (No Content). When I checked the labels with a GET, the existing four labels were untouched and only the specified label had been added.

["existing-a","existing-b","existing-c","existing-d","new-label"]

2. Adding and removing in one request

Swapping labels, as in "remove the old label and attach a new one," can also be done in a single request by listing add and remove side by side.

curl -s -o /dev/null -w "%{http_code}\n" \
  -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
  -X PUT -H "Content-Type: application/json" \
  "https://your-domain.atlassian.net/rest/api/3/issue/ISSUE-1" \
  -d '{"update":{"labels":[{"add":"replacement-label"},{"remove":"new-label"}]}}'

The result was 204, and checking with a GET showed that both operations had been applied.

["existing-a","existing-b","existing-c","existing-d","replacement-label"]

3. Updating another field at the same time

fields and update can coexist in one request as long as they don't specify the same field. Let's add a label and set the due date at the same time.

curl -s -o /dev/null -w "%{http_code}\n" \
  -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
  -X PUT -H "Content-Type: application/json" \
  "https://your-domain.atlassian.net/rest/api/3/issue/ISSUE-1" \
  -d '{"update":{"labels":[{"add":"another-label"}]},"fields":{"duedate":"2026-09-30"}}'

The result was 204, and checking with a GET showed that both the label addition and the duedate setting were applied in a single PUT.

{"labels":["existing-a","existing-b","existing-c","existing-d","replacement-label","another-label"],"duedate":"2026-09-30"}

Key points of the behavior

  1. Writing the same field in both returns 400
    • As stated in the official reference, sending fields.labels and update.labels together returned 400 with the error message Field 'labels' cannot appear in both 'fields' and 'update'
  2. Removing a nonexistent label does not cause an error
    • Doing a remove on a label that wasn't attached returned 204 and did not affect the other labels. Repeating the same request doesn't change the result, so it's a behavior that makes retries easy
  3. You can receive the updated state with returnIssue=true
    • On success, the response is 204 with no body by default, but I also confirmed that adding the query parameter returnIssue=true returns 200 with the updated issue. This also lets you skip the GET to confirm the changes

Summary

Replacing the read-modify-write of GET → merge → PUT with add / remove in update.labels reduces the number of requests and also avoids conflicts with concurrent updates. The application I mentioned at the beginning was also reduced to 1 PUT per issue with this replacement.

Similar operations are defined for array fields other than labels, so whenever you want to do a partial update, it's a good idea to first check whether update can be used.

I hope this helps someone.


Reference links:

Share this article