Webhooks
Get signed events when custom emoji, tenants or usage change.
On this page
Webhooks tell your server when something changes in an app, so you can sync your own database, warm a cache or alert someone before a limit is reached.
Events
| Type | When |
|---|---|
custom_emoji.created | A custom emoji was uploaded or imported. |
custom_emoji.deleted | A custom emoji was deleted. |
tenant.created | A tenant was created. |
tenant.deleted | A tenant was deleted. |
usage.threshold | A metric reached 80% or 100% of the plan’s monthly limit. |
Payload
Each delivery is a POST with a JSON body:
{
"id": "…",
"type": "custom_emoji.created",
"createdAt": …,
"appId": "…",
"data": { … }
}Add an endpoint
Add webhook endpoints per app in the dashboard. When you create one, the dashboard shows its signing secret (whsec_…) once. Store it in your server’s secrets. You can send a test event and see every delivery attempt.
Verify the signature
Every request has an Emojisense-Signature header: t=<unix seconds>,v1=<hex>. The v1 value is the HMAC-SHA256 of <t>.<raw body>, keyed with your endpoint’s secret. Check it against the raw body, before you parse the JSON.
import { createHmac, timingSafeEqual } from "node:crypto";
/** Check the Emojisense-Signature header of a webhook request. */
export function verifySignature(
rawBody: string,
header: string,
secret: string,
): boolean {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
const timestamp = Number(parts.t);
if (!parts.v1 || !Number.isFinite(timestamp)) return false;
// Refuse old deliveries, so a captured request cannot be replayed later.
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const given = Buffer.from(parts.v1, "hex");
return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, "hex"));
}Retries
Answer with a 2xx status quickly. If a delivery fails, Emojisense tries up to 3 times in total: at once, after 10 seconds and after 60 seconds. The hosted Workers stop background work 30 seconds after a response, so the third attempt is often cut off: do not rely on it. Each attempt is recorded, so you can see it in the dashboard. Redirects are not followed. Make your handler idempotent with the event id, because an event can arrive more than once.