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

# Managing Webhooks

> Create, update, test, rotate, and delete webhook subscriptions from the SDK.

The `client.webhooks` namespace wraps the webhook management API. Use it to register endpoints, adjust filters, rotate signing secrets, and verify deliveries in your handler. For the full event catalogue and payload shapes, see the [Webhooks tab](/webhooks/getting-started).

## Create a webhook

Each webhook subscribes to a single event and delivers to one HTTPS endpoint. Pass optional filters to narrow the deliveries and a `secret` to enable signature verification.

```typescript theme={null}
import { StructClient } from "@structbuild/sdk";

const client = new StructClient({ apiKey: "sk_live_xxx" });

const { data: webhook } = await client.webhooks.create({
  url: "https://your-server.com/webhooks/struct",
  event: "price_spike",
  description: "Election price alerts",
  secret: "whsec_abc123...",
  filters: {
    event_slugs: ["us-election-2028"],
    min_probability_change_pct: 5,
  },
});

console.log(webhook.id);
```

<Warning>The `secret` you provide (or one generated via `rotateSecret`) is returned once and never echoed back. Store it somewhere your handler can reach.</Warning>

## List webhooks

```typescript theme={null}
const { data: page } = await client.webhooks.list({ limit: 50 });

for (const hook of page.webhooks) {
  console.log(hook.id, hook.event, hook.status);
}
```

`list` supports `limit`, `offset`, and filtering by `event` or `status`. Pagination is offset-based, so walk pages by incrementing `offset`:

```typescript theme={null}
const pageSize = 50;
let offset = 0;

while (true) {
  const { data: page } = await client.webhooks.list({ limit: pageSize, offset });
  for (const hook of page.webhooks) {
    console.log(hook.id, hook.url);
  }
  if (page.webhooks.length < pageSize) break;
  offset += pageSize;
}
```

## Fetch one webhook

```typescript theme={null}
const { data: hook } = await client.webhooks.getWebhook({
  webhookId: "whk_abc123",
});
```

## Update a webhook

`update` is a partial update. Pass only the fields you want to change. You can pause or resume a webhook by setting `status`.

```typescript theme={null}
await client.webhooks.update({
  webhookId: "whk_abc123",
  filters: {
    event_slugs: ["us-election-2028", "uk-election-2029"],
    min_probability_change_pct: 10,
  },
});

await client.webhooks.update({
  webhookId: "whk_abc123",
  status: "paused",
});
```

## Test a webhook

`test` delivers a synthetic payload to the configured URL so you can confirm your handler is reachable and verifies signatures correctly. Use it any time you change the URL or the signing secret.

```typescript theme={null}
const { data: result } = await client.webhooks.test({
  webhookId: "whk_abc123",
});

console.log(result.status_code, result.duration_ms);
```

## Rotate the signing secret

`rotateSecret` issues a new HMAC secret and invalidates the previous one immediately. The new value is returned once.

```typescript theme={null}
const { data } = await client.webhooks.rotateSecret({
  webhookId: "whk_abc123",
});

await secrets.save("struct.webhook.signing_key", data.secret);
```

## Delete a webhook

```typescript theme={null}
await client.webhooks.deleteWebhook({ webhookId: "whk_abc123" });
```

## Discover available events

`listEvents` returns every event type you can subscribe to, along with the filter keys each one accepts. This is the source of truth for what to pass as `event` and `filters`.

```typescript theme={null}
const { data } = await client.webhooks.listEvents();

for (const info of data.events) {
  console.log(info.event, info.applicable_filters);
}
```

## Verifying deliveries

Every delivery includes two headers your handler should inspect:

| Header                | Value                                                                                                                              |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `X-Webhook-ID`        | UUID of the webhook subscription that fired.                                                                                       |
| `X-Webhook-Signature` | `sha256=<hex>`, an HMAC-SHA256 of the **raw request body** using the webhook's secret. Only present when a `secret` is configured. |

Recompute the HMAC over the raw bytes of the request body and compare with a constant-time check. Parsing the body to JSON before hashing will break the comparison.

