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

# Outbound Webhooks

> BattleTest outbound webhooks reference — envelope, supported events, per-event payload schemas, headers, HMAC signature verification, and retry behaviour.

<Warning>
  **Dev plan or higher.** Outbound webhooks are not available on the Free plan. [View plans →](https://battletest.dev/pricing)
</Warning>

BattleTest sends a signed HTTP POST to your endpoint for each event you subscribe to. Configure webhooks at [Settings → Webhooks](https://battletest.dev/settings?tab=webhooks).

## Envelope

Every webhook request body is a JSON object with this shape:

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "workflow.finding",
  "timestamp": "2025-11-14T10:32:00.000Z",
  "data": { ... }
}
```

| Field       | Type     | Description                                            |
| ----------- | -------- | ------------------------------------------------------ |
| `id`        | `string` | Unique event ID (UUID v4)                              |
| `type`      | `string` | Event type — see [Supported events](#supported-events) |
| `timestamp` | `string` | ISO 8601 UTC time the event was created                |
| `data`      | `object` | Event-specific payload — see sections below            |

## Request headers

| Header                   | Description                                                       |
| ------------------------ | ----------------------------------------------------------------- |
| `Content-Type`           | `application/json`                                                |
| `User-Agent`             | `Battletest-Workflows/1.0`                                        |
| `X-Webhook-Event`        | Event type (same as `type` in the body)                           |
| `X-Webhook-Id`           | Event ID (same as `id` in the body)                               |
| `X-Webhook-Timestamp`    | Event timestamp (same as `timestamp` in the body)                 |
| `X-Webhook-Attempt`      | Delivery attempt number starting at `1`                           |
| `X-BattleTest-Signature` | HMAC-SHA256 signature — present only when a signing secret is set |

## Supported events

| Event                     | Fires when                                        |
| ------------------------- | ------------------------------------------------- |
| `workflow.started`        | A live battletest scan begins                     |
| `workflow.completed`      | A live battletest scan finishes successfully      |
| `workflow.failed`         | A live battletest scan fails to complete          |
| `workflow.finding`        | A new security finding is confirmed during a scan |
| `workflow.step_completed` | A scan pipeline step finishes                     |
| `workflow.checkpoint`     | A human-in-the-loop approval is requested         |
| `pr_review.started`       | A PR security review begins                       |
| `pr_review.completed`     | A PR security review finishes                     |
| `pr_review.failed`        | A PR security review fails to complete            |

***

## Payload schemas

### `workflow.started`

Fires when a live battletest scan begins.

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "workflow.started",
  "timestamp": "2025-11-14T10:30:00.000Z",
  "data": {
    "workflowId": "wf_abc123",
    "workflowType": "hypothesisTest",
    "runId": "run_xyz789",
    "customerId": "usr_...",
    "target": "app.example.com",
    "startedAt": "2025-11-14T10:30:00.000Z"
  }
}
```

| Field          | Type     | Description                                |
| -------------- | -------- | ------------------------------------------ |
| `workflowId`   | `string` | Internal workflow instance ID              |
| `workflowType` | `string` | Workflow pipeline name                     |
| `runId`        | `string` | Scan run ID — use this to correlate events |
| `customerId`   | `string` | Your user ID                               |
| `target`       | `string` | Domain or URL being tested                 |
| `startedAt`    | `string` | ISO 8601 start time                        |

***

### `workflow.completed`

Fires when a live battletest scan finishes successfully.

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "workflow.completed",
  "timestamp": "2025-11-14T10:45:00.000Z",
  "data": {
    "workflowId": "wf_abc123",
    "workflowType": "hypothesisTest",
    "runId": "run_xyz789",
    "customerId": "usr_...",
    "target": "app.example.com",
    "duration": 900000,
    "findings": 3,
    "completedAt": "2025-11-14T10:45:00.000Z"
  }
}
```

| Field          | Type                    | Description                         |
| -------------- | ----------------------- | ----------------------------------- |
| `workflowId`   | `string`                | Internal workflow instance ID       |
| `workflowType` | `string`                | Workflow pipeline name              |
| `runId`        | `string`                | Scan run ID                         |
| `customerId`   | `string`                | Your user ID                        |
| `target`       | `string`                | Domain or URL tested                |
| `duration`     | `number`                | Wall-clock duration in milliseconds |
| `findings`     | `number` \| `undefined` | Total confirmed findings            |
| `completedAt`  | `string`                | ISO 8601 completion time            |

***

### `workflow.failed`

Fires when a live battletest scan fails to complete.

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "workflow.failed",
  "timestamp": "2025-11-14T10:35:00.000Z",
  "data": {
    "workflowId": "wf_abc123",
    "workflowType": "hypothesisTest",
    "runId": "run_xyz789",
    "customerId": "usr_...",
    "target": "app.example.com",
    "duration": 300000,
    "error": "Target unreachable after 3 retries",
    "step": "discovery",
    "failedAt": "2025-11-14T10:35:00.000Z"
  }
}
```

