Fix GitHub Webhook Signature in Next.js

GitHub webhooks use HMAC-SHA256 with the X-Hub-Signature-256 header. The same raw body problem applies — Next.js parses JSON before you can verify the signature.

App Router handler

// app/api/webhooks/github/route.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { NextResponse } from 'next/server';

const secret = process.env.GITHUB_WEBHOOK_SECRET!;

export async function POST(request: Request) {
  const body = await request.text(); // raw body first
  const sig  = request.headers.get('x-hub-signature-256');

  if (!sig) {
    return NextResponse.json({ error: 'Missing X-Hub-Signature-256' }, { status: 400 });
  }

  const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');

  // Use timingSafeEqual to prevent timing attacks
  const sigBuf = Buffer.from(sig);
  const expBuf = Buffer.from(expected);
  if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const payload = JSON.parse(body);
  const event   = request.headers.get('x-github-event');

  switch (event) {
    case 'push':
      // handle push
      break;
    case 'pull_request':
      // handle PR
      break;
    case 'issues':
      // handle issues
      break;
  }

  return NextResponse.json({ received: true });
}
Always use timingSafeEqual for signature comparison. A simple === comparison leaks timing information that can be used to forge signatures.

Pages Router handler

// pages/api/webhooks/github.ts
import { buffer } from 'micro';
import { createHmac, timingSafeEqual } from 'crypto';
export const config = { api: { bodyParser: false } };

const secret = process.env.GITHUB_WEBHOOK_SECRET!;

export default async function handler(req, res) {
  const rawBody = (await buffer(req)).toString('utf8');
  const sig     = req.headers['x-hub-signature-256'] as string;

  if (!sig) return res.status(400).json({ error: 'Missing signature' });

  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  const sigBuf   = Buffer.from(sig);
  const expBuf   = Buffer.from(expected);

  if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event   = req.headers['x-github-event'];
  const payload = JSON.parse(rawBody);

  res.json({ received: true });
}

Set the secret in GitHub

Repository → Settings → Webhooks → Edit → Secret
# Add the same value as GITHUB_WEBHOOK_SECRET in your .env
# GitHub hashes the payload with this secret using HMAC-SHA256

Generate the complete GitHub webhook handler for your framework.

Open WebhookFix →
For informational purposes only. Always test in staging before production. MetricLogic accepts no responsibility for issues arising from use of these tools. © 2026 MetricLogic.
HttpFixer by MetricLogic · Blog · All Tools · Generators MIT · GitHub →