Skip to content
Webhooks SDK
Esc
navigateopen⌘Jpreview
On this page

Discord

Ed25519 signatures over timestamp + body — with a mode switch for Discord's two disagreeing products.

Discord signs each request with an Ed25519 public-key signature over {timestamp}{body}, via X-Signature-Ed25519 and X-Signature-Timestamp (scheme family 4). No shared secret to leak — you configure the public key from the developer portal.

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

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.

Was this page helpful?