| Field          | Type                    | Description                                       |
| -------------- | ----------------------- | ------------------------------------------------- |
| `workflowId`   | `string`                | Internal workflow instance ID                     |
| `workflowType` | `string`                | Workflow pipeline name                            |
| `runId`        | `string`                | Scan run ID                                       |
| `customerId`   | `string`                | Your user ID                                      |
| `target`       | `string`                | Domain or URL being tested                        |
| `duration`     | `number`                | Wall-clock duration in milliseconds until failure |
| `error`        | `string`                | Human-readable error message                      |
| `step`         | `string` \| `undefined` | Pipeline step that failed, if known               |
| `failedAt`     | `string`                | ISO 8601 failure time                             |

***

### `workflow.finding`

Fires once per confirmed security finding during a scan. Subscribe to this event to get real-time alerts as vulnerabilities are discovered rather than waiting for the scan to complete.

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "workflow.finding",
  "timestamp": "2025-11-14T10:38:00.000Z",
  "data": {
    "workflowId": "wf_abc123",
    "workflowType": "hypothesisTest",
    "runId": "run_xyz789",
    "customerId": "usr_...",
    "target": "app.example.com",
    "environmentId": null,
    "finding": {
      "id": "fnd_...",
      "title": "SQL Injection in /api/search",
      "description": "The `q` parameter is interpolated directly into a raw SQL query...",
      "severity": "critical",
      "exploitable": true,
      "cweId": "CWE-89",
      "endpoint": "/api/search?q=",
      "cvss": 9.8
    },
    "discoveredAt": "2025-11-14T10:38:00.000Z"
  }
}
```

| Field                 | Type                    | Description                                   |
| --------------------- | ----------------------- | --------------------------------------------- |
| `workflowId`          | `string`                | Internal workflow instance ID                 |
| `workflowType`        | `string`                | Workflow pipeline name                        |
| `runId`               | `string`                | Scan run ID                                   |
| `customerId`          | `string`                | Your user ID                                  |
| `target`              | `string`                | Domain or URL tested                          |
| `environmentId`       | `string` \| `null`      | Environment ID if scoping is configured       |
| `finding.id`          | `string`                | Finding ID                                    |
| `finding.title`       | `string`                | Short description of the vulnerability        |
| `finding.description` | `string` \| `undefined` | Detailed explanation                          |
| `finding.severity`    | `string`                | `critical` \| `high` \| `medium` \| `low`     |
| `finding.exploitable` | `boolean`               | Whether the finding was confirmed exploitable |
| `finding.cweId`       | `string` \| `undefined` | CWE identifier (e.g. `CWE-89`)                |
| `finding.endpoint`    | `string` \| `undefined` | Affected path or URL                          |
| `finding.cvss`        | `number` \| `undefined` | CVSS v3 base score                            |
| `discoveredAt`        | `string`                | ISO 8601 discovery time                       |

***

### `workflow.step_completed`

Fires after each pipeline step finishes. Useful for progress tracking in long-running scans.

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "workflow.step_completed",
  "timestamp": "2025-11-14T10:33:00.000Z",
  "data": {
    "workflowId": "wf_abc123",
    "workflowType": "hypothesisTest",
    "runId": "run_xyz789",
    "step": "discovery",
    "stepNumber": 1,
    "totalSteps": 5,
    "duration": 45000,
    "completedAt": "2025-11-14T10:33:00.000Z"
  }
}
```

| Field          | Type     | Description                                           |
| -------------- | -------- | ----------------------------------------------------- |
| `workflowId`   | `string` | Internal workflow instance ID                         |
| `workflowType` | `string` | Workflow pipeline name                                |
| `runId`        | `string` | Scan run ID                                           |
| `step`         | `string` | Step name (e.g. `discovery`, `hypothesis`, `exploit`) |
| `stepNumber`   | `number` | 1-based step index                                    |
| `totalSteps`   | `number` | Total steps in this workflow                          |
| `duration`     | `number` | Step duration in milliseconds                         |
| `completedAt`  | `string` | ISO 8601 completion time                              |

***

### `workflow.checkpoint`

Fires when the scan engine pauses to request human approval before taking a potentially sensitive action (human-in-the-loop mode). Respond to the checkpoint via the BattleTest API or dashboard to resume the scan.

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "workflow.checkpoint",
  "timestamp": "2025-11-14T10:40:00.000Z",
  "data": {
    "workflowId": "wf_abc123",
    "workflowType": "hypothesisTest",
    "runId": "run_xyz789",
    "customerId": "usr_...",
    "target": "app.example.com",
    "environmentId": null,
    "checkpointId": "chk_...",
    "step": "exploit",
    "actionDescription": "Attempt blind SQL injection on POST /api/login to confirm exploitability",
    "createdAt": "2025-11-14T10:40:00.000Z"
  }
}
```

| Field               | Type               | Description                                     |
| ------------------- | ------------------ | ----------------------------------------------- |
| `workflowId`        | `string`           | Internal workflow instance ID                   |
| `workflowType`      | `string`           | Workflow pipeline name                          |
| `runId`             | `string`           | Scan run ID                                     |
| `customerId`        | `string`           | Your user ID                                    |
| `target`            | `string`           | Domain or URL being tested                      |
| `environmentId`     | `string` \| `null` | Environment ID if scoping is configured         |
| `checkpointId`      | `string`           | ID to use when approving or rejecting via API   |
| `step`              | `string`           | Pipeline step that triggered the checkpoint     |
| `actionDescription` | `string`           | Plain-English description of the pending action |
| `createdAt`         | `string`           | ISO 8601 time the checkpoint was created        |

***

### `pr_review.started`

Fires when BattleTest begins reviewing a pull request.

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "pr_review.started",
  "timestamp": "2025-11-14T09:01:00.000Z",
  "data": {
    "reviewId": "prr_...",
    "repo": "acme/backend",
    "prNumber": 42,
    "prTitle": "Add OAuth2 login",
    "author": "jsmith",
    "baseRef": "main",
    "headRef": "feat/oauth2",
    "customerId": "usr_...",
    "startedAt": "2025-11-14T09:01:00.000Z"
  }
}
```

