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

# Webhooks

> Subscribe to pentest and vulnerability lifecycle events

export const ScarfPixel = () => <img referrerPolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=831669b6-b4bd-42cb-8832-2122c03e1d5f" alt="" width="1" height="1" style={{
  position: "absolute",
  width: 0,
  height: 0,
  opacity: 0,
  pointerEvents: "none"
}} />;

<ScarfPixel />

## Overview

Webhooks send events to your endpoint when the events occur. Use webhooks to start remediation workflows, update dashboards, or send alerts to your team.

## Events

| Event | Description |
| - | - |
| `scan.created` | A new pentest started |
| `scan.completed` | A pentest completed successfully |
| `scan.failed` | A pentest failed during execution |
| `scan.cancelled` | A pentest was cancelled |
| `vulnerability.created` | A new vulnerability was found |
| `vulnerability.status_changed` | The status of a vulnerability changed |
| `vulnerability.severity_changed` | The severity of a vulnerability changed, with an override reason |
| `*` | Subscribe to all events |

## Payload format

Every delivery is a `POST` with a stable JSON envelope. The `data` object is different for each event. The sections below give the payload for each event.

| Field | Type | Description |
| - | - | - |
| `id` | uuid | Unique delivery id. Also sent as `X-Strix-Delivery`. |
| `event` | string | Webhook event type, such as `scan.completed`. |
| `created_at` | ISO 8601 timestamp | Time the delivery was created. |
| `data` | object | Event-specific payload. |

### scan.created

A pentest was created and queued.

| Field | Type | Description |
| - | - | - |
| `scan_id` | string | Scan id |
| `organization_id` | string | Organization that owns the scan |
| `status` | string | Current scan status |
| `title` | string \| null | Scan title |
| `created_at` | ISO 8601 timestamp | Scan creation time |

```json theme={null}
{
  "id": "delivery_123",
  "event": "scan.created",
  "created_at": "2026-05-19T04:30:00.000Z",
  "data": {
    "scan_id": "scan_123",
    "organization_id": "org_123",
    "status": "pending",
    "title": "Production API pentest",
    "created_at": "2026-05-19T04:30:00.000Z"
  }
}
```

### scan.completed, scan.failed, scan.cancelled

The status of a pentest changed. The three events have the same payload shape. The `status` field is `completed`, `failed`, or `cancelled`.

| Field | Type | Description |
| - | - | - |
| `scan_id` | string | Scan id |
| `organization_id` | string | Organization that owns the scan |
| `status` | string | New scan status |
| `previous_status` | string \| null | Previous scan status |
| `title` | string \| null | Scan title |
| `schedule_id` | string \| null | Schedule that launched the scan, when applicable |
| `updated_at` | ISO 8601 timestamp | Time the status change was observed |

```json theme={null}
{
  "id": "delivery_123",
  "event": "scan.completed",
  "created_at": "2026-05-19T05:12:00.000Z",
  "data": {
    "scan_id": "scan_123",
    "organization_id": "org_123",
    "status": "completed",
    "previous_status": "running",
    "title": "Production API pentest",
    "schedule_id": "schedule_123",
    "updated_at": "2026-05-19T05:12:00.000Z"
  }
}
```

### vulnerability.created

A vulnerability was created for a pentest.

| Field | Type | Description |
| - | - | - |
| `vulnerability_id` | string | Vulnerability id |
| `organization_id` | string | Organization that owns the vulnerability |
| `scan_id` | string | Scan that found the vulnerability |
| `title` | string | Vulnerability title |
| `severity` | `critical` \| `high` \| `medium` \| `low` | Current severity |
| `status` | string | Current vulnerability status |
| `created_at` | ISO 8601 timestamp | Vulnerability creation time |

```json theme={null}
{
  "id": "delivery_123",
  "event": "vulnerability.created",
  "created_at": "2026-05-19T05:08:00.000Z",
  "data": {
    "vulnerability_id": "vuln_123",
    "organization_id": "org_123",
    "scan_id": "scan_123",
    "title": "Unauthenticated account export",
    "severity": "high",
    "status": "open",
    "created_at": "2026-05-19T05:08:00.000Z"
  }
}
```

### vulnerability.status\_changed

The status of a vulnerability changed.

| Field | Type | Description |
| - | - | - |
| `vulnerability_id` | string | Vulnerability id |
| `organization_id` | string | Organization that owns the vulnerability |
| `scan_id` | string \| null | Related scan, when present |
| `pr_review_id` | string \| null | Related PR review, when present |
| `status` | string | New vulnerability status |
| `previous_status` | string \| null | Previous vulnerability status |
| `status_changed_at` | ISO 8601 timestamp \| null | Status change time |
| `status_changed_by` | string \| null | Actor that changed the status |
| `updated_at` | ISO 8601 timestamp | Time the webhook event was created |

```json theme={null}
{
  "id": "delivery_123",
  "event": "vulnerability.status_changed",
  "created_at": "2026-05-19T06:45:00.000Z",
  "data": {
    "vulnerability_id": "vuln_123",
    "organization_id": "org_123",
    "scan_id": "scan_123",
    "pr_review_id": null,
    "status": "fixed",
    "previous_status": "open",
    "status_changed_at": "2026-05-19T06:45:00.000Z",
    "status_changed_by": "user@example.com",
    "updated_at": "2026-05-19T06:45:00.000Z"
  }
}
```

### vulnerability.severity\_changed

The severity of a vulnerability changed, with an override reason.

