Skip to main content

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.

Use the Template

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! 🙌
Finding your API endpoint

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

  1. Go to your repository → SettingsWebhooks
  2. Click Add webhook
  3. Enter:
    • Payload URL: https://your-app.api.codehooks.io/dev/github/webhooks
    • Content type: application/json
    • Secret: Your webhook secret (save this!)
  4. Select events:
    • Choose Let me select individual events
    • Select: Push, Pull requests, Issues, etc.
  5. 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

EventTriggerCommon Use Case
pushCode pushed to branchCI/CD, deployments
pull_requestPR opened/closed/mergedCode review automation
pull_request_reviewReview submittedMerge automation
issuesIssue created/updatedTicket management
issue_commentComment on issue/PRBot responses
releaseRelease publishedProduction deployment
workflow_runGitHub Action completedStatus notifications
createBranch/tag createdEnvironment provisioning
deleteBranch/tag deletedCleanup resources
deployment_statusDeployment status changeMonitoring
starRepository starredAnalytics
forkRepository forkedTracking

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), not req.body (parsed JSON)
  • Use x-hub-signature-256 (SHA-256), not the deprecated x-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?

FeatureDIY SetupCodehooks.io
Setup timeHours/Days5 minutes
Signature verificationManual implementationBuilt-in support
ScalingConfigure infrastructureAutomatic
Multiple reposComplex routingSimple
TemplatesBuild from scratchReady-to-use
CostServer costsFree tier available

GitHub Webhooks FAQ

Common questions about handling GitHub webhooks

How do I verify GitHub webhook signatures?
GitHub sends a signature in the 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?
Common causes: (1) Using parsed JSON body instead of raw body, (2) Wrong secret - copy it exactly from GitHub, (3) Using SHA-1 header instead of SHA-256, (4) Content-Type not set to application/json in GitHub settings. Ensure you're using x-hub-signature-256.
What is the ping event?
GitHub sends a 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?
Options: (1) Create a webhook in each repository pointing to the same URL, (2) Create an organization webhook to receive events from all repos in the org, (3) Use a GitHub App which can receive webhooks for all installed repositories.
What happens if my webhook endpoint is down?
GitHub retries failed webhooks. After a failure, GitHub will attempt redelivery with exponential backoff. You can also manually redeliver from the webhook settings page. Check the 'Recent Deliveries' tab to see delivery status and response codes.
How do I handle different events in one endpoint?
Check the 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?
Yes, when creating the webhook, select 'Let me select individual events' and choose only the events you need. This reduces unnecessary traffic and processing. You can also update the event selection later in webhook settings.
How do I test GitHub webhooks?
With Codehooks.io, just deploy and you're live in seconds - no tunneling or local setup needed. Use coho deploy to get a public URL instantly. Then configure that URL in GitHub and use the 'Recent Deliveries' tab to redeliver test events.