How to Receive GitHub Webhooks with Examples
GitHub webhooks notify your application when events occur in your repositories — code is pushed, pull requests are opened, issues are created, releases are published, and more. They're essential for CI/CD pipelines, automated workflows, and integrations.
The problem? Setting up GitHub webhooks requires signature verification, handling various event types, and managing payload parsing.
The solution? Deploy a production-ready GitHub webhook handler with Codehooks.io in under 5 minutes using our ready-made template.
Prerequisites
Before you begin, sign up for a free Codehooks.io account and install the CLI:
npm install -g codehooks
Quick Deploy with Template
The fastest way to get started is using the Codehooks GitHub webhook template:
coho create mygithubhandler --template webhook-github-minimal
You'll see output like:
Creating project: mygithubhandler
Project created successfully!
Your API endpoints:
https://mygithubhandler-a1b2.api.codehooks.io/dev
Then install, deploy, and configure:
cd mygithubhandler
npm install
coho deploy
coho set-env GITHUB_WEBHOOK_SECRET your-webhook-secret --encrypted
That's it! The template includes signature verification, event handling, and common webhook patterns.
The webhook-github-minimal template is production-ready with proper signature verification and event handling. Browse all available templates in the templates repository.
Custom Implementation
If you prefer to build from scratch, create a new project and add your GitHub webhook handler code to index.js:
import { app } from 'codehooks-js';
import { Datastore } from 'codehooks-js';
import crypto from 'crypto';
const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
// Verify GitHub webhook signature (SHA-256)
function verifyGitHubWebhook(req) {
const signature = req.headers['x-hub-signature-256'];
if (!signature) return false;
const body = req.rawBody;
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', GITHUB_WEBHOOK_SECRET)
.update(body)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (e) {
return false;
}
}
// GitHub webhook endpoint
app.post('/github/webhooks', async (req, res) => {
// Verify the webhook signature
if (!verifyGitHubWebhook(req)) {
console.error('Invalid GitHub webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.headers['x-github-event'];
const deliveryId = req.headers['x-github-delivery'];
const payload = req.body;
console.log(`Received ${event} event (${deliveryId})`);
// Handle different GitHub events
switch (event) {
case 'push':
await handlePush(payload);
break;
case 'pull_request':
await handlePullRequest(payload);
break;
case 'issues':
await handleIssue(payload);
break;
case 'release':
await handleRelease(payload);
break;
case 'workflow_run':
await handleWorkflowRun(payload);
break;
case 'ping':
console.log('Ping received! Webhook configured successfully.');
break;
default:
console.log(`Unhandled event: ${event}`);
}
res.json({ received: true });
});
async function handlePush(payload) {
const { repository, pusher, commits, ref } = payload;
const branch = ref.replace('refs/heads/', '');
console.log(`Push to ${repository.full_name}/${branch} by ${pusher.name}`);
console.log(`Commits: ${commits.length}`);
commits.forEach(commit => {
console.log(`- ${commit.id.slice(0, 7)}: ${commit.message.split('\n')[0]}`);
});
// Your business logic:
// - Trigger deployment
// - Run tests
// - Send notifications
// - Update documentation
}
async function handlePullRequest(payload) {
const { action, pull_request, repository } = payload;
console.log(`PR #${pull_request.number} ${action} in ${repository.full_name}`);
console.log(`Title: ${pull_request.title}`);
console.log(`Author: ${pull_request.user.login}`);
switch (action) {
case 'opened':
// New PR opened
// - Run CI checks
// - Add labels
// - Assign reviewers
break;
case 'closed':
if (pull_request.merged) {
// PR was merged
// - Trigger deployment
// - Update changelog
}
break;
case 'synchronize':
// New commits pushed to PR
// - Re-run CI checks
break;
}
}
async function handleIssue(payload) {
const { action, issue, repository } = payload;
console.log(`Issue #${issue.number} ${action} in ${repository.full_name}`);
console.log(`Title: ${issue.title}`);
if (action === 'opened') {
// New issue created
// - Auto-label based on content
// - Notify team on Slack
// - Create task in project management tool
}
}
async function handleRelease(payload) {
const { action, release, repository } = payload;
if (action === 'published') {
console.log(`New release ${release.tag_name} in ${repository.full_name}`);
// Your business logic:
// - Trigger production deployment
// - Update documentation
// - Send announcement
// - Update package registries
}
}
async function handleWorkflowRun(payload) {
const { action, workflow_run, repository } = payload;
if (action === 'completed') {
const status = workflow_run.conclusion;
console.log(`Workflow "${workflow_run.name}" ${status}`);
if (status === 'failure') {
// Notify team of failed workflow
}
}
}
export default app.init();
Deploy:
coho deploy
Project: mygithubhandler-a1b2 Space: dev
Deployed Codehook successfully! 🙌
Run coho info to see your API endpoints and tokens. Your webhook URL will be:
https://mygithubhandler-a1b2.api.codehooks.io/dev/github/webhooks
Configure GitHub Webhooks
Via GitHub Repository Settings
- Go to your repository → Settings → Webhooks
- Click Add webhook
- Enter:
- Payload URL:
https://your-app.api.codehooks.io/dev/github/webhooks - Content type:
application/json - Secret: Your webhook secret (save this!)
- Payload URL:
- Select events:
- Choose Let me select individual events
- Select: Push, Pull requests, Issues, etc.
- Click Add webhook
Via GitHub API
// Create a webhook programmatically
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/hooks`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${githubToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'web',
active: true,
events: ['push', 'pull_request', 'issues'],
config: {
url: 'https://your-app.api.codehooks.io/dev/github/webhooks',
content_type: 'json',
secret: process.env.GITHUB_WEBHOOK_SECRET
}
})
}
);
Set Environment Variables
coho set-env GITHUB_WEBHOOK_SECRET your-secret-here --encrypted
Common GitHub Webhook Events
| Event | Trigger | Common Use Case |
|---|---|---|
push | Code pushed to branch | CI/CD, deployments |
pull_request | PR opened/closed/merged | Code review automation |
pull_request_review | Review submitted | Merge automation |
issues | Issue created/updated | Ticket management |
issue_comment | Comment on issue/PR | Bot responses |
release | Release published | Production deployment |
workflow_run | GitHub Action completed | Status notifications |
create | Branch/tag created | Environment provisioning |
delete | Branch/tag deleted | Cleanup resources |
deployment_status | Deployment status change | Monitoring |
star | Repository starred | Analytics |
fork | Repository forked | Tracking |
GitHub Signature Verification
GitHub signs all webhook payloads with HMAC-SHA256. Always verify signatures:
import crypto from 'crypto';
function verifyGitHubWebhook(req) {
const signature = req.headers['x-hub-signature-256'];
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!signature) {
return false;
}
// Must use raw body, not parsed JSON
const body = req.rawBody;
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (e) {
return false;
}
}
Important:
- Use
req.rawBody(the raw string), notreq.body(parsed JSON) - Use
x-hub-signature-256(SHA-256), not the deprecatedx-hub-signature(SHA-1) - Use
crypto.timingSafeEqual()to prevent timing attacks
CI/CD Integration Example
Trigger deployments on push to main:
import { app } from 'codehooks-js';
app.post('/github/webhooks', async (req, res) => {
if (!verifyGitHubWebhook(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.headers['x-github-event'];
const payload = req.body;
if (event === 'push') {
const branch = payload.ref.replace('refs/heads/', '');
if (branch === 'main') {
// Trigger production deployment
await triggerDeployment({
environment: 'production',
commit: payload.after,
repository: payload.repository.full_name
});
// Notify team
await sendSlackNotification(
`🚀 Deploying ${payload.repository.name} to production\n` +
`Commit: ${payload.after.slice(0, 7)}`
);
} else if (branch === 'develop') {
// Trigger staging deployment
await triggerDeployment({
environment: 'staging',
commit: payload.after,
repository: payload.repository.full_name
});
}
}
res.json({ received: true });
});
async function triggerDeployment({ environment, commit, repository }) {
// Call your deployment API
await fetch(process.env.DEPLOY_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ environment, commit, repository })
});
}
export default app.init();
Slack Notifications for GitHub Events
async function sendGitHubNotificationToSlack(event, payload) {
let message;
switch (event) {
case 'push':
const branch = payload.ref.replace('refs/heads/', '');
const commits = payload.commits.length;
message = `📦 *${payload.pusher.name}* pushed ${commits} commit(s) to \`${branch}\`\n` +
`Repository: ${payload.repository.full_name}`;
break;
case 'pull_request':
const pr = payload.pull_request;
const action = payload.action;
const emoji = action === 'opened' ? '🆕' : action === 'closed' ? '✅' : '📝';
message = `${emoji} PR #${pr.number} ${action}: *${pr.title}*\n` +
`Author: ${pr.user.login}\n` +
`<${pr.html_url}|View PR>`;
break;
case 'issues':
const issue = payload.issue;
message = `🎫 Issue #${issue.number} ${payload.action}: *${issue.title}*\n` +
`<${issue.html_url}|View Issue>`;
break;
}
if (message) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message })
});
}
}
Best Practices for GitHub Webhooks
1. Handle the Ping Event
GitHub sends a ping event when you create a webhook. Handle it to confirm setup:
if (event === 'ping') {
console.log('Webhook configured! Zen:', payload.zen);
return res.json({ message: 'pong' });
}
2. Use Delivery ID for Idempotency
const deliveryId = req.headers['x-github-delivery'];
// Check if already processed
const conn = await Datastore.open();
const existing = await conn.getOne('github_deliveries', deliveryId);
if (existing) {
return res.json({ received: true, duplicate: true });
}
// Process and store
await processWebhook(payload);
await conn.insertOne('github_deliveries', {
_id: deliveryId,
event,
processedAt: new Date().toISOString()
});
3. Respond Quickly
GitHub expects a response within 10 seconds:
app.post('/github/webhooks', async (req, res) => {
// Verify and acknowledge immediately
if (!verifyGitHubWebhook(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
res.json({ received: true });
// Process asynchronously
await queueWebhookProcessing(req.body, req.headers);
});
Why Use Codehooks.io for GitHub Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---|---|---|
| Setup time | Hours/Days | 5 minutes |
| Signature verification | Manual implementation | Built-in support |
| Scaling | Configure infrastructure | Automatic |
| Multiple repos | Complex routing | Simple |
| Templates | Build from scratch | Ready-to-use |
| Cost | Server costs | Free tier available |
Related Resources
- GitHub Webhook Template - Ready-to-deploy template
- All Codehooks Templates - Browse all webhook templates
- GitHub Webhooks Documentation
- GitHub Webhook Events Reference
- Codehooks.io Quick Start
- Webhook Delivery System - Build your own outgoing webhook system
GitHub Webhooks FAQ
Common questions about handling GitHub webhooks
How do I verify GitHub webhook signatures?
X-Hub-Signature-256 header. Create an HMAC-SHA256 hash of the raw request body using your webhook secret, prepend 'sha256=', and compare using crypto.timingSafeEqual(). Always use the raw body, not parsed JSON.Why is my GitHub webhook signature verification failing?
application/json in GitHub settings. Ensure you're using x-hub-signature-256.What is the ping event?
ping event when you first create a webhook. It confirms your endpoint is reachable and properly configured. The payload includes a zen field with a random GitHub-ism quote. Always handle this event to verify your setup works.How do I receive webhooks for multiple repositories?
What happens if my webhook endpoint is down?
How do I handle different events in one endpoint?
X-GitHub-Event header to determine the event type, then use a switch statement to handle each event differently. Each event has a specific payload structure documented in GitHub's webhook events reference.Can I filter which events trigger my webhook?
How do I test GitHub webhooks?
coho deploy to get a public URL instantly. Then configure that URL in GitHub and use the 'Recent Deliveries' tab to redeliver test events.