# Guide to Signal Sciences Webhooks: Features and Best Practices

Signal Sciences, now Fastly Next-Gen WAF, can notify your systems when something happens in a protected workspace: an IP address is flagged for attacking your site, an agent raises an alert, or someone changes the agent mode, a rule or a list. Its generic webhook integration is how you get those notifications into your own tooling, such as an incident channel, a SIEM or an audit log, rather than relying on someone watching the dashboard.

This guide covers how to set up the generic webhook in both of Fastly's control panels, how to verify the `X-SigSci-Signature` header, the activity types you can subscribe to, and what Fastly leaves undocumented.

## What are Signal Sciences webhooks?

Signal Sciences is now Fastly Next-Gen WAF, but the webhook still carries the old name: the signature header is `X-SigSci-Signature`. Hookdeck's source type is named Signal Sciences for the same reason.

The generic webhook is an alert integration configured per workspace, which the older control panel calls a site. When a selected activity happens, Next-Gen WAF sends a JSON notification to your URL, signed with a key generated when you created the integration.

What you can subscribe to depends on which control panel you use. In the Fastly control panel, the alert form offers Flagged IPs. In the legacy Next-Gen WAF control panel, you can choose all activity or pick from 32 activity types.

## Signal Sciences webhook features

| Feature | Details |
| --- | --- |
| Configuration | Per workspace, in the Fastly control panel or the legacy Next-Gen WAF control panel |
| Subscription | Flagged IPs (Fastly control panel), or all activity or chosen activity types (legacy panel) |
| Signature header | `X-SigSci-Signature` |
| Scheme | HMAC-SHA256, hex digest, no prefix |
| Signed content | The JSON payload |
| Secret | A signing key per integration, generated on creation |
| Key rotation | **Rotate** replaces the key straight away |
| Activity identifier | `type` in the body |
| Retries and timeout | Not documented |
| Source IPs | Not published |

## Common events

The `type` field names the activity. These are the activity types the legacy control panel lets you choose:

| Area | Activity types | Fires when |
| --- | --- | --- |
| Attacks | `flag`, `expireFlag` | An IP address was flagged, or a flag was manually expired |
| Agents | `agentAlert`, `loggingModeChanged`, `agentAnonModeChanged` | An agent alert was triggered, or the agent mode (Blocking, Not Blocking, Off) or IP anonymisation mode changed |
| Workspace | `siteDisplayNameChanged`, `siteNameChanged` | The workspace's display name or short name changed |
| Rules | `ruleCreated`, `ruleUpdated`, `ruleDeleted`, `detectionCreated`, `detectionUpdated`, `detectionDeleted` | A request rule or templated rule was created, updated or removed |
| Lists and signals | `listCreated`, `listUpdated`, `listDeleted`, `customTagCreated`, `customTagUpdated`, `customTagDeleted` | A list or custom signal was created, updated or removed |
| Alerts and redactions | `customAlertCreated`, `customAlertUpdated`, `customAlertDeleted`, `createCustomRedaction`, `updateCustomRedaction`, `removeCustomRedaction` | A custom alert or custom redaction changed |
| Dashboards | `customDashboard*` and `customDashboardWidget*` | A custom dashboard or one of its cards was created, updated, reset or removed |

Fastly documents one notification example:

```json
{
  "created": "2024-02-09T21:08:21.31644377Z",
  "type": "flag",
  "payload": {
    "ID": "abcde123456789",
    "CorpID": "123456789abcde",
    "SiteID": "987654321fedcba",
    "Scope": "site",
    "Slug": "site.example",
    "Name": "example",
    "Description": "Webhook integration for Example site",
    "CreatedBy": "admin@example.com",
    "CreatedByID": "edcba987654321",
    "Created": "2024-02-09T21:08:21.31644377Z",
    "Updated": "2025-01-07T21:06:31.50725289Z",
    "ResourceID": "ASDFghjkl123456789"
  },
  "link": "dashboard link to event",
  "workspaceId": "www.mysite.com"
}

```

`payload` holds the object the activity concerns, and its shape varies by type. Fastly's activity table points to the matching Next-Gen WAF API response for some of them: "Get event by ID" for `flag`, "Get site by name" for `loggingModeChanged`, and the create, update or remove response for lists, custom alerts and redactions. Many types, including `agentAlert` and the rule types, have no payload reference at all. The example above looks more like an integration record than a flagged-IP event, so don't build a parser from it alone. Capture real notifications first.

## Setting up Signal Sciences webhooks

In the Fastly control panel:

1. Go to **Security > Next-Gen WAF > Workspaces**.
2. Click the gear next to the workspace, then **Alerts** and **Add alert**.
3. Set **Integration type** to **Webhook**, enter your **Webhook URL**, and leave **Flagged IPs** checked under **Select activities**.
4. Click **Add workspace alert**.

In the legacy Next-Gen WAF control panel:

1. Select the site, then go to **Manage > Site Integrations** and click **Add site integration**.
2. Choose **Generic Webhook** and enter your **Webhook URL**.
3. Choose **All activity**, or **Specific activity** and the activity types you want.
4. Click **Create site integration**.

To get the signing key, edit the alert or integration and click **Reveal**. Store it server-side, for example as `SIGSCI_SIGNING_KEY`. Fastly documents no validation or test request, so the first notification you see is a real one.

## Securing Signal Sciences webhooks

