Skip to main content
ContraForce delivers events to your endpoint over HTTPS as signed JSON. This page lists every event type the platform can send, the exact body shape for each, and the delivery rules that apply to all of them. Webhooks are managed under Settings → Developer Integrations in the portal, or through the /webhooks endpoints with the webhooks:read and webhooks:manage scopes. The portal shows the same sample deliveries as this page: use the info button next to an event when creating or editing a webhook to view and copy its sample payload.

Event Types

Routing models

The webhook lists the event type in its eventTypes array. Every active webhook in scope receives the event.Scope resolution covers the incident’s own account plus any managing service provider accounts. A service provider webhook with a non-empty monitoredAccountIds list only receives events from the accounts it lists; an empty list means all managed accounts.
Subscribing alone does not opt a webhook in. An owning entity has to name the webhook explicitly. Today this applies only to agent.investigation.completed.v1, where an Agent Configuration’s classification card selects the target webhook.The webhook must also keep agent.investigation.completed.v1 in its eventTypes. If it is unsubscribed while still bound to a classification card, the delivery is recorded as Failed in the delivery log rather than dropped silently.
webhook.test.v1 is only produced by the Send test action against one specific webhook. It is not a subscribable event.The incident Trigger webhook action is also manual, but it reuses incident.created.v1 so that existing receivers process it without code changes. Subscription matching is bypassed for that fire, though monitoredAccountIds is still enforced.

The Delivery Envelope

Every event, regardless of type, is delivered as the same outer envelope. The event-specific schema lives under data.
Verify the signature over the full raw body, envelope included. Do not sign or hash only the data object.
All payload property names are camelCase.

Request Headers


Verifying the Signature

The signature covers the timestamp and the exact raw request 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.

webhook.test.v1

A synthetic connectivity check, sent only by the Send test action on a single webhook. It carries X-CF-Test: true and isTest: true, and is signed exactly like a live event, so you can validate your verification code end to end before any real traffic arrives. The payload mirrors the incident.created.v1 shape with fixed sample values. Both vendor blocks are populated in the test event so you can build against the full shape; a real incident carries whichever blocks apply to it.
Test deliveries are excluded from the auto-disable circuit breaker, so a failing test never takes a live webhook offline.

incident.created.v1

Fires when a new incident is ingested into a workspace the webhook monitors. The same event type is emitted by the incident Trigger webhook action so that receivers do not need a separate code path for a manually replayed incident.

Fields

Null fields are omitted from this payload rather than serialized as null. Treat a missing owner or lastActivityAt as absent.

Vendor identifiers

incident.created.v1 carries an vendorIdentifiers object holding the identifiers of every vendor system known to hold a record of the incident. It exists because incidentId alone is not always enough to act: addressing a Microsoft Sentinel incident over ARM needs the full resource id, and one real-world incident can exist in both Defender XDR and Sentinel under different ids. The same object rides on incident.created.v1, incident.closed.v1, incident.status.changed.v1, incident.assignee.changed.v1 and incident.comment.added.v1, and is exposed on the incident representation in the REST API, so a receiver resolves an incident the same way whichever surface it came from.

Which blocks are present

Every block and every field inside it is conditional. Treat an absent block as “not resolvable for this incident”, not as an error.
For a Defender XDR incident, the sentinel block depends on Microsoft having already created the Sentinel counterpart. Immediately after an incident is created that copy may not exist yet, so the block can be absent on incident.created.v1 and present when the same incident is later read over the REST API. Branch on the field you need being present rather than assuming it always will be.
This object is additive. Existing fields, including the top-level incidentId, are unchanged and continue to carry the identifier of the source the incident was ingested from.

incident.closed.v1

Fires when an incident reaches Closed, whichever route got it there: the portal, a bulk close, the AI agent, or the public API. Closures performed directly in the vendor’s own console are also picked up by reconciliation and delivered here, distinguished by closedBy.origin.
A close also fires incident.status.changed.v1 for webhooks subscribed to it. The two are separate deliveries with different event IDs. Handle closes from one of them: this event when you need the verdict and closing comment, the status event when you mirror every status.

Fields

Branch on classification, not on vendor strings. ContraForce normalizes every source’s closure vocabulary into the four canonical values, so your integration does not have to track vendor enum changes. See Incident classification.

incident.status.changed.v1

Fires on every status transition an incident goes through in ContraForce: the portal, a bulk update, the AI agent, or the public API. Closures picked up from the vendor’s own console by reconciliation are delivered too, with changedBy.origin set to upstream.
This event fires on close as well. A close produces both this event (with status: "Closed") and incident.closed.v1, as two separate deliveries with different event IDs. Subscribe to this event to mirror every status into another system, and to incident.closed.v1 when you only care about the verdict and the closing comment. Subscribing to both means handling each close twice.

Fields

OnHold and WaitingOnCustomer are ContraForce statuses. They pause the SLA clocks and never propagate to the vendor, so a receiver mirroring status into a PSA should map them to its own waiting states rather than expect to find them in Sentinel or Defender.

incident.assignee.changed.v1

Fires when an incident is assigned, reassigned or unassigned through ContraForce, whether by an analyst, a bulk update, or a Security Delivery Agent assigning itself. Ownership changes made directly in the vendor’s console are not detected.

Fields

A previousAssignee of null with an assignee is a first assignment, and an assignee of null is an unassignment. Use assignee.isAgent to skip the agent’s own self-assignment when your ticketing sync should only follow human owners.

incident.comment.added.v1

