Skip to main content
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.

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.
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: "probability_spike",
  description: "Election probability alerts",
  secret: "whsec_abc123...",
  filters: {
    event_slugs: ["us-election-2028"],
    min_probability_change_pct: 5,
  },
});

console.log(webhook.id);
The secret you provide (or one generated via rotateSecret) is returned once and never echoed back. Store it somewhere your handler can reach.

List webhooks

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:
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

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.
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.
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.
const { data } = await client.webhooks.rotateSecret({
  webhookId: "whk_abc123",
});

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

Delete a webhook

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.
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:
HeaderValue
X-Webhook-IDUUID of the webhook subscription that fired.
X-Webhook-Signaturesha256=<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.
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();
  },
);
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.

Handling deliveries

Every delivery arrives as a JSON WebhookDeliveryEnvelope wrapping the event-specific payload:
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.
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 "probability_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):
FieldTypeDescription
idstring (UUID)This delivery attempt. Matches the X-Delivery-ID header.
webhook_idstring (UUID)The subscription that fired. Matches X-Webhook-ID.
eventPolymarketWebhookEventDiscriminant. Test deliveries append _test (for example, "probability_spike_test").
dataevent-specificPayload shape varies per event. Narrowed automatically by the switch.
timestampnumberUnix milliseconds when the delivery was created.
attemptnumber1 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:
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:
import type { WebhookEventPayloadMap } from "@structbuild/sdk";

type SpikePayload = WebhookEventPayloadMap["probability_spike"];

Dispatching with a handler map

For larger codebases, a record of per-event handlers keeps each one typed without a giant switch:
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);
  },
  probability_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.
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 for the full retry policy and payload envelope.
Last modified on April 14, 2026