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

# Agent Investigation Completed Webhook

> Verify and parse the agent.investigation.completed.v1 webhook, then route a Security Delivery Agent's verdict to your SIEM, ticketing, or on-call tools.

When a Security Delivery Agent finishes investigating an incident, ContraForce can send a signed webhook to an endpoint you control. Use it to escalate true-positive incidents into your SIEM, ticketing, or on-call tooling at the moment the agent reaches a verdict.

<Info>
  This event is configured per classification on an Agent Configuration card, not as a broadcast subscription. It is delivered **only** to the webhook a classification card points to. Set it up under [Configuring Security Delivery Agents](/guides/getting-started/configuring-security-delivery-agents) by enabling **Advanced** mode and choosing a webhook as the **custom action** for a classification.
</Info>

***

## When It Fires

The event fires once per investigation, when the agent completes and reaches a classification, for any classification whose policy has a webhook custom action configured. The event type (schema) is:

```
agent.investigation.completed.v1
```

***

## Request Headers

Every delivery includes these headers. Use them to verify authenticity before trusting the body.

| Header | Value | Notes |
| - | - | - |
| `X-CF-Schema` | `agent.investigation.completed.v1` | The event type. |
| `X-CF-Event-Id` | A unique event identifier (GUID) | Use it to deduplicate retries. |
| `X-CF-Timestamp` | ISO 8601 timestamp | Reject if it is more than 5 minutes from your clock. |
| `X-CF-Signature` | Base64-encoded HMAC-SHA256 signature | See verification below. |
| `X-CF-Test` | `true` | Present only for test deliveries. |
| `Authorization` | `Bearer <token>` or `Basic <base64>` | Present only if you configured authentication on the webhook. |

<Warning>
  Retries reuse the same `X-CF-Event-Id`. Treat delivery as at-least-once and make your handler idempotent.
</Warning>

***

## Verifying the Signature

The signature covers the timestamp and the exact raw request body:

```
signature = Base64( HMAC_SHA256( signing_key, X-CF-Timestamp + "." + raw_body ) )
```

The `signing_key` is the secret shown once when the webhook was created, unless you supplied your own signing token override when setting bearer-token credentials, in which case it is that token.

<Steps>
  <Step title="Read the raw body">
    Compute the signature over the unparsed request body bytes, before any JSON deserialization.
  </Step>

  <Step title="Recompute">
    Concatenate the `X-CF-Timestamp` value, a literal `.`, and the raw body. HMAC-SHA256 it with your signing key and Base64-encode the result.
  </Step>

  <Step title="Compare in constant time">
    Compare your value to `X-CF-Signature` using a constant-time comparison. Reject on mismatch.
  </Step>

  <Step title="Check the timestamp">
    Reject the request if `X-CF-Timestamp` is more than 5 minutes from current time, to limit replay.
  </Step>
</Steps>

<CodeGroup>
  ```python verify.py theme={null}
  import base64, hashlib, hmac, time
  from datetime import datetime, timezone

  def verify(signing_key: str, headers: dict, raw_body: bytes) -> bool:
      ts = headers["X-CF-Timestamp"]
      sent = headers["X-CF-Signature"]
      signed = ts.encode() + b"." + raw_body
      expected = base64.b64encode(
          hmac.new(signing_key.encode(), signed, hashlib.sha256).digest()
      ).decode()
      if not hmac.compare_digest(expected, sent):
          return False
      age = abs(time.time() - datetime.fromisoformat(ts).replace(tzinfo=timezone.utc).timestamp())
      return age <= 300
  ```

  ```csharp Verify.cs theme={null}
  static bool Verify(string signingKey, string timestamp, string sentSignature, byte[] rawBody)
  {
      var signed = System.Text.Encoding.UTF8.GetBytes(timestamp + ".")
          .Concat(rawBody).ToArray();
      using var hmac = new System.Security.Cryptography.HMACSHA256(
          System.Text.Encoding.UTF8.GetBytes(signingKey));
      var expected = Convert.ToBase64String(hmac.ComputeHash(signed));
      var match = System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
          System.Text.Encoding.UTF8.GetBytes(expected),
          System.Text.Encoding.UTF8.GetBytes(sentSignature));
      var age = Math.Abs((DateTimeOffset.UtcNow - DateTimeOffset.Parse(timestamp)).TotalSeconds);
      return match && age <= 300;
  }
  ```
</CodeGroup>

***

## Payload

The body is JSON with camelCase fields. Like every ContraForce webhook, the event-specific payload is nested under `data` inside a common envelope:

