Skip to content

Webhook Events

Webhook events allow your app to receive notifications from Discord about events happening in your application, such as entitlement purchases, lobby messages, or app authorizations. Unlike Gateway events, webhook events are sent over HTTP and don’t require maintaining a persistent connection.

Webhook events are one-way HTTP notifications sent from Discord to your app when specific events occur. They differ from interactions in several key ways:

FeatureInteractionsWebhook Events
DirectionUser → Your AppDiscord → Your App
PurposeHandle user actionsReceive event notifications
Response Time3 seconds3 seconds (acknowledgment)
ExamplesSlash commands, buttonsApplication authorized, Entitlement created
  1. Create a WebhookEventHandler

    Choose the event type you want to handle from ApplicationWebhookEventType:

    import { WebhookEventHandler } from "honocord";
    import { ApplicationWebhookEventType } from "discord-api-types/v10";
    const entitlementHandler = new WebhookEventHandler(ApplicationWebhookEventType.EntitlementCreate);
    entitlementHandler.addHandler(async (c) => {
    const entitlement = c.var.data;
    console.log(`New entitlement: ${entitlement.id}`);
    // Process the entitlement...
    return c.body(null, 200); // empty response to acknowledge receipt
    });
  2. Load the handler into Honocord

    import { Honocord } from "honocord";
    const bot = new Honocord();
    bot.loadHandlers(entitlementHandler);
    export default bot.getApp(); // Webhook available at /webhook
  3. Configure Discord Developer Portal

    • Navigate to your app’s settings
    • Go to the Webhooks page
    • Enter your public URL: https://your-domain.com/webhook
    • Enable Events and select the events you want to receive
    • Click Save Changes

    Discord will verify your endpoint by sending a PING event.

Honocord supports two modes for webhook handlers, optimized for different deployment environments.

In standard mode, your handler must return a Response object. This gives you full control over the response sent to Discord.

const handler = new WebhookEventHandler(
ApplicationWebhookEventType.LobbyMessageCreate
// Standard mode (default)
);
handler.addHandler(async (c) => {
const message = c.var.data;
// Validate or process...
if (!message.content) {
return c.json({ error: "No content" }, 400);
}
// Must return Response
return c.json({ ok: true });
});

Use standard mode when:

  • Running on traditional servers (Node.js, Bun)
  • You need custom response codes or bodies
  • You want full control over the response

In worker mode, you don’t need to return anything. Honocord automatically responds with 200 OK to Discord and processes your handler asynchronously using waitUntil.

const handler = new WebhookEventHandler(
ApplicationWebhookEventType.EntitlementCreate,
true // Enable worker mode
);
handler.addHandler(async (c) => {
const entitlement = c.var.data;
// Process asynchronously - no return needed
await logToDatabase(entitlement);
await notifyUser(entitlement.user_id);
// No return statement required!
});

Use worker mode when:

  • Deploying to Cloudflare Workers
  • Processing takes longer than 3 seconds
  • You want fire-and-forget processing
  • You don’t need custom response codes

Discord supports various webhook event types. Here are the most common:

Monitor entitlement lifecycle for in-app purchases:

const entitlementCreate = new WebhookEventHandler(ApplicationWebhookEventType.EntitlementCreate);
const entitlementUpdate = new WebhookEventHandler(ApplicationWebhookEventType.EntitlementUpdate);
const entitlementDelete = new WebhookEventHandler(ApplicationWebhookEventType.EntitlementDelete);

Track when users add or remove your app:

const authHandler = new WebhookEventHandler(ApplicationWebhookEventType.ApplicationAuthorized);
authHandler.addHandler(async (c) => {
const auth = c.var.data;
console.log(`App authorized by ${auth.user.username}`);
if (auth.guild) {
console.log(`Added to guild: ${auth.guild.name}`);
}
return c.json({ ok: true });
});
const deauthHandler = new WebhookEventHandler(ApplicationWebhookEventType.ApplicationDeauthorized);

With custom types:

interface MyEnv {
DISCORD_PUBLIC_KEY: string;
DATABASE: D1Database;
}
interface MyVariables {
userId: string;
}
// Specify types explicitly - T is inferred from constructor, then Env and Variables
const authHandler = new WebhookEventHandler<ApplicationWebhookEventType.ApplicationAuthorized, MyEnv, MyVariables>(
ApplicationWebhookEventType.ApplicationAuthorized
);
authHandler.addHandler(async (c) => {
// c.env is now typed as MyEnv
const db = c.env.DATABASE;
// c.var includes both MyVariables and data
const auth = c.var.data;
return c.json({ ok: true });
});

