Bulk Removing GitLab Webhooks with Python
We were retiring an old integration and needed to clean up the associated webhooks from all our GitLab projects. The problem: hundreds of projects, each potentially with multiple webhooks, all added over the years.
The GitLab UI lets you manage webhooks one project at a time. That wasn’t going to work.
The approach
The GitLab API has full CRUD for webhooks. The plan was straightforward:
- Paginate through all projects I’m a member of
- For each project, list its webhooks
- Delete any webhook whose URL contains the target keyword
""" Bulk-delete GitLab webhooks matching a keyword """
import requests
GITLAB_TOKEN = '<your_access_token>'
GITLAB_API_URL = 'https://gitlab.com/api/v4/'
KEYWORD = 'your-integration-keyword'
headers = {'Private-Token': GITLAB_TOKEN}
def get_all_projects():
projects = []
page = 1
while True:
response = requests.get(
f"{GITLAB_API_URL}projects?page={page}&per_page=100&membership=true",
headers=headers
)
if response.status_code != 200:
break
data = response.json()
if not data:
break
projects.extend(data)
page += 1
return projects
def get_project_webhooks(project_id):
response = requests.get(f"{GITLAB_API_URL}projects/{project_id}/hooks", headers=headers)
return response.json() if response.status_code == 200 else []
def delete_project_webhook(project_id, hook_id):
requests.delete(f"{GITLAB_API_URL}projects/{project_id}/hooks/{hook_id}", headers=headers)
def main():
projects = get_all_projects()
for project in projects:
webhooks = get_project_webhooks(project['id'])
for webhook in webhooks:
if KEYWORD in webhook['url']:
print(f"Removing {webhook['url']} from {project['name']}")
delete_project_webhook(project['id'], webhook['id'])
if __name__ == "__main__":
main()
A few things worth noting
The membership=true filter is important — without it you’d get every public project on GitLab. Pagination is essential; the API caps responses at 20 projects by default and won’t tell you there’s more unless you keep requesting.
The script is intentionally simple: no concurrency, no dry-run flag, no progress bar. I ran it once, watched the output, and it finished in under a minute across the full project list.
What I’d add for a larger scale
If this needed to run regularly or against a much larger instance:
| Addition | Why |
|---|---|
| Dry-run mode | Print matches before deleting |
| Rate limiting | GitLab will push back if you hit it too fast |
| Parallel requests | Speed up across hundreds of projects |
For a one-time cleanup job, none of that was necessary. The script did exactly what was needed and nothing more.
The views and opinions expressed here are my own and do not reflect those of my employer.