Fires when a comment is added to an incident through ContraForce by an analyst, a Security Delivery Agent, or a Gamebook. Comments written directly in the vendor’s console are not detected, and the closing comment entered on a close travels on incident.closed.v1 rather than here.

Fields

The incident block (incidentNumber, title, severity, sourceDisplayName, vendorIdentifiers) is read from the source system when the event is built. If that read fails, the event is still delivered with those fields null. incidentId, source and comment are always present, so key your handler on those.

gamebook.run.v1

Fires when a Gamebook is dispatched to the executor against an incident, whether an analyst started it from the portal or public API, or the AI agent did.
Gamebooks awaiting approval do not fire this event while queued. The event is emitted when the run is actually dispatched.

Fields


agent.investigation.completed.v1

Fires once per investigation, when a Security Delivery Agent completes and reaches a classification whose card has a webhook custom action configured.
This event is configured per classification on an Agent Configuration card, not as a broadcast subscription. Set it up under Configuring Security Delivery Agents by enabling Advanced mode and choosing a webhook as the custom action for a classification. Full walkthrough and troubleshooting: Agent Investigation Completed Webhook.

Fields

Malicious score

verdict.maliciousScore reports how malicious the agent judged the incident to be, on a 0-100 scale. It lets you branch on degree rather than only on the four-value classificationBucket, for example auto-closing below a threshold while escalating above one.
The score is a maliciousness reading, not a confidence reading. A verdict the agent is completely certain is a False Positive scores near 0, not near 100. Reading it as confidence inverts the meaning of every benign verdict.
The bands above match how the portal colors the score. They are guidance for reading a value, not thresholds the platform enforces. maliciousScore is null when the investigation came through the legacy agent path, which does not collect a score. The key is always present on this payload, so check for null rather than for absence.

Delivery Rules

These apply to every event type.

Retries

Delivery is at-least-once. A failed attempt is retried with a fixed backoff, up to 10 attempts within a 5 minute window from the first attempt. Whichever limit is hit first ends the retry budget and the event is dead-lettered. Retries are triggered by HTTP 408, 429, 500, 502, 503, and 504, and by connection or timeout errors. Any other non-success status is treated as a permanent failure and is not retried. A Retry-After header is honored when it is longer than the scheduled backoff, clamped to the 5 minute retry window.
Retries reuse the same X-CF-Event-Id. Make your handler idempotent and deduplicate on that value.

Event IDs

incident.created.v1, incident.closed.v1, gamebook.run.v1 and incident.comment.added.v1 derive a deterministic event ID from the underlying subject (the comment, for comment events), so a republished event produces the same ID and can be deduplicated. incident.status.changed.v1 and incident.assignee.changed.v1 get a fresh ID per transition. The same transition can happen more than once on one incident (pause, resume, pause again), and each occurrence is a distinct event that a subject-derived ID would wrongly collapse. Test events and manually triggered incident fires get a fresh ID per fire, which is what distinguishes a manual replay from the original automatic event.

Auto-disable

If 5 or more distinct incidents fail final delivery to the same webhook within a rolling 24 hours, the webhook is automatically disabled and stops receiving events. Test deliveries never count toward this threshold. Re-enable it with POST /webhooks/{id}/enable or from the webhook detail page in the portal, after fixing the endpoint.

Delivery logs

Every delivery attempt is recorded as a delivery-log entry on the webhook. Entries are listed under Settings → Developer Integrations in the portal and through GET /webhooks/{id}/delivery-logs. A delivery-log entry is delivery metadata, not a copy of the event. Incident content is processed in memory while the event is built and sent, and none of it is written to the log. Entries are kept for a bounded period and expire automatically; they are not retained for the life of the account. Opening an entry in the portal, or calling GET /webhooks/{id}/delivery-logs/{logId}, shows the incident as it stands now, fetched live from the source system. It is not a snapshot of what was sent. When the incident no longer exists, the entry shows its delivery metadata and no body. See the delivery log model for the response shape.
Entries written before 2026-08-27 may still carry a stored body and response snippet until they expire or are purged.

Redelivery

A Failed entry can be sent again with POST /webhooks/{id}/delivery-logs/{logId}/redeliver?createdAt= or from the entry’s details in the portal. Because the body is not stored, redelivery rebuilds it from the source incident at the moment you redeliver.
  • The envelope keeps the original X-CF-Event-Id and occurredAt, so deduplication treats it as the same event.
  • data reflects the incident’s current state. Status, owner, severity, and alerts can differ from what the original attempt carried.
  • Only incident.created.v1 can be rebuilt. incident.closed.v1, gamebook.run.v1, and agent.investigation.completed.v1 cannot be redelivered.
When redelivery is not possible the response is a 422 with code VALIDATION_ERROR, or a 409 with code CONFLICT. The detail field says why:

Requirements for your endpoint

  • Must be reachable over HTTPS. Plain HTTP, localhost, and private network addresses are rejected at configuration time.
  • Must respond within 30 seconds. Acknowledge with a 2xx immediately and do the real work asynchronously.
  • Response bodies are read up to 1 MB and then discarded. The delivery log records the HTTP status code and latency, never the body.
  • Should default-ignore unknown values of X-CF-Schema so new event types do not break your handler.

Subscribing

Set the event types when you create or update a webhook. At least one is required.
webhook.test.v1 is not subscribable. agent.investigation.completed.v1 must be listed here and selected on an Agent Configuration classification card before it delivers anything. Delivery outcomes for every event are visible per webhook under Settings → Developer Integrations, and through GET /webhooks/{id}/delivery-logs.
Questions about webhook events? Contact us at support@contraforce.com.