| Field | Type | Description |
| - | - | - |
| `vulnerability_id` | string | Vulnerability id |
| `organization_id` | string | Organization that owns the vulnerability |
| `scan_id` | string \| null | Related scan, when present |
| `pr_review_id` | string \| null | Related PR review, when present |
| `severity` | `critical` \| `high` \| `medium` \| `low` | New severity |
| `previous_severity` | `critical` \| `high` \| `medium` \| `low` \| null | Previous severity |
| `original_severity` | `critical` \| `high` \| `medium` \| `low` \| null | Original severity before overrides |
| `severity_changed_at` | ISO 8601 timestamp \| null | Severity change time |
| `severity_changed_by` | string \| null | Actor that changed the severity |
| `severity_override_reason` | string \| null | Reason supplied for the change |
| `updated_at` | ISO 8601 timestamp | Time the webhook event was created |

```json theme={null}
{
  "id": "delivery_123",
  "event": "vulnerability.severity_changed",
  "created_at": "2026-05-19T06:52:00.000Z",
  "data": {
    "vulnerability_id": "vuln_123",
    "organization_id": "org_123",
    "scan_id": "scan_123",
    "pr_review_id": null,
    "severity": "medium",
    "previous_severity": "high",
    "original_severity": "high",
    "severity_changed_at": "2026-05-19T06:52:00.000Z",
    "severity_changed_by": "user@example.com",
    "severity_override_reason": "Compensating control is deployed.",
    "updated_at": "2026-05-19T06:52:00.000Z"
  }
}
```

## Create a webhook

```bash theme={null}
curl -X POST "https://app.strix.ai/api/v1/webhooks" \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/strix-webhook",
    "events": ["scan.completed", "vulnerability.created"],
    "is_active": true
  }'
```

**Required scope:** `webhooks:write`

The response includes a `secret` field. Keep the secret in a safe location. Strix does not show the secret again.

## Per-business\_unit routing

By default, a subscription is **org-wide**. It receives every applicable event in your organization. On Enterprise plans, you can attach a subscription to a single `business_unit`. The subscription then receives only the events for assets in that business unit. This is useful when you have multiple end-customers in one organization and each customer must receive only its own events.

```bash theme={null}
curl -X POST "https://app.strix.ai/api/v1/webhooks" \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://acme.example.com/strix-webhook",
    "events": ["*"],
    "business_unit": "acme"
  }'
```

* For an org-wide subscription, omit `business_unit` or set it to `null`. This is the default behavior.
* To receive only the events for one business unit, set `business_unit` to that value. The business units of an event come from the related assets (domains and repositories).
* The `business_unit` field requires the Enterprise plan (the `rbac` feature). On other plans, requests that set this field return `403`.
* To convert a scoped subscription back to org-wide, `PATCH` it with `"business_unit": null`.

<Note>
  To set the `business_unit` of an asset, use `POST` or `PATCH` on `/v1/domains` and `/v1/repositories`. A token that is scoped to a `business_unit` can only see and manage the assets, scans, and vulnerabilities of that unit.
</Note>

## Verify webhook signatures

Every webhook delivery includes signature headers for verification:

| Header | Description |
| - | - |
| `X-Strix-Event` | The event type (for example, `scan.completed`) |
| `X-Strix-Delivery` | Unique delivery ID (use it as an idempotency key) |
| `X-Strix-Timestamp` | Time when the event was sent (ISO 8601) |
| `X-Strix-Signature` | HMAC-SHA256 signature for payload verification |

### Signature verification (Node.js)

```javascript theme={null}
import crypto from "crypto";

export function verifyStrixWebhook(rawBody, headers, secret) {
  const signature = headers["x-strix-signature"];
  const timestamp = headers["x-strix-timestamp"];
  if (!signature || !timestamp) return false;

  const parsedTime = Date.parse(timestamp);
  const ageMs = Math.abs(Date.now() - parsedTime);
  if (!Number.isFinite(parsedTime) || ageMs > 5 * 60 * 1000) return false;

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

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}
```

<Warning>
  Always make sure that the signature is correct before you process a webhook payload. Reject requests that have a timestamp older than 5 minutes. This prevents replay attacks.
</Warning>

## Delivery and retries

Strix sends events asynchronously, with retries and exponential backoff. A delivery can occur more than one time. When you process events, use the `X-Strix-Delivery` header as an idempotency key.

## Manage webhooks

### List webhooks

```bash theme={null}
curl -X GET "https://app.strix.ai/api/v1/webhooks" \
  -H "Authorization: Bearer <YOUR_API_TOKEN>"
```

**Required scope:** `webhooks:read`

### Update a webhook

```bash theme={null}
curl -X PATCH "https://app.strix.ai/api/v1/webhooks/<WEBHOOK_ID>" \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"events": ["*"], "is_active": true}'
```

**Required scope:** `webhooks:write`

### Rotate webhook secret

```bash theme={null}
curl -X PATCH "https://app.strix.ai/api/v1/webhooks/<WEBHOOK_ID>" \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"rotate_secret": true}'
```

The response includes the new `secret`. The old secret immediately becomes invalid.

**Required scope:** `webhooks:write`

### Delete a webhook

```bash theme={null}
curl -X DELETE "https://app.strix.ai/api/v1/webhooks/<WEBHOOK_ID>" \
  -H "Authorization: Bearer <YOUR_API_TOKEN>"
```

**Required scope:** `webhooks:write`

### Inspect deliveries

```bash theme={null}
curl -X GET "https://app.strix.ai/api/v1/webhooks/<WEBHOOK_ID>/deliveries?limit=25" \
  -H "Authorization: Bearer <YOUR_API_TOKEN>"
```

**Required scope:** `webhooks:read`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.