# GitLab Webhooks Reference ## Overview GitLab webhooks allow external services to be notified when certain events happen in GitLab. When triggered, GitLab sends an HTTP POST request with event data to the configured URL. ## Webhook Configuration ### Creating a Webhook **Via UI**: Project Settings > Webhooks **Via API**: ``` POST /projects/:id/hooks ``` Parameters: - `url` (required): The webhook URL - `token`: Secret token for request verification - `push_events`: Trigger on push events (default: true) - `tag_push_events`: Trigger on tag push events - `issues_events`: Trigger on issue events - `confidential_issues_events`: Trigger on confidential issue events - `merge_requests_events`: Trigger on merge request events - `wiki_page_events`: Trigger on wiki page events - `deployment_events`: Trigger on deployment events - `job_events`: Trigger on job events - `pipeline_events`: Trigger on pipeline events - `releases_events`: Trigger on release events - `enable_ssl_verification`: Verify SSL certificates (default: true) ### Webhook Security **Secret Token**: Include a secret token to verify requests: ```python import hmac import hashlib def verify_webhook(payload, signature, secret): expected = hmac.new( secret.encode(), payload.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) ``` **Request Headers**: - `X-Gitlab-Token`: Contains the secret token (if configured) - `X-Gitlab-Event`: Event type name - `User-Agent`: GitLab/{version} ## Event Types ### Push Events **Trigger**: Code pushed to repository **Event Header**: `X-Gitlab-Event: Push Hook` **Payload Structure**: ```json { "object_kind": "push", "event_name": "push", "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "ref": "refs/heads/main", "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "user_id": 4, "user_name": "John Doe", "user_username": "jdoe", "user_email": "john@example.com", "user_avatar": "https://gitlab.com/uploads/user/avatar/4/avatar.jpg", "project_id": 15, "project": { "id": 15, "name": "My Project", "description": "Project description", "web_url": "https://gitlab.com/namespace/project", "avatar_url": null, "git_ssh_url": "git@gitlab.com:namespace/project.git", "git_http_url": "https://gitlab.com/namespace/project.git", "namespace": "Namespace", "visibility_level": 0, "path_with_namespace": "namespace/project", "default_branch": "main" }, "commits": [ { "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "message": "Fix bug in authentication", "title": "Fix bug in authentication", "timestamp": "2025-01-15T12:30:00+00:00", "url": "https://gitlab.com/namespace/project/-/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "author": { "name": "John Doe", "email": "john@example.com" }, "added": ["new-file.txt"], "modified": ["existing-file.txt"], "removed": ["old-file.txt"] } ], "total_commits_count": 1, "repository": { "name": "My Project", "url": "git@gitlab.com:namespace/project.git", "description": "Project description", "homepage": "https://gitlab.com/namespace/project" } } ``` ### Tag Push Events **Trigger**: Tag created or deleted **Event Header**: `X-Gitlab-Event: Tag Push Hook` **Payload Structure**: ```json { "object_kind": "tag_push", "event_name": "tag_push", "before": "0000000000000000000000000000000000000000", "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", "ref": "refs/tags/v1.0.0", "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", "user_id": 4, "user_name": "John Doe", "user_avatar": "https://gitlab.com/uploads/user/avatar/4/avatar.jpg", "project_id": 15, "project": { /* project details */ }, "commits": [ /* commit details */ ], "total_commits_count": 0, "repository": { /* repository details */ } } ``` **Note**: `before` is all zeros for tag creation, `after` is all zeros for tag deletion. ### Issue Events **Trigger**: Issue created, updated, closed, or reopened **Event Header**: `X-Gitlab-Event: Issue Hook` **Payload Structure**: ```json { "object_kind": "issue", "event_type": "issue", "user": { "id": 4, "name": "John Doe", "username": "jdoe", "avatar_url": "https://gitlab.com/uploads/user/avatar/4/avatar.jpg", "email": "john@example.com" }, "project": { /* project details */ }, "object_attributes": { "id": 301, "title": "Bug in login feature", "assignee_ids": [5, 6], "assignee_id": 5, "author_id": 4, "project_id": 15, "created_at": "2025-01-15 12:30:00 UTC", "updated_at": "2025-01-15 13:00:00 UTC", "position": 0, "branch_name": null, "description": "Login page crashes when...", "milestone_id": 10, "state": "opened", "state_id": 1, "iid": 23, "url": "https://gitlab.com/namespace/project/-/issues/23", "action": "open", "labels": [ { "id": 100, "title": "bug", "color": "#FF0000", "project_id": 15, "created_at": "2024-01-01 00:00:00 UTC", "updated_at": "2024-01-01 00:00:00 UTC" } ] }, "assignees": [ { "id": 5, "name": "Jane Smith", "username": "jsmith", "avatar_url": "https://gitlab.com/uploads/user/avatar/5/avatar.jpg" } ], "labels": [ /* label details */ ], "changes": { "updated_at": { "previous": "2025-01-15 12:30:00 UTC", "current": "2025-01-15 13:00:00 UTC" }, "title": { "previous": "Old title", "current": "Bug in login feature" } } } ``` **Action Values**: `open`, `update`, `close`, `reopen` ### Merge Request Events **Trigger**: MR created, updated, merged, or closed **Event Header**: `X-Gitlab-Event: Merge Request Hook` **Payload Structure**: ```json { "object_kind": "merge_request", "event_type": "merge_request", "user": { /* user details */ }, "project": { /* project details */ }, "object_attributes": { "id": 99, "iid": 1, "target_branch": "main", "source_branch": "feature-branch", "source_project_id": 15, "author_id": 4, "assignee_ids": [5], "assignee_id": 5, "reviewer_ids": [6, 7], "title": "Add new authentication feature", "created_at": "2025-01-15 12:00:00 UTC", "updated_at": "2025-01-15 14:00:00 UTC", "milestone_id": 10, "state": "opened", "state_id": 1, "merge_status": "can_be_merged", "target_project_id": 15, "description": "This MR adds...", "url": "https://gitlab.com/namespace/project/-/merge_requests/1", "source": { /* source project */ }, "target": { /* target project */ }, "last_commit": { "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "message": "Update authentication", "timestamp": "2025-01-15T13:30:00+00:00", "url": "https://gitlab.com/namespace/project/-/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "author": { "name": "John Doe", "email": "john@example.com" } }, "work_in_progress": false, "draft": false, "action": "open", "assignee": { /* assignee details */ }, "labels": [ /* label details */ ] }, "labels": [ /* label details */ ], "changes": { /* changed attributes */ }, "assignees": [ /* assignee details */ ], "reviewers": [ /* reviewer details */ ] } ``` **Action Values**: `open`, `update`, `close`, `reopen`, `merge`, `approved`, `unapproved` ### Pipeline Events **Trigger**: Pipeline status changes **Event Header**: `X-Gitlab-Event: Pipeline Hook` **Payload Structure**: ```json { "object_kind": "pipeline", "object_attributes": { "id": 31, "ref": "main", "tag": false, "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", "source": "push", "status": "success", "detailed_status": "passed", "stages": ["build", "test", "deploy"], "created_at": "2025-01-15 12:00:00 UTC", "finished_at": "2025-01-15 12:15:00 UTC", "duration": 900, "queued_duration": 10, "variables": [ { "key": "ENVIRONMENT", "value": "production" } ] }, "merge_request": { "id": 1, "iid": 1, "title": "Add feature", "source_branch": "feature-branch", "source_project_id": 15, "target_branch": "main", "target_project_id": 15, "state": "opened", "merge_status": "can_be_merged", "url": "https://gitlab.com/namespace/project/-/merge_requests/1" }, "user": { /* user details */ }, "project": { /* project details */ }, "commit": { "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", "message": "Add new feature", "title": "Add new feature", "timestamp": "2025-01-15T11:55:00+00:00", "url": "https://gitlab.com/namespace/project/-/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2", "author": { "name": "John Doe", "email": "john@example.com" } }, "builds": [ { "id": 380, "stage": "test", "name": "unit-tests", "status": "success", "created_at": "2025-01-15 12:00:00 UTC", "started_at": "2025-01-15 12:01:00 UTC", "finished_at": "2025-01-15 12:05:00 UTC", "duration": 240, "queued_duration": 60, "when": "on_success", "manual": false, "allow_failure": false, "user": { /* user details */ }, "runner": { "id": 1, "description": "runner-1", "active": true, "runner_type": "project_type", "is_shared": false, "tags": ["docker", "linux"] }, "artifacts_file": { "filename": "artifacts.zip", "size": 1024000 }, "environment": null } ] } ``` **Status Values**: `pending`, `running`, `success`, `failed`, `canceled`, `skipped` ### Job Events **Trigger**: Job status changes **Event Header**: `X-Gitlab-Event: Job Hook` **Payload Structure**: ```json { "object_kind": "build", "ref": "main", "tag": false, "before_sha": "95790bf891e76fee5e1747ab589903a6a1f80f22", "sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "build_id": 380, "build_name": "unit-tests", "build_stage": "test", "build_status": "success", "build_created_at": "2025-01-15 12:00:00 UTC", "build_started_at": "2025-01-15 12:01:00 UTC", "build_finished_at": "2025-01-15 12:05:00 UTC", "build_duration": 240, "build_queued_duration": 60, "build_allow_failure": false, "build_failure_reason": "unknown_failure", "pipeline_id": 31, "runner": { "id": 1, "description": "runner-1", "runner_type": "project_type", "active": true, "is_shared": false, "tags": ["docker", "linux"] }, "project_id": 15, "project_name": "My Project", "user": { /* user details */ }, "commit": { /* commit details */ }, "repository": { /* repository details */ }, "environment": { "name": "production", "action": "start", "deployment_tier": "production" } } ``` **Build Status Values**: `pending`, `running`, `success`, `failed`, `canceled` ### Deployment Events **Trigger**: Deployment created or status changes **Event Header**: `X-Gitlab-Event: Deployment Hook` **Payload Structure**: ```json { "object_kind": "deployment", "status": "success", "status_changed_at": "2025-01-15 12:20:00 UTC", "deployment_id": 15, "deployable_id": 380, "deployable_url": "https://gitlab.com/namespace/project/-/jobs/380", "environment": "production", "environment_tier": "production", "environment_slug": "production", "environment_external_url": "https://prod.example.com", "project": { /* project details */ }, "short_sha": "da156088", "user": { /* user details */ }, "user_url": "https://gitlab.com/jdoe", "commit_url": "https://gitlab.com/namespace/project/-/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "commit_title": "Deploy to production" } ``` **Status Values**: `created`, `running`, `success`, `failed`, `canceled` ### Wiki Page Events **Trigger**: Wiki page created, updated, or deleted **Event Header**: `X-Gitlab-Event: Wiki Page Hook` **Payload Structure**: ```json { "object_kind": "wiki_page", "user": { /* user details */ }, "project": { /* project details */ }, "wiki": { "web_url": "https://gitlab.com/namespace/project/-/wikis/home", "git_ssh_url": "git@gitlab.com:namespace/project.wiki.git", "git_http_url": "https://gitlab.com/namespace/project.wiki.git", "path_with_namespace": "namespace/project", "default_branch": "main" }, "object_attributes": { "title": "Home", "content": "# Welcome\n\nThis is the home page...", "format": "markdown", "message": "Update home page", "slug": "home", "url": "https://gitlab.com/namespace/project/-/wikis/home", "action": "update" } } ``` **Action Values**: `create`, `update`, `delete` ### Release Events **Trigger**: Release created, updated, or deleted **Event Header**: `X-Gitlab-Event: Release Hook` **Payload Structure**: ```json { "id": 1, "created_at": "2025-01-15 12:00:00 UTC", "description": "Release notes for v1.0.0", "name": "Version 1.0.0", "released_at": "2025-01-15 12:00:00 UTC", "tag": "v1.0.0", "object_kind": "release", "project": { /* project details */ }, "url": "https://gitlab.com/namespace/project/-/releases/v1.0.0", "action": "create", "assets": { "count": 2, "links": [ { "id": 1, "external": true, "link_type": "other", "name": "Binary", "url": "https://example.com/binary" } ], "sources": [ { "format": "zip", "url": "https://gitlab.com/namespace/project/-/archive/v1.0.0/project-v1.0.0.zip" }, { "format": "tar.gz", "url": "https://gitlab.com/namespace/project/-/archive/v1.0.0/project-v1.0.0.tar.gz" } ] }, "commit": { /* commit details */ } } ``` **Action Values**: `create`, `update`, `delete` ## Testing Webhooks ### Via GitLab UI 1. Navigate to Project Settings > Webhooks 2. Find your webhook 3. Click "Test" dropdown 4. Select event type to test 5. View response in UI ### Via API ```bash curl -X POST "https://gitlab.com/api/v4/projects/:id/hooks/:hook_id/test/push_events" \ --header "PRIVATE-TOKEN: " ``` ### Local Testing Use tools like: - **ngrok**: Create public URL for local development - **webhook.site**: Test webhook delivery - **requestbin.com**: Inspect webhook payloads ```bash # Using ngrok ngrok http 3000 # Update webhook URL to ngrok URL # Trigger event in GitLab # View webhook payload in ngrok dashboard ``` ## Webhook Best Practices ### 1. Security - **Use HTTPS**: Always use HTTPS URLs for webhooks - **Verify SSL**: Enable SSL verification in production - **Secret Tokens**: Use secret tokens to verify webhook authenticity - **Validate Payloads**: Verify request signatures - **IP Allowlisting**: Restrict webhook sources to GitLab IPs ### 2. Reliability - **Return Quickly**: Respond with 2xx status code quickly (< 10 seconds) - **Async Processing**: Queue webhook payloads for async processing - **Idempotency**: Handle duplicate webhook deliveries - **Error Handling**: Handle errors gracefully - **Retry Logic**: Implement retry logic for failed processing ### 3. Monitoring - **Log Webhooks**: Log all webhook deliveries - **Track Failures**: Monitor webhook failure rates - **Alert on Issues**: Set up alerts for webhook failures - **Recent Deliveries**: Check recent deliveries in GitLab UI ### 4. Performance - **Rate Limiting**: Handle rate limits on external APIs - **Batch Processing**: Batch similar webhook events - **Caching**: Cache frequently accessed data - **Timeouts**: Set appropriate timeouts ## Example Webhook Handler ### Python Flask Example ```python from flask import Flask, request, jsonify import hmac import hashlib app = Flask(__name__) WEBHOOK_SECRET = "your-secret-token" def verify_signature(payload, signature): """Verify webhook signature""" if not signature: return False expected = hmac.new( WEBHOOK_SECRET.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) @app.route('/webhook', methods=['POST']) def handle_webhook(): # Get signature from header signature = request.headers.get('X-Gitlab-Token') # Verify signature if not verify_signature(request.data, signature): return jsonify({'error': 'Invalid signature'}), 401 # Get event type event_type = request.headers.get('X-Gitlab-Event') # Parse payload payload = request.json # Handle different event types if event_type == 'Push Hook': handle_push_event(payload) elif event_type == 'Merge Request Hook': handle_merge_request_event(payload) elif event_type == 'Pipeline Hook': handle_pipeline_event(payload) return jsonify({'status': 'received'}), 200 def handle_push_event(payload): """Handle push events""" ref = payload['ref'] commits = payload['commits'] print(f"Received push to {ref} with {len(commits)} commits") def handle_merge_request_event(payload): """Handle merge request events""" action = payload['object_attributes']['action'] iid = payload['object_attributes']['iid'] print(f"Merge request {iid} was {action}") def handle_pipeline_event(payload): """Handle pipeline events""" status = payload['object_attributes']['status'] ref = payload['object_attributes']['ref'] print(f"Pipeline on {ref} status: {status}") if __name__ == '__main__': app.run(port=5000) ``` ### Node.js Express Example ```javascript const express = require('express'); const crypto = require('crypto'); const bodyParser = require('body-parser'); const app = express(); const WEBHOOK_SECRET = 'your-secret-token'; // Verify webhook signature function verifySignature(payload, signature) { if (!signature) return false; const expected = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } app.post('/webhook', bodyParser.raw({type: 'application/json'}), (req, res) => { const signature = req.headers['x-gitlab-token']; const eventType = req.headers['x-gitlab-event']; // Verify signature if (!verifySignature(req.body, signature)) { return res.status(401).json({ error: 'Invalid signature' }); } const payload = JSON.parse(req.body.toString()); // Handle different event types switch (eventType) { case 'Push Hook': handlePushEvent(payload); break; case 'Merge Request Hook': handleMergeRequestEvent(payload); break; case 'Pipeline Hook': handlePipelineEvent(payload); break; } res.json({ status: 'received' }); }); function handlePushEvent(payload) { console.log(`Push to ${payload.ref} with ${payload.commits.length} commits`); } function handleMergeRequestEvent(payload) { const { action, iid } = payload.object_attributes; console.log(`Merge request ${iid} was ${action}`); } function handlePipelineEvent(payload) { const { status, ref } = payload.object_attributes; console.log(`Pipeline on ${ref} status: ${status}`); } app.listen(5000, () => { console.log('Webhook server listening on port 5000'); }); ``` ## Troubleshooting ### Common Issues 1. **Webhook Not Triggering** - Verify event is enabled in webhook configuration - Check webhook URL is accessible from internet - Review Recent Deliveries in GitLab UI - Check firewall rules 2. **SSL Verification Failures** - Ensure SSL certificate is valid - Check certificate chain is complete - Temporarily disable SSL verification for testing (not recommended for production) 3. **Timeouts** - Reduce processing time in webhook handler - Return 2xx response quickly, process async - Check network connectivity 4. **Authentication Errors** - Verify secret token matches - Check signature verification logic - Ensure token is transmitted correctly ### Debugging Tips 1. Use webhook testing services (webhook.site, requestbin.com) 2. Check "Recent Deliveries" in GitLab webhook settings 3. Enable detailed logging in webhook handler 4. Test with curl/Postman using sample payloads 5. Verify IP addresses if using allowlisting 6. Check response status codes and headers ## Additional Resources - GitLab Webhooks Documentation: https://docs.gitlab.com/ee/user/project/integrations/webhooks.html - Webhook Events: https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html - System Hooks: https://docs.gitlab.com/ee/administration/system_hooks.html