Discord webhooks let you send messages to channels automatically. But without verification, anyone who knows your webhook URL can send fake messages. This can lead to spam, phishing, or data leaks. Discord does not sign webhook requests by default. This article explains how to add a signature to your webhook requests so your server can verify that each message came from you.
Key Takeaways: Securing Discord Webhooks with Signed Requests
- HMAC-SHA256 signature: Encrypts the request body with a secret key to prove authenticity.
- X-Signature-Ed25519 header: Alternative method using Ed25519 public-key cryptography for signed interactions.
- Verification endpoint: Your server checks the signature before processing any webhook payload.
Why Signing Discord Webhook Requests Matters
A Discord webhook URL is a simple HTTP endpoint. Anyone who intercepts or guesses that URL can send a POST request with any content. Your bot or application will treat it as legitimate. This is a security gap. Signing the request means your sender adds a cryptographic hash to the HTTP headers. Your receiver recalculates the hash using a shared secret. If the hashes match, the request is authentic. If they do not match, the request is rejected.
How Signature Verification Works
The sender computes an HMAC-SHA256 hash of the request body using a secret key. The hash is placed in a custom header, often X-Signature-256. The receiver reads the header, computes its own HMAC-SHA256 of the body with the same secret, and compares the two. A constant-time comparison prevents timing attacks. This method works for any programming language that supports HMAC.
Alternative: Ed25519 Signatures
Discord itself uses Ed25519 signatures for its Interactions Endpoint. This is a public-key system. The sender signs with a private key. The receiver verifies with the corresponding public key. For custom webhooks, you can adopt the same pattern. It is more secure than HMAC because the private key never leaves the sender. However, it requires more setup and a library like nacl or tweetnacl.
Steps to Sign and Verify Discord Webhook Requests
These steps assume you control both the sender (your application) and the receiver (another server or bot). You need a shared secret. Generate a random 32-byte hex string. Keep it in a secure environment variable on both sides.
Method 1: HMAC-SHA256 Signing (Recommended for Most Users)
- Generate a shared secret
Runopenssl rand -hex 32in your terminal. Copy the output. This is your signing key. - Create the signature on the sender side
Take the raw request body as a string. ComputeHMAC-SHA256(secret, body). Convert the result to a hex string. Add it as a header:X-Signature-256: <hex string>. - Send the webhook request
Include theX-Signature-256header along with the POST request to your receiver endpoint. - Verify on the receiver side
Read theX-Signature-256header from the incoming request. ComputeHMAC-SHA256(secret, body)using the same secret. Compare the two hex strings using a constant-time comparison function. If they match, process the request. If not, return a 401 Unauthorized response.
Method 2: Ed25519 Signing (Advanced)
- Generate an Ed25519 key pair
Use a library liketweetnaclin Node.js orPyNaClin Python. Generate a private key and a public key. Store the private key on the sender. Share the public key with the receiver. - Sign the request body on the sender
Sign the raw body string with the private key. Encode the signature as a hex string. Add it as a header:X-Signature-Ed25519: <hex string>. - Send the request
Include theX-Signature-Ed25519header with the POST request. - Verify on the receiver
Read the header. Use the public key to verify the signature against the body. If verification succeeds, process the request. If it fails, reject it.
Common Mistakes and Limitations
Signature Header Name Mismatch
Both sides must agree on the exact header name. If the sender uses X-Signature-256 but the receiver expects X-Signature, verification fails. Define the header name in your documentation and enforce it in code.
Body Encoding Differences
The signature is computed from the exact byte sequence of the request body. If the sender includes a newline at the end and the receiver does not, the hashes will differ. Use a consistent encoding, such as UTF-8 without BOM, and avoid whitespace changes.
Timing Attacks on Comparison
Never use a simple equality operator to compare HMAC strings. That allows attackers to guess the signature byte by byte. Use a constant-time comparison function provided by your language. In Python, use hmac.compare_digest. In Node.js, use crypto.timingSafeEqual.
Secret Key Exposure
If your shared secret leaks, anyone can forge signatures. Rotate the secret periodically. Store it in a secrets manager or environment variable. Never hardcode it in source code.
HMAC-SHA256 vs Ed25519 for Webhook Signing
| Item | HMAC-SHA256 | Ed25519 |
|---|---|---|
| Key type | Symmetric (shared secret) | Asymmetric (private + public) |
| Setup complexity | Low | Medium |
| Security strength | Strong | Stronger (no shared secret on receiver) |
| Key rotation | Requires updating both sides | Only rotate private key |
| Library support | Built into most languages | Requires external library |
Signing your Discord webhook requests stops unauthorized senders from posting fake messages. Choose HMAC-SHA256 for simplicity or Ed25519 for stronger security. Implement constant-time comparison and rotate your secret regularly. After signing, test the setup by sending a request with a wrong signature to confirm your receiver rejects it.