> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alakazam.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Get server-side notifications when your programmable worlds generate and play.

Webhooks let your backend react to what happens inside your **programmable
worlds** without polling. When a world finishes generating or fails, Alakazam
`POST`s 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`).

<CodeGroup>
  ```bash cURL theme={null}
  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"]
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    "https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks",
    {
      method: "POST",
      headers: {
        "Authorization": "Bearer YOUR_USER_TOKEN",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        url: "https://your-app.com/webhooks/alakazam",
        events: ["world.generation.succeeded", "world.generation.failed"],
      }),
    },
  );

  const webhook = await response.json();
  console.log(webhook);
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks",
      headers={
          "Authorization": "Bearer YOUR_USER_TOKEN",
          "Content-Type": "application/json",
      },
      json={
          "url": "https://your-app.com/webhooks/alakazam",
          "events": ["world.generation.succeeded", "world.generation.failed"],
      },
  )

  print(response.json())
  ```
</CodeGroup>

The response includes the endpoint's `secret`, prefixed `whsec_`. Use it to
verify every delivery from this endpoint.

```json theme={null}
{
  "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"
}
```

<Warning>
  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.
</Warning>

## 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.

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks' \
    -H 'Authorization: Bearer YOUR_USER_TOKEN'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    "https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks",
    { headers: { "Authorization": "Bearer YOUR_USER_TOKEN" } },
  );

  const { webhooks } = await response.json();
  console.log(webhooks);
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      "https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks",
      headers={"Authorization": "Bearer YOUR_USER_TOKEN"},
  )

  print(response.json()["webhooks"])
  ```
</CodeGroup>

```json theme={null}
{
  "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`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE \
    'https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks/WEBHOOK_ID' \
    -H 'Authorization: Bearer YOUR_USER_TOKEN'
  ```

  ```javascript JavaScript theme={null}
  await fetch(
    "https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks/WEBHOOK_ID",
    { method: "DELETE", headers: { "Authorization": "Bearer YOUR_USER_TOKEN" } },
  );
  ```

  ```python Python theme={null}
  import requests

  requests.delete(
      "https://api.alakazam.gg/v1/apps/YOUR_APP_ID/webhooks/WEBHOOK_ID",
      headers={"Authorization": "Bearer YOUR_USER_TOKEN"},
  )
  ```
</CodeGroup>

```json theme={null}
{ "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.

| Event                        | Fires when                                                                                                                                                                                                     |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `world.generation.succeeded` | An async world generation job finishes and a playable world is ready. `data` carries `worldId` and `jobId`.                                                                                                    |
| `world.generation.failed`    | An async generation job exhausts its retries. `data` carries `jobId` and `error`.                                                                                                                              |
| `session.ended`              | **Reserved — 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. |

<Note>
  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.
</Note>

## Event payload

Every delivery is a `POST` with a JSON body in this envelope:

```json theme={null}
{
  "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"
  }
}
```

| Field     | Description                                                           |
| --------- | --------------------------------------------------------------------- |
| `type`    | The event type, also mirrored in the `Alakazam-Event` request header. |
| `created` | Unix timestamp (seconds) when the event was emitted.                  |
| `appId`   | The app the event belongs to.                                         |
| `data`    | Event-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.

<Warning>
  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.
</Warning>

<CodeGroup>
  ```javascript Node theme={null}
  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 });
    },
  );
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  import time
  from flask import Flask, request, abort

  app = Flask(__name__)
  SECRET = "whsec_…"
  TOLERANCE_SECONDS = 300


  @app.post("/webhooks/alakazam")
  def alakazam_webhook():
      header = request.headers.get("Alakazam-Signature", "")
      parts = dict(kv.split("=", 1) for kv in header.split(","))
      t = parts.get("t", "")
      raw_body = request.get_data(as_text=True)  # raw bytes, before parsing

      expected = hmac.new(
          SECRET.encode(),
          f"{t}.{raw_body}".encode(),
          hashlib.sha256,
      ).hexdigest()

      fresh = abs(time.time() - int(t)) < TOLERANCE_SECONDS
      if not (hmac.compare_digest(parts.get("v1", ""), expected) and fresh):
          abort(400, "bad signature")

      event = request.get_json()
      print("verified", event["type"], event["data"])
      return {"received": True}
  ```
</CodeGroup>

## Retries and ordering

<Note>
  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.
</Note>

* **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](https://play.alakazam.gg/?view=developer).
