Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

Earn badges and make progress

You're on your way to the next level! Join the Kudos program to earn points and save your progress.

Deleted user Avatar
Deleted user

Level 1: Seed

25 / 150 points

Next: Root


1 badge earned


Participate in fun challenges

Challenges come and go, but your rewards stay with you. Do more to earn more!


Gift kudos to your peers

What goes around comes around! Share the love by gifting kudos to your peers.


Rise up in the ranks

Keep earning points to reach the top of the leaderboard. It resets every quarter so you always have a chance!


Come for the products,
stay for the community

The Atlassian Community can help you and your team get more value out of Atlassian products and practices.

Atlassian Community about banner
Community Members
Community Events
Community Groups

Deleting JSM customer users via API


Adding users into you instance is one thing, however removing these users could be another challenge. If you're familiar with Jira's REST API it should be your saving grace to tweak and automate certain things. However making these calls could be challenging for people who are new to it and sometimes you can't get it to work the way you want due to various reasons. Below I'm going to show you how to delete users from your Jira cloud instance (JSM customer users or Jira users) using API and in less than 20 lines of code. Sound exciting, how about you continue reading below to get the insight.

To do this, we're focusing on python and using a python package called jiraone (get it via pip install jiraone).

Deleting a user

In order to delete a user on cloud, you will need to know the accountId. How about if there was a way to delete a user without knowing the accountId and simply by just knowing the display name of the user? Well this feature already exist within Jira's API, you just need to know how to call it. Let's start off with a basic call using the jiraone package to delete a user.

from jiraone import LOGIN, endpoint, USER, echo

user = "email"
password = "token"
link = ""

LOGIN(user=user, password=password, url=link)
# search for users
name_of_user = "Elf App 607"
search = USER.search_user(name_of_user, pull="active", user_type="customer")
for name in search:
# delete a user that's found
delete = LOGIN.delete(endpoint.jira_user(name.get("accountId")))
# returns 204 for successful deletion
if delete.status_code < 300:
echo("User deleted...")
echo("User not deleted...")

Let me explain what is happening on the above code. We need to delete a user with a name of "Elf App 607". Now we're calling our search_user method and our arguments needs to pull only active users of type "customer" - This way we want to only search for JSM customer with that name. If the user exist and is found, we delete and get a response of 204. This same principle applies to Jira users in such a scenario change the user_type argument to "atlassian".

Bulk deleting users

I think we got the hang of things, how about we take this further into bulk deleting multiple users. Remember we're keeping our code within 20 lines and not looking for any accountId but by means of the user's display name. In order to do this, we'll need a file source to wrap our user's display names. You can do this by using a csv file and placing each user's name on a single line and save as a .csv file format. Example below

Elf App 607

Elf App 61

Elf App 405

from jiraone import LOGIN, endpoint, USER, echo, file_reader

args = "email", "token", ""
read = file_reader(file_name="user_files.csv")
get_user_list = []
get_account_id = []
for users in read:
for names in get_user_list:
search = USER.search_user(names, pull="active", user_type="customer", skip=True)
for name in search:
for aid in get_account_id:
delete = LOGIN.delete(endpoint.jira_user(aid))
if delete.status_code < 300:
print("User deleted")
echo("User not deleted...")

Now we're going for a bulk delete of users, we start by using a tuple that way we can reduce our code line to just one and keep it within 20 lines of code. It has to be in a sequence to follow user, password and url keyword arguments from the LOGIN variable as shown in the first example above. We prepare two empty list to capture the names of the users after being read and the accountId after being found. In the latter situations, if the users is not found, the script will automatically terminate with an error of user not found. Once we get all the user's accountId we've listed out, then we run a delete of the users.


I'm getting a 404 status_code on trying to delete a user. Am I missing something?

404 generally means the resource was not found. Most probably, the user you're searching for was not found and returned 0 instead of a list containing key-value pairs of the user. Please can you print the result of the user search to see what's being returned?

Like Etienne likes this

@Prince Nyeche I get a 400 error and given that the api docs state:

Returned if:

accountId, query or property is missing.
query and accountId are provided.
property parameter is not valid.
A schema has not been defined for this response code.


Im wondering if the returned values in the dict:

"accountId": <numbers>
"emailAddress": "<emailaddress>",
"displayName": <name>


arent whats causing the 400 due to  the emailaddress key being present before the display name?

Im simply guessing here, as Im not that experienced in python. 


Troubleshooting 101 :facepalm:

I ran the default API DELETE user command and got an error message:

"errorMessages":["Cannot delete user '1234xyza' because 1 issues were reported by this person."],"errors":{}}


Should put in an output for any errorMessages to the console to help with these errors when running your script.

Hey @Robert Rodrigues 

Glad to know you've gotten a clearer message of the error by returning the response content. Usually the response content should contain additional information about the error and why it happens.

Indeed, except when running your script, there is no indication of what the error pertains to, other than error response 400.

It would be good to log the response, rather than check to see if the string of <300 is returned.