| Field        | Type     | Description                              |
| ------------ | -------- | ---------------------------------------- |
| `reviewId`   | `string` | Review run ID                            |
| `repo`       | `string` | GitHub repository in `owner/name` format |
| `prNumber`   | `number` | Pull request number                      |
| `prTitle`    | `string` | Pull request title                       |
| `author`     | `string` | GitHub username of the PR author         |
| `baseRef`    | `string` | Target branch                            |
| `headRef`    | `string` | Source branch                            |
| `customerId` | `string` | Your user ID                             |
| `startedAt`  | `string` | ISO 8601 start time                      |

***

### `pr_review.completed`

Fires when BattleTest finishes reviewing a pull request.

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "pr_review.completed",
  "timestamp": "2025-11-14T09:04:30.000Z",
  "data": {
    "reviewId": "prr_...",
    "repo": "acme/backend",
    "prNumber": 42,
    "prTitle": "Add OAuth2 login",
    "customerId": "usr_...",
    "verdict": "request_changes",
    "riskScore": 72,
    "findings": {
      "critical": 0,
      "high": 1,
      "medium": 2,
      "low": 1
    },
    "completedAt": "2025-11-14T09:04:30.000Z"
  }
}
```

| Field               | Type     | Description                                                |
| ------------------- | -------- | ---------------------------------------------------------- |
| `reviewId`          | `string` | Review run ID                                              |
| `repo`              | `string` | GitHub repository in `owner/name` format                   |
| `prNumber`          | `number` | Pull request number                                        |
| `prTitle`           | `string` | Pull request title                                         |
| `customerId`        | `string` | Your user ID                                               |
| `verdict`           | `string` | `approve` \| `request_changes` \| `comment`                |
| `riskScore`         | `number` | 0–100 composite risk score; ≥ 40 maps to `request_changes` |
| `findings.critical` | `number` | Count of CRITICAL findings                                 |
| `findings.high`     | `number` | Count of HIGH findings                                     |
| `findings.medium`   | `number` | Count of MEDIUM findings                                   |
| `findings.low`      | `number` | Count of LOW findings                                      |
| `completedAt`       | `string` | ISO 8601 completion time                                   |

***

### `pr_review.failed`

Fires when the review pipeline fails to complete due to an internal error.

```json theme={null}
{
  "id": "evt_01j9...",
  "type": "pr_review.failed",
  "timestamp": "2025-11-14T09:02:15.000Z",
  "data": {
    "reviewId": "prr_...",
    "repo": "acme/backend",
    "prNumber": 42,
    "customerId": "usr_...",
    "error": "Diff fetch timed out after 30 s",
    "failedAt": "2025-11-14T09:02:15.000Z"
  }
}
```

| Field        | Type     | Description                              |
| ------------ | -------- | ---------------------------------------- |
| `reviewId`   | `string` | Review run ID                            |
| `repo`       | `string` | GitHub repository in `owner/name` format |
| `prNumber`   | `number` | Pull request number                      |
| `customerId` | `string` | Your user ID                             |
| `error`      | `string` | Human-readable error message             |
| `failedAt`   | `string` | ISO 8601 failure time                    |

***

<h2 id="signature-verification">
  Signature verification
</h2>

When a signing secret is set, every delivery includes an `X-BattleTest-Signature` header containing `sha256=<hex>`. Verify it against the raw request body before processing the payload.

```js theme={null}
// Node.js / Express
const crypto = require('crypto')

app.post('/hooks/battletest', (req, res) => {
  const sig = req.headers['x-battletest-signature']
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.rawBody) // raw Buffer — do not parse first
    .digest('hex')

  if (sig !== expected) return res.status(401).send('Bad signature')

  const event = req.body
  // handle event.type …
  res.sendStatus(200)
})
```

<Warning>
  Always verify the signature before acting on a payload. Use a constant-time comparison to prevent timing attacks.
</Warning>

## Retries

Failed deliveries (non-2xx response or timeout) are retried up to **3 times** with exponential backoff starting at 1 second. Respond with any 2xx status to acknowledge receipt — BattleTest does not inspect the response body.
