Discord
Verify Discord interactions and Webhook Events — the right PING answer for both products, and the 401 the setup probe demands.
Grab your app’s public key from the developer portal — Discord verifies with public-key signatures, so there is no shared secret to leak:
import { createWebhookHandler } from 'webhooks-sdk'
import { discord } from 'webhooks-sdk/discord'
const handler = createWebhookHandler({
provider: discord({ publicKey: process.env.DISCORD_PUBLIC_KEY! }),
on: {
application_command: async (event) => await runCommand(event.payload),
},
})
export const POST = handler.fetch
Signatures are Ed25519 over the timestamp and body, so stale requests fall outside the replay window on their own.
Options
| Option | Type | Default | |
|---|---|---|---|
publicKey |
string | string[] |
— | The app’s public key (hex, 64 chars) from the developer portal. |
mode |
'interactions' | 'events' |
'interactions' |
Which Discord product this endpoint serves — see below. |
tolerance |
number |
300 |
Replay window in seconds. 0 disables it. |
Two products that disagree
Discord ships an interactions endpoint and a Webhook Events API.
Both use the same Ed25519 verification, and both open with a PING — but
PING is type: 1 on one and type: 0 on the other, and they expect
different answers ({"type":1} versus a bare 204). Since type: 1 means
“an event” in the second product, nothing in the payload distinguishes
them — so the provider takes a mode instead of guessing:
discord({ publicKey })Event types are the interaction kinds: ping, application_command,
message_component, application_command_autocomplete, modal_submit.
discord({ publicKey, mode: 'events' })Event types come from the payload’s event.type field.
The handshake is signed — and strict
When you save an endpoint URL, Discord probes it with a deliberately
invalid signature and refuses to save the URL unless it receives a
401. It then sends a signed PING that must be answered correctly.
The provider declares signedHandshake: true, so verification runs before
the handshake and both probes are answered properly. If endpoint
registration keeps failing, something in front of the SDK (a catch-all
error page, a proxy returning 200s) is answering the invalid probe instead
of letting the 401 through. See Handshakes.
Standalone & testing
import {
verifyDiscordWebhook, // (raw, { publicKey, mode?, tolerance? }) — throws on failure
parseDiscordWebhook, // (raw, options) — the envelope
signDiscordWebhook, // (body, privateKey, timestamp?) — both headers, for tests
} from 'webhooks-sdk/discord'
signDiscordWebhook takes a private CryptoKey — generate a throwaway
Ed25519 pair in the test and give the provider the public half. See
Testing.