GitHub webhooks are great for repository automation, but getting useful push notifications into Discord often becomes more work than expected.
You need to receive the GitHub event securely, validate its signature, send it to Discord, and make sure a temporary network error does not silently lose the notification.
This tutorial shows a small, secure approach:
GitHub → Serverless verification bridge → RelayHook → Discord
The bridge verifies that GitHub really sent the event. RelayHook then handles delivery, retries, delivery logs, and failed-message inspection.
There is no always-on server or VPS to maintain—just a small serverless route.
Why not point GitHub directly at RelayHook?
RelayHook production ingress endpoints require an X-API-Key header.
GitHub's native webhook configuration supports a payload URL, content type, secret, and event selection—but it does not let you add arbitrary request headers such as X-API-Key.
The solution is to use a small server-side bridge:
- GitHub sends the webhook to your bridge.
- The bridge verifies GitHub's
X-Hub-Signature-256. - The bridge adds the RelayHook API key safely from its environment variables.
- RelayHook delivers the event to your Discord webhook.
Your RelayHook API key never appears in GitHub settings, browser code, or repository source code.
What you need
Before starting, prepare:
- A GitHub repository where you have admin access.
- A Discord Incoming Webhook URL.
- A RelayHook account and project.
- A project API key and Ingress URL from RelayHook.
- A serverless application. This example uses a Next.js App Router route and can be deployed on Vercel.
1. Configure RelayHook
Create a RelayHook project, then add your Discord Incoming Webhook URL as a Discord destination.
Next, copy two values from the project:
RELAYHOOK_INGRESS_URL
RELAYHOOK_API_KEY
Keep the API key secret. It must only exist in server-side environment variables.
For this example, keep the project in the default Raw ingress mode. RelayHook will preserve the GitHub event envelope and deliver it as formatted JSON to Discord.
2. Add environment variables
In your Next.js project, add these variables locally and in your hosting provider:
GITHUB_WEBHOOK_SECRET=replace-with-a-long-random-secret
RELAYHOOK_INGRESS_URL=https://relay-hook-api.fluxhub.dev/v1/ingress/your-project-token
RELAYHOOK_API_KEY=rh_your_project_api_key
Generate a long random value for GITHUB_WEBHOOK_SECRET. You will paste the exact same value into GitHub's Webhook Secret field later.
Do not expose any of these variables with a NEXT_PUBLIC_ prefix.
3. Create the verification bridge
Create this file:
app/api/github-webhook/route.ts
Then add:
import { createHmac, timingSafeEqual } from "node:crypto";
function hasValidSignature(body: string, signature: string | null) {
if (!signature) return false;
const expected =
"sha256=" +
createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET!)
.update(body)
.digest("hex");
const actualBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expected);
return (
actualBuffer.length === expectedBuffer.length &&
timingSafeEqual(actualBuffer, expectedBuffer)
);
}
export async function POST(request: Request) {
// GitHub signs the original request body.
const rawBody = await request.text();
const signature = request.headers.get("x-hub-signature-256");
if (!hasValidSignature(rawBody, signature)) {
return new Response("Invalid GitHub signature", { status: 401 });
}
const event = request.headers.get("x-github-event") ?? "unknown";
// GitHub sends a ping as soon as a webhook is created.
if (event === "ping") {
return new Response("pong", { status: 200 });
}
const response = await fetch(process.env.RELAYHOOK_INGRESS_URL!, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.RELAYHOOK_API_KEY!,
},
body: JSON.stringify({
source: "github",
event,
// Useful for log correlation and downstream deduplication.
event_id: request.headers.get("x-github-delivery"),
data: JSON.parse(rawBody),
}),
});
return response.ok
? new Response("Accepted", { status: 200 })
: new Response("RelayHook rejected the event", { status: 502 });
}
This route does three important things:
- Verifies the GitHub HMAC signature before trusting the request.
- Keeps RelayHook credentials on the server.
- Passes GitHub's delivery ID along with the event.
4. Deploy the route
Deploy your Next.js application to Vercel or another serverless platform.
After deployment, your endpoint will look similar to:
https://your-app.vercel.app/api/github-webhook
Use HTTPS. GitHub webhooks should never target an unsecured endpoint.
5. Configure the GitHub Webhook
In your GitHub repository:
- Open Settings → Webhooks → Add webhook.
- Set Payload URL to your deployed bridge URL:
https://your-app.vercel.app/api/github-webhook
- Set Content type to:
application/json
- Paste your
GITHUB_WEBHOOK_SECRETinto Secret. - Choose Let me select individual events.
- Enable Pushes.
- Save the webhook.
GitHub sends a ping event immediately. Your bridge returns 200 pong, so the GitHub delivery page should show a successful response.
6. Test it
Push a small commit:
git commit --allow-empty -m "Test GitHub webhook notification"
git push
Then check each layer:
GitHub
Open the webhook's Recent Deliveries page and confirm a2xxresponse.RelayHook
Open Logs and confirm that an ingress event and a Discord delivery job were created.Discord
Confirm that the GitHub push payload arrived in your target channel.
If Discord is temporarily unavailable, RelayHook keeps a durable delivery job, retries the request automatically, and records final failures in the dead-letter queue instead of silently losing the event.
Troubleshooting
GitHub gets a 401 response
The webhook secret and GITHUB_WEBHOOK_SECRET do not match, or the raw request body was modified before validation.
Always calculate the HMAC using the untouched await request.text() body.
GitHub ping succeeds but push events do not arrive
Check that:
- The webhook is active.
- The Pushes event is enabled.
- The deployed route has the latest environment variables.
- Your deployment logs show incoming requests.
RelayHook rejects the event
Make sure RELAYHOOK_API_KEY belongs to the same RelayHook project as RELAYHOOK_INGRESS_URL.
Discord delivery fails
Open the failed delivery job in RelayHook Logs. A common cause is a revoked or deleted Discord Incoming Webhook URL.
A note about duplicate delivery
Webhook delivery is generally at least once, not exactly once.
GitHub may redeliver events, and a reliable delivery system may retry a request after a temporary failure. If your downstream action must happen exactly once, use GitHub's X-GitHub-Delivery value (sent here as event_id) to deduplicate it in your own application.
Wrap-up
A small serverless bridge is enough to securely connect GitHub to RelayHook:
GitHub verifies → RelayHook persists → Discord receives
RelayHook gives you a central place to fan out events to Discord, Slack, Telegram, or email—with delivery logs, retries, and a dead-letter queue.
Try RelayHook here:
https://relay-hook-app.fluxhub.dev/?utm_source=devto&utm_medium=article&utm_campaign=github_discord
Top comments (0)