Library / SDK
rvagg/github-webhook-handler avatar
rvagg/github-webhook-handler

github-webhook-handler verifies signatures and dispatches events

Node.js web handler / middleware for processing GitHub Webhooks

790 stars121 forksJavaScriptMIT

At a glance

What is it?
github-webhook-handler is a small Node.js middleware that receives GitHub webhook requests, checks the X-Hub-Signature against a shared secret and emits typed events through an EventEmitter. It handles the receiving and verification half of a webhook integration and leaves routing, retries and persistence to you.
Who is it for?
Adopt github-webhook-handler when you are writing a Node.js service that must accept GitHub webhooks and want signature verification and event dispatch handled for you, and when your deployment target is Node 20 or newer. Do not adopt it if you need the framework specific adapters, retry handling, delivery logging or signature algorithm choice that a larger integration library offers, or if your runtime is not Node.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Receiving and verifying is the whole job, and that is the point

GitHub lets you register a webhook on a repository, and each time an event occurs the registered address is pinged with details. The work that follows is the tedious part, and this library exists to remove it. It is described as a small handler, or middleware, for Node.js web servers that handles all of the logic of receiving and verifying webhook requests from GitHub. That phrase sets the boundary clearly. The library does not receive HTTP requests for you, does not persist deliveries, does not retry, and does not know what your application should do with a push. It gives you a function you call inside your own server and a set of events you subscribe to. The audience is engineers writing a small Node service, often a bot or a CI trigger, who want correct signature verification without reimplementing the HMAC comparison. The repository itself is small to match. At the top level there is the handler file, package.json, an events.json, a LICENSE.md, a CHANGELOG.md, a test.js, a tsconfig.json and a types directory.

Signature verification with a shared secret

The security mechanism is a hash key you share with GitHub. The options object takes a secret property, described as the hash key used for creating the SHA-1 HMAC signature of the JSON blob sent by GitHub, and the same secret key must be registered with GitHub. The rule for a request is absolute. Any request not delivering an X-Hub-Signature that matches the signature generated using this key against the blob will be rejected, and rejection causes an error event and the callback to be called with an Error object. That means an unauthenticated caller cannot make your endpoint act on a forged payload, which is the entire reason the library is worth using rather than reading the raw body yourself. The implementation depends on a single runtime dependency, bl, the Buffer implementation for Node, and the package is published as an ES module with a single import export entry point in package.json.

One function creates a handler that behaves like middleware

The package exports a single function, and calling it with an options object creates a webhook handler. Three options matter. path is the complete, case sensitive path or route to match when looking at req.url for incoming requests, and any request that does not match it causes the callback function to the handler to be called. secret is the shared key described above. events is an optional array of whitelisted event types, read from events.json, and if it is defined then any incoming request whose X-Github-Event is not in the whitelist is rejected, with a single acceptable type also accepted as a string. The resulting handler takes request, response and callback arguments and behaves like middleware you can insert into a processing chain. The callback is not called when the request is handled successfully, and otherwise it is called either with an Error or with no arguments. A minimal server wires it up like this:

js
import http from 'node:http'
import createHandler from 'github-webhook-handler'

const handler = createHandler({ path: '/webhook', secret: 'myhashsecret' })

http.createServer((req, res) => {
  handler(req, res, (err) => {
    res.statusCode = 404
    res.end('no such location')
  })
}).listen(7777)

The listener is registered before the server starts, and each event type is subscribed by name on the handler itself, which is also an EventEmitter.

Events arrive as EventEmitter emissions with a payload

Because the handler is an EventEmitter, you register a listener per GitHub event type and the payload arrives on the event object. A push handler reads the repository name and the ref, and an issues handler reads the action, the issue number and the title:

js
handler.on('push', (event) => {
  console.log('Received a push event for %s to %s',
    event.payload.repository.name,
    event.payload.ref)
})

handler.on('issues', (event) => {
  console.log('Received an issue event for %s action=%s: #%d %s',
    event.payload.repository.name,
    event.payload.action,
    event.payload.issue.number,
    event.payload.issue.title)
})

The payload shape is whatever GitHub sends, so the event name tells you which schema to expect. There is also a special wildcard event that lets you listen to everything at once, which is useful when you want to log all traffic before deciding which events to handle properly. The distribution includes an events.json file mapping event names to descriptions taken from the API, and the documentation shows how to read it, which is the fastest way to see the full list of event types your endpoint could receive:

js
import events from 'github-webhook-handler/events.json' with { type: 'json' }

