Webhooks are often described as "callbacks" or "reverse APIs," but the most surprising truth is that they’re fundamentally just HTTP POST requests.

Let’s see this in action. Imagine you want to be notified whenever a new issue is opened in your GitHub repository. You’ve set up a webhook pointing to https://your-server.com/github-webhook. When an issue is opened, GitHub will send a POST request to that URL.

Here’s a simplified look at what that request might contain:

{
  "zen": "Some random string.",
  "ref": null,
  "before": null,
  "after": null,
  "created": true,
  "deleted": false,
  "forced": false,
  "base_ref": null,
  "commits": [],
  "compare": null,
  "repository": {
    "id": 123456789,
    "name": "my-repo",
    "full_name": "my-username/my-repo",
    "owner": {
      "login": "my-username",
      "id": 987654321,
      "avatar_url": "https://avatars.githubusercontent.com/u/987654321?v=4"
    },
    "html_url": "https://github.com/my-username/my-repo",
    "description": "A test repository.",
    "fork": false,
    "url": "https://api.github.com/repos/my-username/my-repo",
    "created_at": "2023-01-01T10:00:00Z",
    "updated_at": "2023-10-27T10:00:00Z",
    "pushed_at": "2023-10-27T10:00:00Z"
  },
  "pusher": null,
  "sender": {
    "login": "my-username",
    "id": 987654321,
    "avatar_url": "https://avatars.githubusercontent.com/u/987654321?v=4",
    "html_url": "https://github.com/my-username"
  },
  "issue": {
    "url": "https://api.github.com/repos/my-username/my-repo/issues/1",
    "html_url": "https://github.com/my-username/my-repo/issues/1",
    "number": 1,
    "state": "open",
    "title": "New Feature Request",
    "body": "This is a new feature request.",
    "user": {
      "login": "another-user",
      "id": 112233445,
      "avatar_url": "https://avatars.github.com/u/112233445?v=4",
      "html_url": "https://github.com/another-user"
    },
    "assignee": null,
    "milestone": null,
    "comments": 0,
    "created_at": "2023-10-27T10:05:00Z",
    "updated_at": "2023-10-27T10:05:00Z"
  },
  "pull_request": null,
  "enterprise": null,
  "installation": null,
  "organization": {
    "login": "my-username",
    "id": 987654321,
    "avatar_url": "https://avatars.githubusercontent.com/u/987654321?v=4",
    "html_url": "https://github.com/my-username"
  }
}

This entire payload is sent as the body of the HTTP POST request, typically with a Content-Type header of application/json. Your server-side application receives this request, parses the JSON body, and then acts upon the event data (e.g., creates a ticket in a ticketing system, sends a notification to Slack, triggers a CI/CD pipeline).

The core problem webhooks solve is decoupling. GitHub doesn’t need to know how your external system processes events. It just needs a URL to send data to. Your external system doesn’t need to constantly poll GitHub’s API for changes; it gets notified asynchronously when something happens.

Configuring a Webhook:

In your GitHub repository settings, navigate to "Webhooks." You’ll see an option to "Add webhook."

  • Payload URL: This is the most critical part. It’s the HTTP(S) endpoint on your server that will receive the POST requests. For example: https://your-awesome-service.com/api/github-hook.
  • Content type: application/json is the standard and recommended format. GitHub can also send application/x-www-form-urlencoded, but JSON is far more common for structured data.
  • Secret: This is a string that you define. GitHub will sign each webhook request with this secret using HMAC-SHA1. Your server can then verify that the request truly came from GitHub, preventing spoofing. If you set a secret, you’ll need to implement verification logic on your server.
  • Which events would you like to trigger this webhook? You can choose to receive "Just the push event" (for code pushes) or "Send me everything" (all events). For more granular control, you can select "Let me select individual events" and pick specific actions like issues, pull_request, fork, release, etc.

Common Use Cases:

  • CI/CD Automation: Trigger builds and deployments on code pushes or pull request merges.
  • Notifications: Send alerts to Slack, Microsoft Teams, or email for critical events.
  • Issue Tracking: Automatically create tickets in Jira, Asana, or Trello when issues are opened or closed.
  • Security Auditing: Log all repository activity to an external SIEM system.
  • Data Synchronization: Keep external databases or analytics platforms updated with repository metadata.

When you configure a webhook, GitHub sends a ping event to your Payload URL to verify that the endpoint is active and reachable. Your server should respond with a 2xx status code (e.g., 200 OK) to acknowledge receipt. If GitHub doesn’t receive a successful response within a timeout period (typically 10 seconds), it will mark the webhook as inactive and stop sending events.

The sender object within the webhook payload is particularly useful. It tells you who performed the action that triggered the webhook, allowing you to implement logic based on the user or bot responsible. This is distinct from the issue.user or pull_request.user which refer to the creator of the specific entity.

The installation object is present for GitHub Apps and indicates which installation of the app received the event, crucial for multi-tenant applications.

If you’re using a secret, the signature will be in the X-Hub-Signature-256 (or X-Hub-Signature for SHA1) header. Your server must compute the HMAC-SHA256 (or SHA1) of the raw request body using your secret and compare it to the value in the header. If they match, the request is authentic.

The next concept you’ll likely encounter is handling webhook delivery failures and ensuring idempotency.

Want structured learning?

Take the full Github course →