Like Prince Nyeche likes this

That's true. Definitely an approach I'm taking with all my new scripts and functions as proper error handling actually helps to point out without a doubt where the problem lies. Thanks for the insight.

No worries! Your script is immensely helpful, so thank you!

Hey @Prince Nyeche 

Quick one:

Im now seeing the error:

 for name in search:
TypeError: 'int' object is not iterable

Im unsure if this is to do with the jiraone library or the fact that the result coming back form the API is now changed but when I check to see what the variable search is, it returns 0, which would explain the error.

The question is, without diving further into the unknown here is, do we know if the USER function inside jiraone is no longer handling the results from the API correctly?

Ive checked my users exist and are present, and match the "customer" and active=true arguments.

Any ideas?

Hey @Robert Rodrigues 

The USER.search_user() method does an exact string match of the displayName. It does not use any regular expression to pin point potential matches. The reason it returns 0 is because it couldn't find the user from the string used. Most probably, the string that is being used as the search term isn't an exact match with what the displayName actually is. Please note that for an exact match, this includes any character that depicts the displayName of the user including space characters.

Like Robert Rodrigues likes this


Have worked it out. I was feeding email address instead of displayName, which I'd incorrectly assumed going from example one to two, would also allow email_address!

Thanks for highlighting.

hey @Prince Nyeche 

Ive stumbled across an error when feeding in bigger list.

Traceback (most recent call last):
File "/Users/rrodrigues/gitrepos/it-infra/scripts/jira-customer-delete/", line 13, in <module>
search = USER.search_user(names, pull="active", user_type="customer", skip=True)
File "/usr/local/lib/python3.9/site-packages/jiraone/", line 1475, in search_user
f = CheckUser._make(_)
File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/collections/", line 441, in _make
raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
TypeError: Expected 4 arguments, got 3

Im assuming this relates to the line:

USER.search_user(names, pull="active", user_type="customer", skip=True)

Is pulling, in this instance, one less item for a record. Havent seen this throughout the testing Ive done and Ive checked the users are coming back with active and customer when you search for them individually through the API.


Any ideas?

Yes, it seems to have one less item in the record at that point in the code. A header is created to ensure what is received matches the header column item. I think the problem might lie on the list_user variable that's reading the user list within the USER.search_user method. You should see a folder and CSV file created automatically in your directory once you encounter this error, as that file should contain the users. Then can you examine the actual file that's being read? See if any of the rows are 3 instead of 4.

Yup - have worked it out. In the file, the variable f is storing 4 arguments, but we're not defining those in the expected dict.

We're missing the reading of account_type from this.


I fixed it by adding the following line to the for loop on line 1474:

account_type = f.account_type

and then appended it to the OrderedDict in both if isInstance

"accountType": account_type,
self.user_list.append(OrderedDict({"accountId": get_user, "accountType": account_type,
"displayName": display_name, "active": status})) 

That seems to have rectified the issue. Not sure why I could run the script before without having this error.

Confirmed by opening that CSV, adding that code and then reopening the csv which now has the accountType column heading. Below is the headings before I amended with above code.

Screenshot 2022-07-28 at 12.52.06.png

I don't think that was the cause. The for loop is suppose to receive the data after being read from the file and the file is written by searching get_all_users method. Looking at the file you've shown, it seems that the failure occurs when trying to read row 2. For some reason, the file itself is mutated with a header. Now I remember, the reason I added skip=True argument is because of this reason, I never got around fully resolving this issue just added a workaround. Although, in your instance it wrote the header twice, that why this problem happened. Please can you create a bug here with the steps you did? So I can add this to the next releases or the one after that.

The main cause is that a header is written to the file which shouldn't happen with the get_all_users.I think this is because of the ordered dict class but I need to trace why and how it can mutate a file that's already saved.

Okay, I think I know the reason why.Since the call to USER.search_user happens consecutively, there's a rewrite of the file and the self.user_list stores all the entries. Somehow it takes the keys of ordered dict values and appends that when rewriting the file hence this problem. Changing the code as a workaround is okay but I need to fix the behaviour of that method as that's not how it should function. It shouldn't be updating the file even with multiple calls to it. The exception is only when there's a difference in user size.

Hey @Robert Rodrigues I did a patch of the code which you can use instead rather than rewriting the headers as the headers shouldn't be written to the file and the call shouldn't be updating the list all the time as that takes time when you're running larger dataset. With this behaviour, it creates the file once when performing a bigger list. Now you can do away with this line


USER.search_user(names, pull="active", user_type="customer", skip=True)


USER.search_user(names, pull="active", user_type="customer")

 I still have other features which I'm bringing, so waiting to complete that first before pushing to a new version, thought I fix this since it's something I should have fixed but forgotten about it down the road.

Ah great!

For clarity, do I replace my local copy of with your highlighted 1447 line & the code under the After heading?

Yes, that's it.


Log in or Sign up to comment

Atlassian Community Events