Every request includes `X-SigSci-Signature`, the hex HMAC-SHA256 of the payload, keyed with the integration's signing key used as a plain string. Compute the same digest over the raw request body and compare it with the header in constant time.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifySigSciSignature(rawBody, signatureHeader, signingKey) {
  if (!signatureHeader || !signingKey) return false;

  const expected = crypto.createHmac('sha256', signingKey).update(rawBody).digest('hex');

  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/sigsci', express.raw({ type: '*/*' }), (req, res) => {
  const key = process.env.SIGSCI_SIGNING_KEY;
  if (!key) return res.sendStatus(500); // fail closed

  if (!verifySigSciSignature(req.body, req.get('x-sigsci-signature'), key)) {
    return res.sendStatus(401);
  }

  const notification = JSON.parse(req.body.toString('utf8'));
  // Route on notification.type, e.g. 'flag' or 'agentAlert'.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_sigsci_signature(raw_body: bytes, signature_header, signing_key: str) -> bool:
    if not signature_header or not signing_key:
        return False
    expected = hmac.new(signing_key.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header.strip().lower().encode("utf-8"), expected.encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_sigsci_signature(raw_body, request.headers.get("x-sigsci-signature"),
#                         os.environ["SIGSCI_SIGNING_KEY"])

```

Fastly publishes samples in Go, Python, Ruby and Bash. The Go, Python and Ruby ones match the code above, though the Python sample compares with `==` rather than `hmac.compare_digest`. The Bash sample compares the raw output of `openssl dgst`, which includes a label such as `(stdin)= ` before the digest, so it never matches as written.

> **Make Signal Sciences webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies `X-SigSci-Signature` on every Next-Gen WAF notification and durably queues flagged-IP and agent alerts before they reach your incident tooling.

## Signal Sciences webhook limitations and pain points

### Delivery behaviour is undocumented

**The Problem:** You cannot tell whether a notification that hit a failing endpoint will ever be sent again.

**Why It Happens:** Fastly's generic webhook page describes setup, format and signing, and nothing about retries, timeouts, the status code it expects, or ordering.

**Workarounds:**

* Return `200` quickly and assume a failed notification is not retried.
* For anything you must not miss, such as flagged IPs, also poll the Next-Gen WAF API on a schedule.

**How Hookdeck Can Help:** Hookdeck accepts each notification as it arrives and stores it, then delivers to your service with [retries](/docs/retries) you control and replay for anything that fails, so an outage on your side does not depend on Fastly's unknown retry behaviour.

### Payloads vary and are thinly documented

**The Problem:** Code written against the documented example breaks on real notifications.

**Why It Happens:** `payload` changes shape with `type`, Fastly references an API response for only some types, and its single example has a placeholder `link`.

**Workarounds:**

* Route on `type` and treat `payload` as untyped until you have captured real examples of each type you use.
* Use `link` and `workspaceId` for display and routing rather than deep fields inside `payload`.

**How Hookdeck Can Help:** Every notification is logged in Hookdeck with its headers and body, so you can capture real payloads for each activity type and [replay](/docs/use-cases/receive-webhooks#replay-for-debugging-and-recovery) them against your parser as you build it.

### Key rotation has no overlap

**The Problem:** Verification fails for every notification as soon as someone clicks **Rotate**.

**Why It Happens:** Rotation takes effect immediately, and Fastly documents no grace period or second signature.

**Workarounds:**

* Rotate during a quiet period, and update your stored key straight away.
* Let your receiver accept either of two configured keys while you switch.

**How Hookdeck Can Help:** With Hookdeck in front, the signing key lives on the Hookdeck source. Rotate in Fastly, paste the new key into the source, and your services, which only trust Hookdeck, need no change.

### No timestamp or event ID

**The Problem:** A captured notification can be replayed, and duplicates cannot be detected from the headers.

**Why It Happens:** Only the payload is signed, and Fastly sends no timestamp or delivery ID.

**Workarounds:**

* Build a key from `type`, `created` and an identifier inside `payload`, and skip repeats.
* Keep the endpoint on HTTPS.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated notifications based on the fields you choose.

## Best practices

### Verify against the raw body

Sign and compare the exact bytes received. In Express, use `express.raw()` on the route; in FastAPI, `await request.body()`. Re-serialised JSON will not match.

### Separate security alerts from configuration noise

`flag` and `agentAlert` usually need a fast response, while dashboard and rule changes belong in an audit log. Subscribe only to the activity types you act on, or route them to different destinations.

### Treat the endpoint as part of your security tooling

A forged `flag` notification could trigger automated blocking or page someone at night. Fail closed when the key is missing, and keep the signing key out of logs.

### Acknowledge fast and process asynchronously

Fastly doesn't document its timeout, so don't do slow work in the request. Verify, store, return `200`, then notify or enrich in the background. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Signal Sciences generic webhooks, now part of Fastly Next-Gen WAF, sign each notification with a hex HMAC-SHA256 in `X-SigSci-Signature`. Verify against the raw body, route on `type`, treat `payload` as variable, and plan for undocumented delivery behaviour by acknowledging quickly and reconciling important activity through the API.

[Hookdeck Event Gateway](https://hookdeck.com) verifies every `X-SigSci-Signature`, keeps a searchable log of notifications, and holds each one until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Signal Sciences webhooks reliably in minutes.