for (const [event, description] of Object.entries(events)) {
  console.log(event, '=', description)
}

The import attributes syntax used there reflects the package's current ES module setup.

The error event will fire for anything that looks like a probe

There is one operational detail that catches people out, and the documentation puts it plainly: the error event will be liberally used, even if someone tries the end point and cannot generate a proper signature, so you should at least register a listener for it or it will throw. An EventEmitter with an error event and no listener throws the error, so an endpoint that is publicly reachable and probed by a scanner, a curious person or a bot will crash the process rather than return a rejection. Registering the listener is not optional in practice:

js
handler.on('error', (err) => {
  console.error('Error:', err.message)
})

Note the asymmetry with the callback. On a failed signature the documentation says both an error event is emitted and the callback is called with an Error object, which means an application that handles errors in both places can double report the same rejection. That is worth knowing when you wire logging, because a flood of invalid signatures looks alarming in your logs even though it is the expected behaviour of a correctly configured endpoint.

Content type has to be application/json and form encoding will not work

The tips section is short and the constraint is hard. In the GitHub Webhooks settings, the content type must be application/json, and application/x-www-form-urlencoded will not work at present. The reason sits in the verification design. The signature is computed over the JSON blob that GitHub sent, so the handler has to read the body back byte for byte in order to recompute the HMAC over the same bytes. A form encoded body is a different byte sequence from the JSON body the signature was computed over, so the comparison can never succeed. That also has a deployment consequence that the documentation does not spell out for you: whatever framework or body parser you use in front of this handler must not consume and re-serialise the request body before the handler sees it. If you use a JSON body parser that runs earlier in the chain, the raw bytes may already be gone. The path matching behaviour is also tied to the raw req.url, so mount the handler on a route and let it see the untouched request.

Multiple handlers and the shape of a release

The library is a single exported function, so running more than one webhook endpoint means calling it more than once with different options, and the documentation points at a discussion of multiple handlers for the pattern. Since each call returns its own EventEmitter with its own path, secret and event list, keeping them separate is workable, but you are the one holding the references. The repository shows careful release engineering. The package.json names semantic-release as a dev dependency with the commit analyzer, release notes generator, GitHub and npm plugins, and the release configuration lists master as the release branch along with custom release rules, including one that maps a breaking change to a major release and one that maps a revert to a patch. The version in package.json reads 2.1.19, matching the most recent tag, with v2.1.18 on 2026-08-06 and v2.1.17 on 2026-08-03, and the last push was on 2026-10-19. The engine requirement is Node 20 or newer. Licensing is MIT, and the copyright is held by Rod Vagg from 2014, which tells you this is a long settled project rather than a new arrival.

Editorial conclusion

Adopt github-webhook-handler when you are writing a Node.js service that must accept GitHub webhooks and want signature verification and event dispatch handled for you, and when your deployment target is Node 20 or newer. Do not adopt it if you need the framework specific adapters, retry handling, delivery logging or signature algorithm choice that a larger integration library offers, or if your runtime is not Node. Verify first that the webhook is configured with the content type application/json, that you register an error listener because the handler throws without one, and that the same secret string is registered with GitHub and passed in your options.

Frequently asked questions

How do I verify that a GitHub webhook request is genuine?

Pass a secret to createHandler, and the handler compares the X-Hub-Signature header against the SHA-1 HMAC it computes with that secret over the JSON body. A request that does not deliver a matching signature is rejected, which raises an error event and calls the callback with an Error.

Which Node.js version does github-webhook-handler support?

The requirements state Node.js >= 20, and the package.json engines field records node as >=20. The package is published as an ES module, so the import style is required rather than optional.

Why do my GitHub webhooks fail with github-webhook-handler?

The documentation states that the content type in the GitHub Webhooks settings must be application/json, and that application/x-www-form-urlencoded will not work at present. The signature is computed over the JSON blob, so the handler needs the original body bytes.

Do I have to register an error listener with github-webhook-handler?

Yes. The documentation says the error event will be liberally used, even when someone probes the end point without a proper signature, so you should at least register a listener for it or it will throw.

How do I restrict github-webhook-handler to specific GitHub events?

Pass an events option in the options object, either as an array of whitelisted event types or as a single string when one type is acceptable. A request whose X-Github-Event is not in the whitelist is then rejected.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. rvagg/github-webhook-handler on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/rvagg-github-webhook-handler.svg)](https://hysenlabs.com/projects/rvagg-github-webhook-handler)

Was this useful?