<CodeGroup>
  ```typescript Node (Express) theme={null}
  import crypto from "node:crypto";
  import express from "express";
  import type { WebhookEvent } from "@structbuild/sdk";

  const app = express();

  app.post(
    "/webhooks/struct",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const header = req.header("x-webhook-signature") ?? "";
      const [, signature] = header.split("=");

      const expected = crypto
        .createHmac("sha256", process.env.STRUCT_WEBHOOK_SECRET!)
        .update(req.body)
        .digest("hex");

      const valid = signature
        && signature.length === expected.length
        && crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expected),
        );

      if (!valid) return res.status(401).end();

      const delivery: WebhookEvent = JSON.parse(req.body.toString("utf8"));
      handle(delivery);

      res.status(200).end();
    },
  );
  ```

  ```typescript Next.js Route Handler theme={null}
  import crypto from "node:crypto";
  import type { WebhookEvent } from "@structbuild/sdk";

  export async function POST(req: Request) {
    const raw = Buffer.from(await req.arrayBuffer());
    const header = req.headers.get("x-webhook-signature") ?? "";
    const [, signature] = header.split("=");

    const expected = crypto
      .createHmac("sha256", process.env.STRUCT_WEBHOOK_SECRET!)
      .update(raw)
      .digest("hex");

    const valid = signature
      && signature.length === expected.length
      && crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expected),
      );

    if (!valid) return new Response(null, { status: 401 });

    const delivery: WebhookEvent = JSON.parse(raw.toString("utf8"));
    await handle(delivery);

    return new Response(null, { status: 200 });
  }
  ```

  ```typescript Bun / Cloudflare Workers theme={null}
  import type { WebhookEvent } from "@structbuild/sdk";

  async function verify(secret: string, body: ArrayBuffer, header: string) {
    const [, provided] = header.split("=");
    if (!provided) return false;

    const key = await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(secret),
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign"],
    );

    const mac = await crypto.subtle.sign("HMAC", key, body);
    const expected = Array.from(new Uint8Array(mac))
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("");

    if (provided.length !== expected.length) return false;
    let diff = 0;
    for (let i = 0; i < expected.length; i++) {
      diff |= provided.charCodeAt(i) ^ expected.charCodeAt(i);
    }
    return diff === 0;
  }

  export default {
    async fetch(req: Request, env: { STRUCT_WEBHOOK_SECRET: string }) {
      const raw = await req.arrayBuffer();
      const header = req.headers.get("x-webhook-signature") ?? "";

      if (!(await verify(env.STRUCT_WEBHOOK_SECRET, raw, header))) {
        return new Response(null, { status: 401 });
      }

      const delivery: WebhookEvent = JSON.parse(new TextDecoder().decode(raw));
      await handle(delivery);

      return new Response(null, { status: 200 });
    },
  };
  ```
</CodeGroup>

