Discord: "The specified interactions endpoint url could not be verified."

You paste your URL into the Developer Portal, hit Save, and Discord throws "interactions_endpoint_url: The specified interactions endpoint url could not be verified." — with zero detail about what went wrong. Here's exactly what Discord does when you press Save, why "just return 200" endpoints always fail, and the specific bugs that cause 90% of these rejections.

1. What Discord actually does when you press Save

Saving the URL triggers an immediate, two-part test of your endpoint:

Both halves must pass or the URL won't save. And it doesn't stop there: Discord keeps running these security checks routinely after you've saved. If your endpoint later regresses (say, a refactor drops the verification middleware), Discord removes your interactions URL and alerts you by email and system DM.

2. Why you can't use a capture URL as the endpoint (and what it's still good for)

Honest note: you cannot point Discord at a Hookden bin (or webhook.site, or any inspector) and pass verification. A capture URL returns a configured, static response — it can't verify Ed25519 signatures in real time, so it answers the invalid-signature probes the same way as the real PING, and Discord rejects it. The same goes for our CLI relay: it re-delivers requests to localhost asynchronously, but Discord needs the verified response to come back on the original connection, synchronously. No capture tool can pass this check — any that claims to is skipping the signature test.

What a bin is good for: seeing the test traffic with your own eyes. Create a bin (curl https://hookden.pages.dev/new), paste it as the endpoint URL, press Save — verification will fail, but the probes land in your dashboard: the exact PING body, the X-Signature-Ed25519 and X-Signature-Timestamp headers, the Discord-Interactions/1.0 user agent, and what the bad-signature probes look like next to the good one. Once you've seen the real traffic, the verification recipe below stops being abstract.

3. The verification recipe

Every interaction request carries two headers:

The signed message is timestamp + raw request body, concatenated as strings, verified against your app's Public Key — the hex string on the General Information page of the Developer Portal. Node example (tweetnacl):

const nacl = require('tweetnacl');
const PUBLIC_KEY = 'your app public key (hex, from the Dev Portal)';

const sig = req.get('X-Signature-Ed25519');
const ts  = req.get('X-Signature-Timestamp');
const ok  = nacl.sign.detached.verify(
  Buffer.from(ts + rawBody),           // rawBody = exact bytes, as a string
  Buffer.from(sig, 'hex'),
  Buffer.from(PUBLIC_KEY, 'hex'),
);
if (!ok) return res.status(401).end('invalid request signature');
const body = JSON.parse(rawBody);
if (body.type === 1) return res.json({ type: 1 });   // PING → ACK

Python (PyNaCl):

from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError

verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY))
try:
    verify_key.verify(f'{timestamp}{raw_body}'.encode(),
                      bytes.fromhex(signature))
except BadSignatureError:
    abort(401, 'invalid request signature')

Or skip the hand-rolling: the official discord-interactions packages (JS/Python) ship a verifyKey helper that does exactly this.

4. The five bugs that cause almost every rejection

5. Developing on localhost

Because the verified response must come from your code, synchronously, local development needs a real tunnel — this is one case where a relay genuinely can't substitute. cloudflared tunnel --url http://localhost:3000 gives you a free public HTTPS URL; ngrok works too if its free-plan limits fit you (see our honest tunnel comparison). Paste the tunnel URL into the portal, and keep in mind the URL must stay reachable — if the tunnel dies, Discord's routine re-checks will eventually unset your endpoint.

For everything around the interactions endpoint — outgoing Discord webhooks you POST to, third-party webhooks your bot consumes, or replaying a captured interaction payload at your handler while unit-testing the parsing logic (verification stubbed) — a capture URL still earns its keep: testing webhooks, webhooks to localhost.

No signup needed. Or from your terminal: curl https://hookden.pages.dev/new

← All guides · Docs · Hookden vs webhook.site