← Back to all blogs
July 9, 2026·3 min read·By Abuzar Siddiqui

Verifying Discord Interactions: Implementing Ed25519 Signature Verification from Scratch

Verifying Discord Interactions: Implementing Ed25519 Signature Verification from Scratch

When you create a Discord slash command, Discord sends every interaction to your server as an HTTP request. Before your application processes that request, Discord expects you to verify that it genuinely came from Discord.

This verification isn't optional—it's a security requirement.

Recently, I was building a small project as part of a job interview. Instead of solving a typical coding problem, I was asked to create something that showcased how I approach real-world engineering challenges. I decided to build a Discord application, and everything was going smoothly until Discord started rejecting my interaction endpoint.

At first, I assumed there was a bug in my code or that I had misconfigured the endpoint. But after digging through the documentation, I discovered that Discord requires every interaction request to be verified using an Ed25519 signature before it will trust your server. It wasn't just another API requirement—it was a fundamental security check.

That sent me down the rabbit hole of understanding how digital signatures work, why Discord uses Ed25519, and what can silently cause verification to fail. This article is the result of that journey.


Why Signature Verification Exists

Imagine your interaction endpoint is publicly accessible:

POST /interactions

Without signature verification, anyone on the internet could send a forged HTTP request to your endpoint and make it appear as though Discord had triggered one of your slash commands.

Discord prevents this by digitally signing every interaction request with its private key. Your server verifies that signature using Discord's public key before processing the request. If the verification fails, the request must be rejected immediately.

But here's something I found particularly interesting while implementing this.

Discord doesn't simply trust that you've written the verification code correctly.

When you register or update your interaction endpoint, Discord intentionally sends invalid interaction requests with incorrect signatures. These requests are expected to fail verification. If your server accepts one of these deliberately invalid requests instead of returning HTTP 401 Unauthorized, Discord assumes your verification logic is broken and refuses to save or validate your endpoint.

In other words, Discord is actively testing your implementation.

It's a clever security measure. Instead of only checking that valid requests work, Discord also checks that invalid requests are rejected. That ensures your endpoint isn't blindly accepting every incoming request and that your Ed25519 verification is actually protecting your application.

Only after your server consistently rejects invalid signatures and accepts valid ones does Discord consider the endpoint secure enough to receive real user interactions.

That small detail completely changed how I thought about signature verification. It's not just about making the "happy path" work—it's about proving that your application refuses to trust anything that isn't genuinely signed by Discord.


The Complete Tutorial: Verifying Discord Requests Step by Step

Once I understood why verification mattered, the next challenge was actually implementing it correctly. On paper, the process looks simple. In practice, there are a few subtle details that can easily break everything.

Here's the exact flow Discord expects your server to follow.

Step 1: Extract Required Headers

Every interaction request from Discord includes two important headers:

  • X-Signature-Ed25519

  • X-Signature-Timestamp

You'll need both of these to verify the request.

The signature arrives as a hex-encoded Ed25519 signature, while the timestamp is included to help prevent replay attacks.

If you're unfamiliar with hexadecimal encoding or want to inspect what a hex string actually represents while debugging, a Hex Encoder/Decoder can be handy:

Hex Encoder/Decoder

Step 2: Read the Raw Request Body

This is where things can go wrong very quickly.

You must use the raw request body exactly as received, before any parsing or modification. If your framework automatically parses JSON and alters whitespace or formatting, the signature verification will fail.

So instead of doing something like:

app.use(express.json())

You need access to the raw body:

app.use(express.raw({ type: '*/*' }))

During debugging, it's often useful to log the raw payload without accidentally modifying it. If you're exchanging payloads over chat, storing test cases, or embedding them in documentation, converting them with a Base64 Encoder/Decoder helps preserve the exact bytes:

Base64 Encoder/Decoder

Step 3: Construct the Message

Discord signs a message that is simply:

timestamp + body

So you concatenate the timestamp header with the raw request body:

const message = timestamp + body

No separators. No transformations. No extra whitespace.

Just the timestamp immediately followed by the raw request body.


Step 4: Verify Using Ed25519

Now you verify the signature using Discord's public key.

Here's an example using Node.js with the tweetnacl library:

const nacl = require('tweetnacl')

function verifySignature(signature, timestamp, body, publicKey) {
  const message = Buffer.from(timestamp + body)
  const sig = Buffer.from(signature, 'hex')
  const key = Buffer.from(publicKey, 'hex')

  return nacl.sign.detached.verify(message, sig, key)
}

If this function returns false, immediately reject the request.

While working with cryptographic code, you'll often encounter hashes, signatures, hexadecimal strings, and encoded binary data. It's important to remember that a digital signature isn't the same thing as a hash—although the two are closely related in many cryptographic systems. If you're experimenting with hashing algorithms or comparing digests while learning how cryptographic primitives fit together, a simple Hash Generator is a useful companion:

Hash Generator

Step 5: Reject Invalid Requests

If verification fails, respond with:

HTTP 401 Unauthorized

Do not process the request any further.

This is critical—not just for security, but because Discord actively checks that your server rejects invalid signatures before allowing your endpoint to receive real interactions.


Step 6: Handle Valid Requests

If verification succeeds, you can safely parse the JSON body and handle the interaction.

For example:

const interaction = JSON.parse(body)

if (interaction.type === 1) {
  return res.json({ type: 1 })
}

Only after the signature has been verified should your application trust the payload enough to deserialize and process it.


Common Pitfalls That Break Verification

While implementing this, I ran into a few issues that weren't obvious at first:

  • Parsing the body before verification changes the payload and invalidates the signature.

  • Using the wrong public key from the Discord Developer Portal.

  • Forgetting to concatenate the timestamp with the body.

  • Mixing up hexadecimal strings, UTF-8 strings, and raw bytes.

  • Middleware silently modifying the request body.

Each of these can cause verification to fail even when the implementation looks perfectly reasonable.


Final Thoughts

Implementing Ed25519 verification isn't complicated once you understand the individual pieces, but it's extremely unforgiving. Tiny mistakes don't produce useful error messages—they simply result in Discord rejecting your endpoint.

What impressed me most was how Discord validates your implementation. It doesn't just assume you've implemented verification correctly—it actively sends requests with invalid signatures to ensure your server refuses to trust them.

That's a lesson that extends far beyond Discord.

Security isn't about making valid requests succeed.

It's about making invalid requests fail for the right reasons.

Explore More ArticlesPublished on AbiTechPros Blog