Skip to main content
Webhooks let your backend react to what happens inside your programmable worlds without polling. When a world finishes generating or fails, Alakazam POSTs a signed JSON event to an HTTPS endpoint you register — so you can update your own database, notify a player, or kick off the next step in your product’s logic. Each delivery is signed with an Alakazam-Signature header you verify against the raw request body, so you can trust that the event came from Alakazam and was not tampered with in flight.

Register an endpoint

Webhook endpoints belong to an app and are managed with your Alakazam user session (the same token you use for app and key management), not an API key. POST the HTTPS url you want events delivered to, and optionally the list of events to subscribe to. Omit events to subscribe to all currently-delivered events (world.generation.succeeded and world.generation.failed).
curl -X POST 'https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_USER_TOKEN' \
  -d '{
    "url": "https://your-app.com/webhooks/alakazam",
    "events": ["world.generation.succeeded", "world.generation.failed"]
  }'
The response includes the endpoint’s secret, prefixed whsec_. Use it to verify every delivery from this endpoint.
{
  "id": "a1b2c3d4-0000-4000-8000-000000000000",
  "url": "https://your-app.com/webhooks/alakazam",
  "events": ["world.generation.succeeded", "world.generation.failed"],
  "enabled": true,
  "created_at": "2026-06-28T12:00:00Z",
  "secret": "whsec_Xa9kQ2mR7v0bN4pL6sD8fG1hJ3wY5z"
}
The signing secret is returned once, at creation. Store it now — it is never shown again. If you lose it, delete the endpoint and register a new one.

List your endpoints

List the webhook endpoints registered for an app. The response is metadata-only — the signing secret is never returned again after creation.
curl 'https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks' \
  -H 'Authorization: Bearer YOUR_USER_TOKEN'
{
  "webhooks": [
    {
      "id": "a1b2c3d4-0000-4000-8000-000000000000",
      "url": "https://your-app.com/webhooks/alakazam",
      "events": ["world.generation.succeeded", "world.generation.failed"],
      "enabled": true,
      "created_at": "2026-06-28T12:00:00Z"
    }
  ]
}

Delete an endpoint

Remove an endpoint to stop deliveries. Either verb works — DELETE, or POST to the /delete sub-path for environments that can’t send DELETE.
curl -X DELETE \
  'https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks/WEBHOOK_ID' \
  -H 'Authorization: Bearer YOUR_USER_TOKEN'
{ "ok": true }

Event types

Subscribe to any subset of these. Omitting events subscribes you to every event Alakazam currently delivers — world.generation.succeeded and world.generation.failed. Subscribe to * to also auto-enroll in new event types as they ship.
EventFires when
world.generation.succeededAn async world generation job finishes and a playable world is ready. data carries worldId and jobId.
world.generation.failedAn async generation job exhausts its retries. data carries jobId and error.
session.endedReserved — not yet delivered. This event type is planned for a future release. You can subscribe to it today, but Alakazam does not currently emit it, so do not build logic that depends on receiving it.
Generation events fire for async jobs — those you create with async: true on POST /v1/worlds and poll at GET /v1/jobs/{jobId}. A webhook lets you skip the polling entirely.

Event payload

Every delivery is a POST with a JSON body in this envelope:
{
  "type": "world.generation.succeeded",
  "created": 1719576000,
  "appId": "11111111-2222-4333-8444-555555555555",
  "data": {
    "worldId": "66666666-7777-4888-8999-aaaaaaaaaaaa",
    "jobId": "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff"
  }
}
FieldDescription
typeThe event type, also mirrored in the Alakazam-Event request header.
createdUnix timestamp (seconds) when the event was emitted.
appIdThe app the event belongs to.
dataEvent-specific payload (see the table above).
Respond with any 2xx status to acknowledge receipt. Any non-2xx, or a network failure, is treated as a failed delivery and retried.

Verify the signature

Every request carries an Alakazam-Signature header:
Alakazam-Signature: t=1719576000,v1=5257a869e7 ... d3f0
  • t is the Unix timestamp (seconds) of the delivery.
  • v1 is the hex HMAC-SHA256 of the string `${t}.${rawBody}` — the timestamp, a literal ., then the exact raw request body — keyed by the endpoint’s whsec_ signing secret.
To verify, recompute the HMAC over the raw body you received and compare it to v1 with a constant-time comparison. Also check that t is recent (within, say, five minutes) to reject replays.
Verify against the raw request bytes, before any JSON parsing or re-serialization. Frameworks that parse and re-stringify the body will change the bytes and break the signature — capture the raw body first.
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.ALAKAZAM_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300;

// Capture the RAW body — verify before parsing.
app.post(
  "/webhooks/alakazam",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const header = req.get("Alakazam-Signature") || "";
    const parts = Object.fromEntries(
      header.split(",").map((kv) => kv.split("=")),
    );
    const t = parts.t;
    const rawBody = req.body.toString("utf8");

    const expected = crypto
      .createHmac("sha256", SECRET)
      .update(`${t}.${rawBody}`)
      .digest("hex");

    // Compare as raw bytes. timingSafeEqual THROWS on length mismatch,
    // so guard equal length first — a malformed v1 must be rejected, not 500.
    const received = Buffer.from(parts.v1 || "", "hex");
    const digest = Buffer.from(expected, "hex");
    const signatureOk =
      received.length === digest.length &&
      crypto.timingSafeEqual(received, digest);

    const ok =
      signatureOk &&
      Math.abs(Date.now() / 1000 - Number(t)) < TOLERANCE_SECONDS;

    if (!ok) return res.status(400).send("bad signature");

    const event = JSON.parse(rawBody);
    console.log("verified", event.type, event.data);
    res.json({ received: true });
  },
);

Retries and ordering

Each event is attempted up to 3 times with exponential backoff per endpoint. A delivery succeeds on any 2xx; anything else is retried until the attempts are exhausted.
  • No ordering guarantee. Events may arrive out of order, and an occasional duplicate is possible. Make your handler idempotent — key off the event’s data ids (for example jobId or worldId) rather than assuming arrival order.
  • Acknowledge fast. Return 2xx as soon as you’ve verified and stored the event, then do slower work asynchronously, so a slow handler doesn’t trigger retries.
  • Per-endpoint signing. Each endpoint has its own whsec_ secret, so verify with the secret for the endpoint that received the request.

Where to get keys

Register endpoints and manage your apps and API keys from the developer dashboard.