Verify Discord webhooks
Verify that a webhook really came from Discord before acting on it.
| Identifier | 'discord' |
| Import | import { verifyDiscord } from 'verihook/discord' |
| Headers | x-signature-ed25519x-signature-timestamp |
| Signature | Ed25519 signature of <timestamp><raw body>. |
| Replay window | 300 seconds (options.tolerance) |
result.eventType |
the webhook event’s event.type, or the interaction type (PING, …) |
Get your secret
Section titled “Get your secret”The application’s Public Key (hex): in the Discord Developer Portal, open your application → General Information. It’s a public key, so there is no shared secret to protect.
Store it in an environment variable (DISCORD_PUBLIC_KEY below) and never commit it.
Verify a request
Section titled “Verify a request”import { verifyDiscord } from 'verihook/discord';
export async function POST(request: Request) { const result = await verifyDiscord(request, process.env.DISCORD_PUBLIC_KEY!);
if (!result.valid) { console.warn(result.code, result.reason, result.hint); return new Response('Invalid signature', { status: 401 }); }
console.log(result.eventType, result.event); return Response.json({ received: true });}request can be a Fetch Request or { headers, body, url? } with the raw body. verifyWebhook('discord', request, secret) from verihook does the same. Read why the raw body matters if verification fails behind a body parser.
Framework examples
Section titled “Framework examples”import { createWebhookHandler } from 'verihook/next';
export const POST = createWebhookHandler('discord', process.env.DISCORD_PUBLIC_KEY!, async (payload, result) => { // Runs only for a valid signature console.log(result.eventType, result.event);});import express from 'express';import { verihookExpress, type VerihookRequestAdditions } from 'verihook/express';
const app = express();
// Register before express.json(), or keep express.json() off this routeapp.post('/webhooks/discord', verihookExpress('discord', process.env.DISCORD_PUBLIC_KEY!), (req, res) => { const { eventType, event } = (req as typeof req & VerihookRequestAdditions).verihook!; res.json({ received: true });});import Fastify from 'fastify';import { verihookFastify, verihookRawBody, type VerihookFastifyRequest } from 'verihook/fastify';
const app = Fastify();await app.register(verihookRawBody);
app.post('/webhooks/discord', { preHandler: verihookFastify('discord', process.env.DISCORD_PUBLIC_KEY!) }, async (request) => { const { eventType, event } = (request as typeof request & VerihookFastifyRequest).verihook!; return { received: true };});import { Hono } from 'hono';import { verihookHono, type VerihookVariables } from 'verihook/hono';
const app = new Hono<{ Bindings: { DISCORD_PUBLIC_KEY: string }; Variables: VerihookVariables }>();
app.post('/webhooks/discord', verihookHono('discord', (c) => c.env.DISCORD_PUBLIC_KEY), (c) => { const { eventType, event } = c.get('verihook'); return c.json({ received: true });});// main.ts: NestFactory.create(AppModule, { rawBody: true })import { Controller, Post, Req, UseGuards } from '@nestjs/common';import { createVerihookGuard, type VerihookNestRequest } from 'verihook/nestjs';
@Controller('webhooks')export class WebhooksController { @Post('discord') @UseGuards(createVerihookGuard('discord', process.env.DISCORD_PUBLIC_KEY!)) handle(@Req() req: VerihookNestRequest) { const { eventType, event } = req.verihook!; }}import { createWebhookHandler } from 'verihook/h3';
export default defineEventHandler( createWebhookHandler('discord', process.env.DISCORD_PUBLIC_KEY!, async (payload, result) => { console.log(result.eventType); }),);import { env } from '$env/dynamic/private';import { createWebhookHandler } from 'verihook/sveltekit';
export const POST = createWebhookHandler('discord', () => env.DISCORD_PUBLIC_KEY, async (payload, result) => { console.log(result.eventType);});import { createWebhookHandler } from 'verihook/remix';
export const action = createWebhookHandler('discord', process.env.DISCORD_PUBLIC_KEY!, async (payload, result) => { console.log(result.eventType);});import { createWebhookHandler } from 'verihook/astro';
export const prerender = false;export const POST = createWebhookHandler('discord', import.meta.env.DISCORD_PUBLIC_KEY, async (payload, result) => { console.log(result.eventType);});import { createWebhookHandler } from 'verihook/lambda';
export const handler = createWebhookHandler('discord', process.env.DISCORD_PUBLIC_KEY!, async (payload, result) => { console.log(result.eventType);});Each adapter responds 401 to an invalid signature, 413 to a body over maxBodySize (2 MB by default) and 200 to a duplicate when you pass a dedupeStore.
Setup handshake
Section titled “Setup handshake”Interactions start with a PING (type 1) that must be answered with { "type": 1 }:
import { createWebhookHandler } from 'verihook/next';
export const POST = createWebhookHandler('discord', process.env.DISCORD_PUBLIC_KEY!, async (payload) => { if ((payload as { type?: number }).type === 1) { return Response.json({ type: 1 }); } // handle the interaction});Things to know
Section titled “Things to know”- Discord sends invalid signatures on purpose when you save the endpoint URL, and refuses the URL unless you reject them with 401. The adapters already do that.
Testing
Section titled “Testing”signWebhook from verihook/testing builds a correctly signed Discord request for your tests:
import { signWebhook } from 'verihook/testing';import { verifyDiscord } from 'verihook/discord';
const hook = await signWebhook('discord', { payload: { id: 'evt_test' },});
const result = await verifyDiscord( { headers: hook.headers, body: hook.body, url: hook.url }, hook.secret,);// result.valid === trueDiscord uses a key pair. signWebhook generates one and returns the public key as hook.secret, so verify with that.
From the command line, npx verihook simulate discord --url http://localhost:3000/webhooks/discord sends one to your local server.
See Testing your handlers for supertest and Fetch Request examples.
Troubleshooting
Section titled “Troubleshooting”Failed results carry a code and often a hint with the likely cause. See Troubleshooting for each error code.