<Warning>Always hash the raw body bytes, not the parsed JSON. Middleware that parses JSON before your handler runs (for example, Express's default `body-parser`) will make verification fail. Mount `express.raw` on the webhook route as shown above.</Warning>

## Handling deliveries

Every delivery arrives as a JSON `WebhookDeliveryEnvelope` wrapping the event-specific payload:

```typescript theme={null}
import type { WebhookEvent } from "@structbuild/sdk";
```

`WebhookEvent` is a discriminated union over every supported event, keyed on the `event` field. Each variant narrows `data` to the matching payload shape, so a single `switch` gives you full type safety across every event type you subscribe to.

```typescript theme={null}
import type { WebhookEvent } from "@structbuild/sdk";

function handle(delivery: WebhookEvent) {
  switch (delivery.event) {
    case "trader_whale_trade":
      console.log(delivery.data.trader, delivery.data.amount_usd);
      break;

    case "price_spike":
      console.log(
        delivery.data.condition_id,
        delivery.data.spike_pct,
      );
      break;

    case "market_created":
      console.log(delivery.data.condition_id, delivery.data.market_slug);
      break;

    case "trader_global_pnl":
      console.log(delivery.data.trader, delivery.data.realized_pnl_usd);
      break;

    case "close_to_bond":
      console.log(delivery.data.condition_id, delivery.data.price);
      break;
  }
}
```

### Envelope

The fields on every delivery (before narrowing on `event`):

| Field        | Type                     | Description                                                                       |
| ------------ | ------------------------ | --------------------------------------------------------------------------------- |
| `id`         | `string` (UUID)          | This delivery attempt. Matches the `X-Delivery-ID` header.                        |
| `webhook_id` | `string` (UUID)          | The subscription that fired. Matches `X-Webhook-ID`.                              |
| `event`      | `PolymarketWebhookEvent` | Discriminant. Test deliveries append `_test` (for example, `"price_spike_test"`). |
| `data`       | event-specific           | Payload shape varies per event. Narrowed automatically by the `switch`.           |
| `timestamp`  | `number`                 | Unix milliseconds when the delivery was created.                                  |
| `attempt`    | `number`                 | `1` on first try, increments on each retry.                                       |

### Per-event payload types

Each payload type is exported directly for cases where you want a narrow parameter type without a `switch`:

```typescript theme={null}
import type {
  WhaleTradePayload,
  ProbabilitySpikePayload,
  MarketCreatedPayload,
  FirstTradePayload,
  NewTradePayload,
  GlobalPnlPayload,
  MarketPnlPayload,
  EventPnlPayload,
  ConditionMetricsPayload,
  EventMetricsPayload,
  PositionMetricsPayload,
  VolumeMilestonePayload,
  EventVolumeMilestonePayload,
  PositionVolumeMilestonePayload,
  MarketVolumeSpikePayload,
  EventVolumeSpikePayload,
  PositionVolumeSpikePayload,
  PriceSpikePayload,
  CloseToBondPayload,
  AssetPriceTickPayload,
  AssetPriceWindowUpdatePayload,
  NewMarketPayload,
  WebhookTraderTradeEventPayload,
} from "@structbuild/sdk";

function alertBigTrader(trade: WhaleTradePayload) {
  if (trade.amount_usd > 100_000) notify(trade.trader, trade.amount_usd);
}
```

You can also map an event name to its payload with `WebhookEventPayloadMap`:

```typescript theme={null}
import type { WebhookEventPayloadMap } from "@structbuild/sdk";

type SpikePayload = WebhookEventPayloadMap["price_spike"];
```

### Dispatching with a handler map

For larger codebases, a record of per-event handlers keeps each one typed without a giant `switch`:

```typescript theme={null}
import type {
  WebhookEvent,
  WebhookEventPayloadMap,
  PolymarketWebhookEvent,
} from "@structbuild/sdk";

type Handlers = {
  [E in PolymarketWebhookEvent]?: (
    data: WebhookEventPayloadMap[E],
    delivery: Extract<WebhookEvent, { event: E }>,
  ) => Promise<void> | void;
};

const handlers: Handlers = {
  trader_whale_trade: async (data) => {
    await notify(data.trader, data.amount_usd);
  },
  price_spike: async (data) => {
    await pushAlert(data.condition_id, data.spike_pct);
  },
  market_created: async (data) => {
    await indexMarket(data.condition_id);
  },
};

async function dispatch(delivery: WebhookEvent) {
  const fn = handlers[delivery.event];
  if (!fn) return;
  await (fn as (data: unknown, delivery: WebhookEvent) => Promise<void>)(
    delivery.data,
    delivery,
  );
}
```

### Ignoring test deliveries

Test deliveries carry the same payload shape but append `_test` to the event name. Drop the suffix when you want to route a test through the same handlers as the real event, or reject them outright when running in production.

```typescript theme={null}
function normaliseEvent(raw: string) {
  return raw.endsWith("_test") ? raw.slice(0, -"_test".length) : raw;
}
```

## Responding to deliveries

Return any 2xx status within 10 seconds to acknowledge a delivery. Non-2xx responses and timeouts trigger retries with exponential backoff. See [Webhook Response Format](/webhooks/response-format) for the full retry policy and payload envelope.
