Skip to content
Webhooks SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Last updated on September 1, 2026

Was this page helpful?