```json theme={null}
{
  "type": "agent.investigation.completed.v1",
  "timestamp": "2026-08-03T14:31:09.740Z",
  "isTest": false,
  "occurredAt": "2026-08-03T14:31:02.310Z",
  "data": {
    "workspace": {
      "id": "00000000-0000-0000-0000-000000000000",
      "alias": "contoso",
      "name": "Contoso Production"
    },
    "agent": {
      "id": "agent-identifier",
      "name": "Tier 1 Triage Agent"
    },
    "incident": {
      "id": "source-system-incident-id",
      "number": 4242,
      "source": "Sentinel",
      "title": "Suspicious sign-in from impossible travel",
      "severity": "High",
      "status": "Active"
    },
    "verdict": {
      "classificationBucket": "TruePositive",
      "classificationReason": "MaliciousActivity",
      "classificationReasonComment": "Confirmed credential theft",
      "comment": "Sign-in originated from a known-malicious ASN minutes after a login from the user's usual location.",
      "maliciousScore": 87
    },
    "gamebookRecommendation": {
      "incidentNumber": 4242,
      "incidentTitle": "Suspicious sign-in from impossible travel",
      "playbooks": [
        {
          "playbookId": "disable-user",
          "affectedEntity": "Account",
          "entityId": "jdoe@contoso.example",
          "sequence": 1
        }
      ]
    }
  }
}
```

<Warning>
  Sign and verify the full raw body, envelope included. Do not compute the signature over the `data` object alone.
</Warning>

### Fields

Field paths below are relative to `data`.

| Field | Type | Description |
| - | - | - |
| `workspace.id` / `alias` / `name` | string | The ContraForce workspace the incident belongs to. |
| `agent.id` / `name` | string | The Security Delivery Agent that ran the investigation. |
| `incident.id` | string | The source system's incident identifier. |
| `incident.number` | number | The incident number shown in the portal. |
| `incident.source` | string | Detection source, for example `Sentinel`. |
| `incident.title` | string | Incident title. |
| `incident.severity` | string | Incident severity, for example `High`. |
| `incident.status` | string | Incident status at completion. |
| `verdict.classificationBucket` | string | The agent's verdict. One of `TruePositive`, `BenignPositive`, `FalsePositive`, `Undetermined`. |
| `verdict.classificationReason` | string \| null | Reason code for the classification. |
| `verdict.classificationReasonComment` | string \| null | Free-text reason detail. |
| `verdict.comment` | string | The agent's investigation summary comment. |
| `verdict.maliciousScore` | number \| null | How malicious the agent judged the incident, on a 0-100 scale. `null` when the investigation came through the legacy agent path, which collects no score. |
| `gamebookRecommendation` | object \| null | Present only when the agent recommended Gamebooks. |
| `gamebookRecommendation.playbooks[]` | array | Recommended Gamebooks, each with `playbookId`, `affectedEntity`, `entityId`, and `sequence`. |

<Tip>
  Branch on `verdict.classificationBucket`. It is a stable, vendor-neutral value, so your integration does not need to handle source-specific classification strings. Use `verdict.maliciousScore` when you need degree rather than category, for example auto-closing below a threshold and paging above one.
</Tip>

<Warning>
  `maliciousScore` measures **maliciousness**, not the agent's confidence. A verdict the agent is completely certain is a False Positive scores near **0**, not near 100. See the [webhook event reference](/api-reference/webhook-events#malicious-score) for the score bands.
</Warning>

***

## Testing

Use the **Send test** action on the webhook in **Developers** to deliver a synthetic event (see [Setting Up Webhooks](/guides/technical/configure-webhooks#send-a-test-and-check-the-delivery)). Test deliveries carry `X-CF-Test: true` and use sample data. They are signed identically to live events, so you can validate your verification code end to end.

***

## Troubleshooting

| Symptom | Likely cause | Resolution |
| - | - | - |
| No events arrive | The classification card does not point to this webhook, or Advanced mode is off | Confirm the webhook is set as the custom action for the classification and that Advanced mode is enabled and saved |
| Events stop arriving | The webhook was deleted, paused, disabled, or unsubscribed | The classification card shows a binding warning. Fix the webhook in Developers. Skipped deliveries are recorded as **Failed** in the delivery log, not dropped silently |
| Signature check fails | Verifying a parsed body instead of the raw bytes, or using the wrong key | Sign the raw request body, and use the signing token override if one was set |
| Duplicate events | Normal retry behavior | Deduplicate on `X-CF-Event-Id` |

***

## Other Event Types

This is one of five events ContraForce can deliver. For the complete list, including incident creation, incident closure, and Gamebook runs, see the [Webhook Event Reference](/api-reference/webhook-events).

***

## Related Guides

<CardGroup cols={2}>
  <Card title="Setting Up Webhooks" icon="webhook" href="/guides/technical/configure-webhooks">
    Create a webhook in Settings → Developers, send a test, and manage deliveries
  </Card>

  <Card title="Configuring Security Delivery Agents" icon="robot" href="/guides/getting-started/configuring-security-delivery-agents">
    Configure agents and choose a webhook as a classification's custom action
  </Card>

  <Card title="Webhook Event Reference" icon="book" href="/api-reference/webhook-events">
    Every event type, the delivery envelope, headers, and retry rules
  </Card>

  <Card title="Classification Mapping" icon="tags" href="/concepts/incident-classification">
    How each classification maps to Sentinel, Defender XDR, and SentinelOne values
  </Card>
</CardGroup>

***

<Note>
  Questions about the agent investigation webhook? Contact us at [support@contraforce.com](mailto:support@contraforce.com).
</Note>


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