Skip to main content
How webhooks work
Language

Webhooks

Set up webhook destinations to receive cside security alerts via HTTP POST requests with HMAC signature verification.

How webhooks work

Webhooks let you receive cside notifications as HTTP POST requests to a URL you specify. When a trigger fires, cside sends a JSON payload to your webhook endpoint containing details about the event.

You can configure webhooks in three formats:

To create Jira work items from webhooks on any plan, see Jira Automation.

Adding a webhook destination

To add a webhook as a destination in a notification config:

  1. Open the dashboard and navigate to Team Settings > Notifications
  2. Create a new notification config or edit an existing one
  3. Under Send To, click Add destination and select Webhook
  4. In the Configuration panel, fill in:
    • URL: the URL where you want to receive webhook notifications (e.g., https://example.com/cside/notify)
    • Secret: a secret key used to sign webhook requests (used for HMAC verification)
    • Format: choose JSON, Slack, or Discord
  5. Click Save or Save & Test

cside signs each request with the x-cside-signature header but cannot add custom headers. If your receiver authenticates with its own header, check whether it also accepts the token in the URL, as Jira Automation does.

The webhook payload

cside sends one HTTP POST per event, with Content-Type: application/json and an x-cside-signature header (see HMAC verification). Each payload carries a top-level event field that identifies the event type, a timestamp, the team or domain the event belongs to, and an event-specific data object.

The field that holds the timestamp differs per event: alert.created and the digest events use timestamp, while security_headers.changed uses sent_at. Read event first, then parse the rest of the payload according to the tables below. Treat unknown fields as optional so that new fields do not break your integration.

Every JSON payload also carries two human-readable fields, summary (a one-line title) and description (multi-line details), both at the top level and repeated inside data. Use them to fill in a ticket or chat message without building your own text per event. They are repeated inside data because Jira Automation treats a top-level data key specially.

Event IDs

IDSent byTimestamp fielddata object
alert.createdImmediate triggers, and the Vulnerable Script Detected trigger on its immediate scheduletimestampAlert object
security_headers.changedSecurity Headers Changedsent_atSecurity headers change
pci_report.availablePCI Report (daily or weekly)timestampPCI report
csp_block_digestScript Blocked by CSP (daily or weekly)timestampCSP block digest
csp_policy_digestCSP Policy Digest (daily or weekly)timestampCSP policy digest
vulnerable_script_digestVulnerable Script Detected (daily or weekly)timestampVulnerability digest
outbound_connections_pending_reviewOutbound Connections Pending Review (daily or weekly)timestampPending outbound connections

Only alert.created, security_headers.changed and pci_report.available honor the Slack and Discord formats. The other digest events are always sent as JSON, whatever format the destination is configured with.

Save & Test sends a sample alert.created payload with the trigger_type of the rule’s trigger, including for triggers that normally deliver a digest. Its summary starts with [Test]. Security Headers Changed sends a sample security_headers.changed payload instead. If your endpoint answers with an error, the test result shows the HTTP status code it returned.

All timestamps are ISO 8601 strings in UTC. IDs are decimal strings.

alert.created

Sent for every alert that an immediate trigger raises: Script Threat Detected, High-risk Vulnerability, Managed Flagged, Malicious Tag Manager, Malicious Outbound Connection, and Vulnerable Script Detected when the rule is set to deliver immediately.

FieldTypeDescription
event"alert.created"The event ID
timestampstringWhen the webhook was sent
domain_idstringID of the domain the alert fired on
domainstringHostname of the domain the alert fired on
trigger_typestringThe trigger that produced the alert, see the table below
summarystringOne-line title, for example [cside] Malicious Script: {ruleset} on {domain}. The same text the Jira integration uses as the issue summary
descriptionstringPlain-text explanation of the alert
dataobjectThe alert object
destination_idstringID of the webhook destination that received the event

Values of trigger_type:

trigger_typeTrigger
script_threat_detected_by_rules_engineScript Threat Detected
high_risk_vulnerability_detectedHigh-risk Vulnerability
vulnerable_script_detectedVulnerable Script Detected (immediate schedule)
cside_managed_list_flaggedManaged Flagged
malicious_tag_managerMalicious Tag Manager
malicious_outbound_connectionMalicious Outbound Connection

Alert object

FieldTypeDescription
summarystringSame as the top-level summary
descriptionstringSame as the top-level description
domainstringSame as the top-level domain
trigger_typestringSame as the top-level trigger_type
target_type"URL" | "HASH" | "IP" | "DOMAIN" | "HOSTNAME" | "VENDOR"What target holds
targetstringThe subject of the alert: a script URL, a script hash, or the flagged IP, domain, or hostname
action"alert"The action recorded for the alert. Currently always "alert"; treat it as a string to stay compatible with future actions
ruleset_idstringID of the ruleset that matched
ruleset_namestringName of the ruleset that matched
ruleset_descriptionstringDescription of the ruleset that matched
script_urlsstring[]The script URLs that triggered the alert; can be empty
vuln_cve_idstring | nullVulnerability ID (CVE or GHSA); null for non-vulnerability alerts
vuln_packagestring | nullAffected package; null for non-vulnerability alerts
vuln_severitystring | nullReported severity such as HIGH; null for non-vulnerability alerts
vuln_reference_urlstring | nullLink to the vulnerability on osv.dev; null when there is no vuln_cve_id

The four vuln_* fields are only populated for vulnerable_script_detected and high_risk_vulnerability_detected. For every other trigger_type they are null.

{
  "event": "alert.created",
  "timestamp": "2026-09-17T10:15:30.123456Z",
  "domain_id": "1897425263682514945",
  "domain": "checkout.example.com",
  "trigger_type": "high_risk_vulnerability_detected",
  "summary": "[cside] High-risk Vulnerability: CVE-2024-1234 on checkout.example.com",
  "description": "cside detected a high-risk vulnerability on checkout.example.com: lodash. Severity: High.\n\nThis is a high-risk finding (malware or a high-severity vulnerability). Investigate it promptly: if it is malware, remove the script immediately and rotate any credentials it may have accessed; otherwise prioritize updating or removing the affected dependency.",
  "data": {
    "summary": "[cside] High-risk Vulnerability: CVE-2024-1234 on checkout.example.com",
    "description": "cside detected a high-risk vulnerability on checkout.example.com: lodash. Severity: High.\n\nThis is a high-risk finding (malware or a high-severity vulnerability). Investigate it promptly: if it is malware, remove the script immediately and rotate any credentials it may have accessed; otherwise prioritize updating or removing the affected dependency.",
    "domain": "checkout.example.com",
    "trigger_type": "high_risk_vulnerability_detected",
    "target_type": "URL",
    "target": "https://cdn.example.com/lodash.js",
    "action": "alert",
    "ruleset_id": "ruleset_vulnerability_detected",
    "ruleset_name": "Vulnerability Detected",
    "ruleset_description": "A script with a known vulnerability was loaded",
    "script_urls": ["https://cdn.example.com/lodash.js"],
    "vuln_cve_id": "CVE-2024-1234",
    "vuln_package": "lodash",
    "vuln_severity": "HIGH",
    "vuln_reference_url": "https://osv.dev/vulnerability/CVE-2024-1234"
  },
  "destination_id": "1897425263682514950"
}

security_headers.changed

Sent by Security Headers Changed when the security headers observed on a domain’s checkout pages differ between two consecutive observation windows.

FieldTypeDescription
event"security_headers.changed"The event ID
sent_atstringWhen the webhook was sent
team_idstringID of the team that owns the domain
summarystring[cside] Security headers changed on {domain}
descriptionstringOne line per changed header: - {header}: {from} -> {to}
data.summarystringSame as summary
data.descriptionstringSame as description
data.domain_idstringID of the domain
data.domainstringHostname of the domain
data.checkout_pathsstring[]The checkout-page selectors that were observed
data.changesobject[]One entry per header that changed
data.changes[].header_namestringThe header, for example content-security-policy
data.changes[].from_valuestringThe previous value; empty when the header was added
data.changes[].to_valuestringThe new value; empty when the header disappeared
data.changes[].from_datestringStart of the observation window the previous value was seen in
data.changes[].to_datestringStart of the observation window the new value was seen in

pci_report.available

Sent by PCI Report once the report for the period is ready, for the scoped domains that have checkout paths configured. It is sent once per report; a rejected delivery is not retried. Each request also carries an x-cside-delivery-id header that stays the same if the same report is delivered again, so you can deduplicate on it.

FieldTypeDescription
event"pci_report.available"The event ID
timestampstringWhen the webhook was sent
organization_idstringID of the organization
team_idstringID of the team
summarystring[cside / PCI DSS]: New PCI Compliance Report for {domains} (the first three domains, then “and N more”)
descriptionstringA sentence naming the domains and dates, followed by the report link
data.summarystringSame as summary
data.descriptionstringSame as description
data.report_idstringID of the report
data.report_urlstringLink to download the report from the cside dashboard
data.domain_idsstring[]IDs of the domains the report covers
data.domainsstring[]Hostnames of the domains the report covers, in the same order
data.starting_datestringStart of the reporting period
data.ending_datestringEnd of the reporting period
data.period"daily" | "weekly" | "custom"The length of the reporting period. custom is used for a report covering any other range

csp_block_digest

Sent by Script Blocked by CSP with the Content Security Policy violation reports collected in the period. Nothing is sent when there were no violations.

FieldTypeDescription
event"csp_block_digest"The event ID
timestampstringWhen the webhook was sent
organization_idstringID of the organization
team_idstringID of the team
summarystringOne-line title for the digest
descriptionstringMulti-line breakdown of the digest
data.summarystringSame as summary
data.descriptionstringSame as description
data.period"daily" | "weekly"The rule’s schedule
data.start_datestringStart of the digest period
data.end_datestringEnd of the digest period
data.domainsobject[]Per-domain counts, ordered by violation count
data.domains[].domain_idstringID of the domain
data.domains[].domainstringHostname of the domain
data.domains[].violation_countnumberViolations recorded for the domain
data.top_blocked_urisstring[]The most frequently blocked URIs
data.top_violated_directivesstring[]The most frequently violated directives
data.total_violationsnumberTotal violations across all scoped domains

csp_policy_digest

Sent by CSP Policy Digest for each scoped domain whose recommended Content Security Policy changed in the period. Nothing is sent when no domain changed.

FieldTypeDescription
event"csp_policy_digest"The event ID
timestampstringWhen the webhook was sent
organization_idstringID of the organization
team_idstringID of the team
summarystringOne-line title for the digest
descriptionstringMulti-line breakdown of the digest
data.summarystringSame as summary
data.descriptionstringSame as description
data.period"daily" | "weekly"The rule’s schedule
data.domainsobject[]One entry per domain whose policy changed
data.domains[].domainstringHostname of the domain
data.domains[].domain_idstringID of the domain
data.domains[].addedstring[]Hosts added to the recommended policy
data.domains[].removedstring[]Hosts removed from the recommended policy
data.domains[].flaggedstring[]Removed hosts that are on cside’s managed threat list
data.domains[].host_countnumberNumber of hosts in the recommended policy
data.domains[].policy_headerstringThe full recommended Content-Security-Policy header value

vulnerable_script_digest

Sent by Vulnerable Script Detected on a daily or weekly schedule with the routine (not high-risk) vulnerability findings from the period. Nothing is sent when there were no findings.

FieldTypeDescription
event"vulnerable_script_digest"The event ID
timestampstringWhen the webhook was sent
organization_idstringID of the organization
team_idstringID of the team
summarystringOne-line title for the digest
descriptionstringMulti-line breakdown of the digest
data.summarystringSame as summary
data.descriptionstringSame as description
data.period"daily" | "weekly"The rule’s schedule
data.start_datestringStart of the digest period
data.end_datestringEnd of the digest period
data.domainsobject[]Per-domain counts
data.domains[].domain_idstringID of the domain
data.domains[].domainstringHostname of the domain
data.domains[].vulnerability_countnumberFindings recorded for the domain
data.top_vulnerabilitiesobject[]The most frequent findings
data.top_vulnerabilities[].vulnerability_idstringVulnerability ID (CVE or GHSA)
data.top_vulnerabilities[].packagestringAffected package
data.top_vulnerabilities[].severitystringReported severity
data.top_vulnerabilities[].occurrence_countnumberHow often the finding occurred
data.total_vulnerabilitiesnumberTotal findings across all scoped domains

outbound_connections_pending_review

Sent by Outbound Connections Pending Review with the number of outbound hostnames your team has not yet approved in Resources inventory. Nothing is sent when nothing is pending.

FieldTypeDescription
event"outbound_connections_pending_review"The event ID
timestampstringWhen the webhook was sent
organization_idstringID of the organization
team_idstringID of the team
summarystringOne-line title for the digest
descriptionstringMulti-line breakdown of the digest
data.summarystringSame as summary
data.descriptionstringSame as description
data.period"daily" | "weekly"The rule’s schedule
data.pending_hostname_countnumberDistinct outbound hostnames awaiting review
data.domain_countnumberNumber of scoped domains on which they were observed

HMAC verification

Every webhook request includes an x-cside-signature header containing an HMAC SHA256 hash of the request body, signed with the secret you provided when creating the webhook destination.

You should always verify this signature to confirm the request came from cside and prevent replay attacks.

How to verify the signature

If you are using JavaScript, use our JavaScript package for built-in verification.

For other languages, compute the HMAC SHA256 hash of the raw request body using your webhook secret, then compare it to the x-cside-signature header value.

async function verifyHmac(body: string, signature: string, secret: string) {
	const encoder = new TextEncoder();
	const encodedBody = encoder.encode(body);

	const key = await crypto.subtle.importKey(
		"raw",
		encoder.encode(secret),
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["sign"]
	);

	const signatureBuffer = await crypto.subtle.sign("HMAC", key, encodedBody);

	const finalSig = Array.from(new Uint8Array(signatureBuffer))
		.map((byte) => byte.toString(16).padStart(2, "0"))
		.join("");

	return signature.toLowerCase() === finalSig;
}
fn verify_hmac(body: &str, signature: &str, secret: &str) -> Result<(), &'static str> {
      let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).map_err(|_| "Invalid secret")?;
      mac.update(body.as_bytes());
      let result = mac.finalize();
      let code_bytes = result.into_bytes();
      let final_sig = hex::encode(code_bytes);

      if signature.to_lowercase() == final_sig {
          Ok(())
      } else {
          Err("Invalid signature")
      }
  }

  // Add to Cargo.toml:
  // [dependencies]
  // hmac = "0.12"
  // sha2 = "0.10"
  // hex = "0.4"
package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "strings"
)

func verifyHmac(body, signature, secret string) bool {
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(body))
	finalSig := hex.EncodeToString(h.Sum(nil))

	return strings.ToLower(signature) == finalSig
}
defmodule HmacVerifier do
	def verify_hmac(body, signature, secret) do
		final_sig =
			:crypto.mac(:hmac, :sha256, secret, body)
			|> Base.encode16(case: :lower)

			String.downcase(signature) == final_sig
	end
end
import hmac
import hashlib
import secrets

def verify_hmac(body: str, signature: str, secret: str) -> bool:
	final_sig = hmac.new(
		secret.encode('utf-8'),
		body.encode('utf-8'),
		hashlib.sha256
	).hexdigest()

	return signature.lower() == final_sig
Was this page helpful?