Skip to main content

Webhooks

Webhooks push Cortex domain events to an HTTP endpoint you host. Delivery is reliable: events are persisted before they’re sent, retried with backoff if your endpoint is down, and signed so you can verify they really came from Cortex.

Creating a subscription

Subscribe to the events you care about (exact names, domain.* wildcards, or * for everything) and provide a secret used to sign deliveries:

import { randomBytes } from "node:crypto";

// Pick any random string and store it with your receiver — it verifies signatures.
// This is NOT your 4Players project secret; it only protects Cortex → your URL.
const webhookSecret = randomBytes(32).toString("hex");

const sub = await project.webhooks.createSubscription({
url: "https://example.com/cortex-webhook",
events: ["session.created", "session.updated", "message.created"],
secret: webhookSecret,
});

You can also configure how Cortex authenticates to your endpoint — authType of none (default), api_key, basic_auth or custom_headers — on top of the signature.

Verifying signatures

Every delivery is signed with HMAC-SHA256 using your subscription secret. Verify the signature on the raw request body before trusting the payload:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, timestamp: string, signature: string, secret: string) {
const expected = createHmac("sha256", secret)
.update(timestamp + rawBody)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Always compare with a constant-time function like timingSafeEqual, never ===.

Reliable delivery (the outbox)

Cortex writes each matching event to a durable outbox before delivering it, so an endpoint that’s temporarily down never loses events:

  • On 2xx, the delivery is marked delivered.
  • On error or non-2xx, it’s retried with exponential backoff until it succeeds or the attempt limit is reached.
  • Each attempt is recorded so you can audit delivery history.
Make your receiver idempotent

Because failed deliveries are retried, your endpoint may receive the same event more than once. Deduplicate by the event id and make handling idempotent.

Inspecting deliveries

List the delivery queue (pending / delivered / failed attempts) to debug integration issues:

const events = await project.webhooks.listEvents();

Webhooks vs SSE vs functions

All three are driven by the same domain-event catalog:

SurfaceBest forDelivery
WebhooksServer-to-server integrations with your backendDurable, retried, signed
SSELive dashboards / UIReal-time stream, no replay
FunctionsCustom logic you don’t want to hostDurable (same outbox path)