Skip to article
Guides / Webhooks

Webhooks

Get signed events when custom emoji, tenants or usage change.

  • Plans Scale
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

TypeWhen
custom_emoji.createdA custom emoji was uploaded or imported.
custom_emoji.deletedA custom emoji was deleted.
tenant.createdA tenant was created.
tenant.deletedA tenant was deleted.
usage.thresholdA metric reached 80% or 100% of the plan’s monthly limit.

Payload

Each delivery is a POST with a JSON body:

JSON
{
  "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.

verify.ts
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.