Webhooks
Approved backend and hybrid apps receive signed HTTPS event deliveries. Webhooks are only available once an admin enables backend and webhook access — client-only mods receive events over the plugin channel instead.
Endpoint lifecycle
Add an endpoint with an https URL and the events you want.
It starts as pending verification — verify it to become active and begin receiving deliveries.
Rotate the signing secret any time; the previous secret stops working.
Disable to pause delivery, or revoke to stop it permanently.
Each endpoint gets its own signing secret, shown once on creation and once on each rotation. Store it securely — it can't be retrieved again.
What a delivery looks like
Each delivery is an HTTPS POST. The JSON body is the same event envelope documented in Hooks — read id, type, version, and the event-specific data. Every delivery carries these headers:
Cosmic-Event-Id: evt_01J... Cosmic-Event-Type: player.enchant_proc Cosmic-Event-Version: 1 Cosmic-Timestamp: 1780359300000 Cosmic-Signature: <hmac-sha256-hex> Content-Type: application/json
Verifying signatures
Each delivery is signed with your endpoint's signing secret. Recompute the signature from the timestamp and the raw request body, then compare it in constant time before trusting a payload.
Signature input: timestamp + "." + raw_body Algorithm: HMAC-SHA256(endpoint_signing_secret, signature_input) Delivery: at least once Dedupe by: event id
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody must be the exact bytes you received, before JSON parsing.
function verifyDelivery(headers, rawBody, signingSecret) {
const timestamp = headers["cosmic-timestamp"];
const signature = headers["cosmic-signature"];
const expected = createHmac("sha256", signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Harden against replay
Compare signatures in constant time, and reject deliveries whose Cosmic-Timestamp is too old. Always verify against the raw body bytes — not a re-serialized object.
Retries and idempotency
Treat deliveries as at-least-once and acknowledge fast:
Respond with a 2xx as soon as you've stored the event — do slow work afterward.
Non-2xx responses are retried with backoff and show up in your deliveries log.
The same event can arrive more than once, so dedupe by the event id.
Branch on the event version so a payload change doesn't break your handler.