For apps using the Discord Social SDK:

// Lobby messages
const lobbyCreate = new WebhookEventHandler(ApplicationWebhookEventType.LobbyMessageCreate);
const lobbyUpdate = new WebhookEventHandler(ApplicationWebhookEventType.LobbyMessageUpdate);
const lobbyDelete = new WebhookEventHandler(ApplicationWebhookEventType.LobbyMessageDelete);
// Game direct messages
const dmCreate = new WebhookEventHandler(ApplicationWebhookEventType.GameDirectMessageCreate);

All available webhook event types:

  • APPLICATION_AUTHORIZED
  • APPLICATION_DEAUTHORIZED
  • ENTITLEMENT_CREATE
  • ENTITLEMENT_UPDATE
  • ENTITLEMENT_DELETE
  • QUEST_USER_ENROLLMENT (currently unavailable)
  • LOBBY_MESSAGE_CREATE
  • LOBBY_MESSAGE_UPDATE
  • LOBBY_MESSAGE_DELETE
  • GAME_DIRECT_MESSAGE_CREATE
  • GAME_DIRECT_MESSAGE_UPDATE
  • GAME_DIRECT_MESSAGE_DELETE

Event data is available via c.var.data / c.get("data") in your handler:

handler.addHandler(async (c) => {
const eventData = c.var.data; // c.get("data") also works
// Type is automatically inferred based on event type
// For EntitlementCreate, eventData is an Entitlement object
console.log(eventData.id);
console.log(eventData.sku_id);
console.log(eventData.user_id);
return c.json({ ok: true });
});

The context object also provides:

  • c.env - Environment variables (including DISCORD_PUBLIC_KEY)
  • c.req - The original request object
  • c.set() / c.get() - Custom context variables

Honocord automatically handles request verification for you:

  1. Signature Validation - Verifies X-Signature-Ed25519 header using your DISCORD_PUBLIC_KEY
  2. Timestamp Validation - Checks X-Signature-Timestamp to prevent replay attacks
  3. PING Responses - Automatically responds to Discord’s PING verification

All you need to do is provide DISCORD_PUBLIC_KEY in your environment variables.

You can use WebhookEventHandler independently of Honocord (standard mode only):

import { Hono } from "hono";
import { WebhookEventHandler } from "honocord";
const handler = new WebhookEventHandler(ApplicationWebhookEventType.EntitlementCreate);
handler.addHandler(async (c) => {
// Process event
return c.json({ ok: true });
});
// Option 1: As a fetch handler
export default {
fetch: handler.fetch.bind(handler),
};
// Option 2: Mount in Hono app
const app = new Hono();
app.route("/webhooks", handler.getApp());
export default app;

The types for WebhookEventHandler are designed to provide strong type safety, however it gets a littlemore complex when you start adding custom bindings and variables.

If you want to use custom Bindings and Variables, you need to specify the first 4 type parameters, even if you want to use the defaults for some of them. For example:

const handler = new WebhookEventHandler<ApplicationWebhookEventType.ApplicationAuthorized, MyEnv, MyVariables, false>(
ApplicationWebhookEventType.ApplicationAuthorized
);

I’m working on improving the ergonomics of this, but this is a limitation of how TypeScript infers types from constructor arguments. If you have any suggestions on improving this, please let me know!

Ensure:

  • Your URL is publicly accessible
  • DISCORD_PUBLIC_KEY is correctly set
  • Your app responds within 3 seconds
  • You’re returning proper status codes (if using standard mode)

Check:

  • Events are enabled in Developer Portal
  • Specific event types are selected
  • Your endpoint hasn’t been disabled due to too many failures
  • Request signatures are being validated correctly

Events being received multiple times in a short time

Section titled “Events being received multiple times in a short time”

This is likely due to your handler not responding in time. If using standard mode, make sure to return a response within 3 seconds. If processing takes longer and you are on Workers, switch to worker mode to avoid timeouts and retries:

If using standard mode on CF Workers, switch to worker mode:

// Before (may timeout)
const handler = new WebhookEventHandler(ApplicationWebhookEventType.EntitlementCreate);
// After (won't timeout)
const handler = new WebhookEventHandler(
ApplicationWebhookEventType.EntitlementCreate,
true // Worker mode
);