Deleting personal access tokens via the API


I’m having a problem with deleting personal access tokens via the API. Any tokens I ‘delete’ this way only gets flagged as ‘revoked’, but they are not actually removed from the list. If I click on the revoke button manually in the graphical UI, the token actually gets deleted, but never from the API.

This is a problem for us since we would like to generate a new personal access token each time a user logs in (to another site which interacts with Gitlab), but if we can’t delete old tokens they quickly start to pile up… I would really rather not have to store personal access tokens in a separate db, so I’m hoping that I’m just doing something wrong although I can’t see what.


can you please share the API calls you are using? I haven’t had the chance yet to try this endpoint.


Hi Michael,

Sure, it’s just (for token with ID 67):

curl -k -X DELETE -w “%{http_code}” -H “PRIVATE-TOKEN:-REDACTED-”

And what I get as output to that is just “204”, which seems to indicate that everything is fine and the token does get revoked, but just never deleted.

Thanks for trying to help!


thanks, this helps to try it without further reading :slight_smile:

This command lists all the personal access tokens. jq is used for better formatting JSON on the shell, GITLAB_TOKEN is exported into my environment in a .env file for ZSH.


Checking the GUI for the ID I can delete. This one is from an older webcast which is not active anymore. Good way to clean it away.

Then I had an idea on listing and filtering this with jq. This resulted in writing a full blog post about jq and listing&revoking GitLab PATs:

TL;DR - after inspecting the API docs, and comparing the request method with DELETE, it became clear to me that PATs can only be revoked but not deleted. This is common practice to keep certificates and tokens, but revoke them. Later audit logs and processes can be dependent on this, matching the PAT with the user id and so on.

The UI uses a filter to not list revoked PATs. Your curl API call needs to filter the received result by checking on revoked == false. jq needs to be installed for the following command to work:

$ curl -H "PRIVATE-TOKEN: $GITLAB_TOKEN" 2&>1 >/dev/null | jq -c '.[] | select( .revoked == false)' | jq

Kind regards,



I appreciate you taking the time to look into this Michael, and I understand the reasoning behind never deleting access tokens. However this causes some problems in our use case, and I suspect we may not be alone in that. There is also another problem which helps compound the first one, which is that when you fetch the list of tokens via the API, you will only get a maximum of 20 returned, and there doesn’t seem to be any provisioning for paging, meaning if you have over 20 tokens, there’s no way to be sure you can revoke them all via the API.

To further add to the confusion, pressing the “Revoke” button in the UI actually does seem to delete - not just revoke - the token. So we have a situation where the “Delete” function via the API actually performs a revoke, and the “Revoke” function in the UI actually performs a delete. I understand that it’s not always easy to maintain consistency in these sort of things when building systems, but I would just like to point out that this is not ideal from a user experience perspective and it would probably be good if it was changed somehow.

To go back to the more central issue, I understand that it might be desirable to never allow deletion of tokens, and I think our use case would be resolved if it was possible to query the token list with a parameter filtering out all of the revoked tokens. I understand that it’s possible to do this client side, but that results in a lot of data being shuffled needlessly which slows down operations.

Something would probably also have to be done for the list of tokens in the UI, to keep it manageable when it contains hundreds or thousands of tokens.

1 Like


appreciate the thoughtful feedback. I think it is worth a discussion with our engineering teams, and see how we can find a solution for your use case. Mind opening an issue? (Bug template could also fit, unsure here).


I have similar problems. I’m creating a temp token, via gitlab-rails runner, for use with Ansible or Terraform to provision parts of Gitlab. But the next run I have to create a new token because any form of revoke or delete does not appear to delete the token. One is force to have an ever increasing amount of revoked tokens. Delete should delete.

@dnsmichi Thanks for the explanation. We are also facing the same problem described in this ticket. Our motivation is a little bit different though.

For safety issue we have a nightly script to generate a new access token of the certain user to be used by CI the next day so even if the token is leaked it’s only valid for a short period of time. If the token can only be revoked but not completely deleted, the amount of all tokens (including the revoked one) pile up very quickly.

We are not too worried about the audit in this case since the service where we record the toke has its own history, so is there a workaround, even a hacky one, to delete the token? Thx

I just tried this:
$ sudo gitlab-rails runner delete-revoked-pas.rb <username>
where delete-revoked-pas.rb:

usn = ARGV[0]
puts "Deleting Revoked Personal Access Tokens for #{usn}"
user = User.find_by_username(usn)
all_tokens = user.personal_access_tokens.find_all
all_tokens.each do |tkn| 
    if tkn.revoked == true 

It seems to work for me but as Michael said probably should be there just in case u need them for activity or security.

1 Like

Meanwhile you can filter the results before they reach the client:

using httpie, but curl will also work of course:

https \
 revoked==false \
 state==active \

see Personal access tokens API | GitLab