Safeguard
Product•

Stream Safeguard events to your systems: webhooks, the event catalog, signatures and retries

How to push Safeguard findings, scans, remediation, policy and audit events into ticketing, chat and your SIEM: the event catalog, the payload, HMAC signature checks, retries, deduplication and Guard webhooks.

Safeguard Security Team
42 min read

Safeguard publishes its state changes as named events, such as findings.vulnerability.created, scan.completed, remediation.pr_opened, policy.violation and audit.action, and delivers them as signed HTTP POST requests to endpoints you register on the Webhooks page. Each delivery carries an X-Safeguard-Signature header containing an HMAC-SHA256 over a timestamp and the raw body, so your receiver can prove Safeguard sent it, and delivery is at least once with exponential backoff and a dead-letter queue, so your receiver deduplicates by event ID. With those three pieces in place (verify, deduplicate, acknowledge fast), the same event stream can feed a ticketing system, a chat channel, an on-call tool and a SIEM without polling.

This post is for the security platform owner or integration engineer who has been asked to "get Safeguard into our systems," and for the SOC lead who will consume what arrives. It covers every outbound path Safeguard documents (platform webhooks, cloud streams, Guard webhooks, chat integrations and workflow steps), the event catalog domain by domain, the payload and headers, signature verification in Python, Node.js and Go, what the retry schedule means for your receiver, how to test without waiting for a real finding, and a staged rollout. Everything about Safeguard here comes from Safeguard's documentation at docs.safeguard.sh. Where we add a design recommendation of our own, we label it as a suggestion.

Key takeaways

  • Events are named <domain>.<subject>.<verb>. Domains include findings, scans, remediation, attestations, workflows, policy, assets and vendors, and audit. You subscribe to the event types you need and can filter server side with a subset of Safeguard's policy expression language.
  • Every delivery is signed. The header is X-Safeguard-Signature: v1,t=<unix>,s=<hex hmac sha-256>, computed over the timestamp, a literal dot and the raw request body with your subscription's signing secret. Verify it with a constant-time comparison and reject stale timestamps.
  • Delivery is at least once, not exactly once. Safeguard retries with exponential backoff over 24 hours and parks undelivered events in a dead-letter queue you can replay from the UI. Your consumer must deduplicate on X-Safeguard-Event-Id.
  • Order is guaranteed only narrowly. Within one event type, created_at increases monotonically for a given subject (a finding, an asset, a workflow run). Use it to discard stale updates; do not assume order across types.
  • Guard has its own webhook system with a different header layout, one event name (alert.triggered) and a short retry schedule. Do not reuse verification code between the two.
  • Chat integrations and workflow steps are the no-code paths. Slack, Microsoft Teams and Discord integrations post to a default channel you pick at setup; workflows can create Jira or Linear issues, page on-call and notify chat as steps.
  • There is no language SDK and no CLI command for subscriptions. Subscriptions are managed on the Webhooks page, and the verification code in this post is all a receiver needs.

Why event plumbing comes before scale

A security platform that people have to log in to and look at works for a pilot. It stops working when the program spans hundreds of repositories, several business units and a SOC that lives in its own tools. At that point the question changes from "what does Safeguard show?" to "how does what Safeguard knows reach the systems where work is assigned, tracked and audited?"

Pulling on a schedule is the usual first answer, and it has known costs. Every poll asks "anything new?" and most answers are "no." Changes land in your systems up to one polling interval late. And the polling job becomes another piece of glue someone owns, with its own credentials and failure modes. Push delivery inverts this: the source tells you when something changed, signed so you can verify who sent it.

NIST's guidance points the same way. SP 800-92, the Guide to Computer Security Log Management (September 2006), treats centralised, reliable collection of security-relevant records as a basic operational need, not an add-on. SP 800-61 Revision 3 (April 2025) frames incident response as part of cybersecurity risk management under CSF 2.0, which only works if detection signals reach the people and tools that respond. A webhook stream is how a supply chain security platform feeds both: the SIEM gets a durable record, and the ticketing and on-call systems get work items.

Which outbound path fits which job?

Safeguard documents several ways for events to leave the platform. They are not interchangeable, so pick per job.

PathWhat it carriesWhere it is configuredBest for
Platform webhooks (Webhooks and Events)The full event catalog: findings, scans, remediation, attestations, workflows, policy, assets, vendors, auditWebhooks page: endpoint URL, event types, signing secret, optional filterCustom integrations, SIEM ingestion, ticket sync, data lakes
Cloud streamsThe same events, pushed into a streamDelivery options on the Webhooks and Events pageTeams already standardised on a managed stream
Guard webhooksGuard alert firings (alert.triggered)Per organization, alongside Guard policies and alert rulesAI agent and MCP traffic alerts
Slack, Teams and Discord integrationsFindings and platform events as chat messagesSettings, IntegrationsHuman awareness in a chat channel, no code
Workflow stepsNotifications, tickets and pages as steps in a workflowWorkflows, as YAML in Git or in the UIConditional, multi-step responses with an audit trail

Two points decide most designs. First, if you need to verify the sender, deduplicate and route by your own rules, you want platform webhooks into a receiver you control. Second, if you want a response that depends on conditions (only reachable, only on main, only KEV), a workflow does that inside Safeguard and produces signed audit records for each side effect. Many teams run both: workflows for the opinionated responses, webhooks for the SIEM and the system of record.

The event catalog, domain by domain

Safeguard's Webhooks and Events page lists the catalog. Every name follows <domain>.<subject>.<verb>, which makes routing by prefix easy: a receiver can send everything under findings. to one handler and everything under remediation. to another. The tables below cover the domains most integrations use; the docs page is the full reference.

Findings

EventWhen it fires
findings.vulnerability.createdA new vulnerability finding on any asset
findings.vulnerability.severity_changedAn EPSS, KEV or CVSS update changed the severity bucket
findings.vulnerability.reachability_changedThe reachability classification was updated
findings.vulnerability.suppressedAn exception was granted or a false positive confirmed
findings.vulnerability.resolvedAn upgrade, patch or removal was verified
findings.license.createdA new license policy violation

The vulnerability events form a lifecycle, and that matters for ticket sync. created opens the work, severity_changed and reachability_changed change its priority, suppressed records a decision not to fix, and resolved closes it with verification. A ticket integration that only listens to created will leave stale tickets open forever, and one that only listens to resolved will never see the decision trail. Subscribe to the full set.

severity_changed deserves attention. Severity moves when outside data moves: a CVE added to CISA's Known Exploited Vulnerabilities catalog, an EPSS score update or a CVSS rescoring. Nothing in your code changed, but the finding's urgency did. This is exactly the change polling misses and on-call wants to hear about.

Scans

EventWhen it fires
scan.startedA scan of any asset begins
scan.completedA scan completes; the payload includes a summary of new findings
scan.failedA scan errored; the payload includes the failure reason

scan.failed is the one teams forget: a scan that silently stops running looks like a clean bill of health. Route it to whoever owns the integration.

Remediation

EventWhen it fires
remediation.plan_createdGriffin, Safeguard's security model, generated a remediation plan
remediation.pr_openedAn auto-fix pull request was opened in your repository
remediation.pr_mergedThe auto-fix pull request was merged
remediation.pr_closedThe auto-fix pull request was closed without merging
remediation.self_heal.startedA self-heal container rebuild started
remediation.self_heal.promotedA self-heal image was promoted to a registry tag
remediation.self_heal.rolled_backA self-heal rollback was triggered

These events let you measure your remediation flow in your own tools. remediation.pr_closed is worth a human look every time, because a closed fix usually means a compatibility problem or a disagreement about priority.

Attestations, workflows and policy

EventWhen it fires
attestation.createdA new attestation was published
attestation.verification_failedAttestation verification failed (admission, CI and similar)
workflow.run.startedA workflow run began
workflow.run.completedA workflow run finished successfully
workflow.run.failedA workflow run failed
policy.violationA BLOCK or WARN fired
policy.exception_grantedAn exception was approved
policy.exception_expiredA time-boxed exception expired

policy.violation is the bridge between enforcement and response. Safeguard's guardrails evaluate rules against a document that combines SBOM, vulnerability, license, signature, provenance and reachability data, with effects of BLOCK, WARN or AUTO_FIX, according to the Guardrails and Enforcement docs. When a BLOCK stops a deployment, the team that owns the service needs to know within minutes, and the event is how they find out.

policy.exception_expired is the event most programs need and few build for: when a time-boxed exception lapses, the violation is live again, and if nobody is told the first sign is a blocked release.

Assets, vendors and audit

EventWhen it fires
asset.discoveredA new asset was found (repository, image, model, workload, SBOM)
asset.ungovernedAn asset was flagged as missing an owner, an SBOM or governance
vendor.sbom_receivedA new vendor SBOM was uploaded
vendor.risk_score_changedA third party risk score crossed its threshold
audit.actionAny state-mutating action recorded in the audit log

audit.action lets your SOC correlate "who granted that exception" with the rest of its identity data. asset.ungoverned turns "we have repositories nobody owns" from a quarterly spreadsheet into a queue.

A routing map you can start from

The table below is our suggestion for a first routing map, not a product default. Adjust it to your teams.

Event (prefix or name)DestinationAction (suggested)
findings.vulnerability.created (filtered to critical and high in production)TicketingCreate an issue for the owning team
findings.vulnerability.severity_changedTicketing, on-call if KEVUpdate priority; page if it moved to KEV in production
findings.vulnerability.resolvedTicketingClose the issue with the verification note
findings.vulnerability.suppressedTicketing, SIEMComment with the decision; keep the record
scan.failedPlatform team channelAlert the integration owner
remediation.pr_opened, remediation.pr_closedTeam chatAsk for review; flag closed-unmerged fixes
policy.violationTeam chat, on-call for BLOCK in productionNotify; page on production BLOCK
policy.exception_expiredTicketingReopen the underlying finding's issue
asset.ungovernedPlatform backlogAssign an owner
audit.action, policy.*, attestation.verification_failedSIEMStore and correlate
workflow.run.failedPlatform team channelInvestigate the automation

What a delivery looks like

According to the Webhooks and Events docs, a delivery is a JSON body with a stable envelope:

{
  "id": "evt_01J...ABC",
  "type": "findings.vulnerability.created",
  "created_at": "2026-04-23T12:34:56.789Z",
  "data": { "finding": { ... }, "asset": { ... } },
  "tenant": "acme",
  "api_version": "2026-01-01"
}

Read each field for what it is for:

  • id is stable per event, so a redelivery of the same event carries the same value. It is your idempotency key.
  • type is the catalog name. Route on it.
  • created_at is an RFC 3339 style UTC timestamp with milliseconds. Within one event type and one subject, it increases monotonically, which is how you tell a newer state from an older one when deliveries arrive out of order.
  • data holds the objects the event is about, scoped to the event (for a finding event, the finding and the asset).
  • tenant identifies your Safeguard tenant.
  • api_version identifies the payload schema. Log it and alert if it changes.

Every delivery also carries four headers:

HeaderMeaning
X-Safeguard-Signaturev1,t=<unix>,s=<hex hmac sha-256>: scheme version, signing timestamp and signature
X-Safeguard-Event-IdThe event ID, for idempotency
X-Safeguard-Delivery-AttemptThe attempt count, 1 for the first delivery
X-Safeguard-Api-VersionThe payload API version

The test events Safeguard can generate carry "test": true in the payload, so your production pipeline can drop them after verifying them.

One note on documentation: the API Reference page carries a short webhook summary with a simpler event list and an older payload example. It points to the Webhooks and Events page "for signing, replay protection and the full event list," so treat that page as the reference. Our suggestion is to make your parser route on the type field, log any delivery whose shape it does not recognise, and park it rather than drop it.

Creating a subscription

Subscriptions are created on the Webhooks page in the Safeguard web app. The docs list four inputs: the endpoint URL, the event types, a signing secret and an optional filter. Subscriptions are listed, edited and deleted on the same page. There is no CLI command for creating or managing subscriptions, so if you keep integration configuration in code, record the subscription settings in your repository as documentation and apply them in the UI.

A few setup choices we suggest:

  • One subscription per consumer, not one per event type. A SIEM receiver that wants all audit.action and policy.* events should have its own subscription with its own secret. A ticketing bridge gets another. Separate secrets mean a leak in one bridge does not let anyone forge events to the other.
  • Generate the signing secret with a cryptographic random source and store it in your secret manager, not in the receiver's configuration file. RFC 2104 notes that HMAC keys shorter than the hash output length are "strongly discouraged," which for SHA-256 means at least 32 random bytes.
  • Start in staging. Point a subscription at a staging receiver first, send test events, and only then create the production subscription.

Server-side filters

Every subscription can be filtered on Safeguard's side using a subset of the policy expression language. The docs show this example:

severity in ["critical","high"]
AND env == "production"
AND finding.kev == true

Filtering at the source cuts traffic, but events you filtered out never arrive, so they are not in your SIEM either. Our suggestion: filter hard for human-facing destinations (tickets, on-call, chat) and lightly or not at all for the SIEM subscription, where completeness matters more than volume.

Verifying signatures

Without a signature check, anyone who learns your endpoint URL can post a fake findings.vulnerability.resolved and close a real ticket.

How the signature is built

The Webhooks and Events docs specify the construction:

  1. Safeguard takes the Unix timestamp t at signing time.
  2. It builds the signed message as the timestamp, a literal ., then the raw request body bytes.
  3. It computes HMAC-SHA256 over that message using your subscription's signing secret as the key, and hex encodes it as s.
  4. It sends X-Safeguard-Signature: v1,t=<t>,s=<s>.

HMAC is defined in RFC 2104 (February 1997) and standardised by NIST in FIPS 198-1 (July 2008). It combines a secret key with a hash function so that only someone holding the key can produce a valid tag for a message. Including the timestamp inside the signed message is what makes a replay guard possible: an attacker who captured an old delivery cannot change its timestamp without breaking the signature.

The four rules of a correct check

  1. Use the raw body bytes. Verify against exactly what arrived on the wire, before any JSON parsing. RFC 8259 lets JSON writers vary whitespace and member order, so a parsed and re-serialised body is not guaranteed to be byte-identical to what was signed. Many web frameworks parse JSON by default; turn that off for the webhook route.
  2. Compare in constant time. Python's documentation for hmac.compare_digest says it avoids "content-based short circuiting behaviour, making it appropriate for cryptography." Node.js's crypto.timingSafeEqual does the same, with a catch covered below. A plain == can leak how many leading characters matched.
  3. Reject stale timestamps. The docs' sample code rejects events whose timestamp is more than 300 seconds from the receiver's clock. Keep your receiver's clock synchronised, or legitimate deliveries will fail this check.
  4. Check the scheme version. The header starts with v1. Our suggestion is to reject any other version explicitly, so a future scheme change produces a clear error rather than a silent mismatch.

Python

The docs' Python sample, made a little more defensive. The hardening (version check, missing-field handling, header length cap) is our suggestion.

import hmac
import hashlib
import time

MAX_SKEW_SECONDS = 300  # matches the replay guard in Safeguard's sample

def verify_safeguard(secret: bytes, header: str, body: bytes, now: float | None = None) -> bool:
    if not header or len(header) > 512:
        return False
    version, _, rest = header.partition(",")
    if version != "v1":
        return False
    fields = {}
    for part in rest.split(","):
        key, sep, value = part.partition("=")
        if sep:
            fields[key.strip()] = value.strip()
    ts, sig = fields.get("t"), fields.get("s")
    if not ts or not sig or not ts.isdigit():
        return False
    now = time.time() if now is None else now
    if abs(now - int(ts)) > MAX_SKEW_SECONDS:
        return False
    expected = hmac.new(secret, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

In Flask, read the body with request.get_data() before touching request.json.

Node.js

Node.js documents that crypto.timingSafeEqual throws an error if the two buffers have different byte lengths. A forged header with a short signature would therefore crash a naive handler instead of returning 401. Check the length first:

import { createHmac, timingSafeEqual } from "node:crypto";

const MAX_SKEW_SECONDS = 300;

export function verifySafeguard(secret, header, rawBody, nowSeconds = Date.now() / 1000) {
  if (typeof header !== "string" || header.length > 512) return false;
  const [version, ...rest] = header.split(",");
  if (version !== "v1") return false;
  const fields = Object.fromEntries(
    rest.map((p) => p.split("=", 2)).filter((kv) => kv.length === 2)
  );
  const ts = fields.t;
  const sig = fields.s;
  if (!ts || !sig || !/^\d+$/.test(ts)) return false;
  if (Math.abs(nowSeconds - Number(ts)) > MAX_SKEW_SECONDS) return false;

  const expected = createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(sig, "utf8");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

With Express, mount the webhook route with a raw body parser so req.body is a Buffer:

import express from "express";
const app = express();

app.post("/hooks/safeguard", express.raw({ type: "*/*", limit: "1mb" }), async (req, res) => {
  const ok = verifySafeguard(process.env.SG_WEBHOOK_SECRET, req.get("X-Safeguard-Signature"), req.body);
  if (!ok) return res.status(401).end();
  await enqueue(req.get("X-Safeguard-Event-Id"), req.body); // durable queue, see below
  res.status(202).end();
});

The Node.js docs add a warning worth repeating: using timingSafeEqual "does not guarantee that the surrounding code is timing-safe." Do not branch on partial matches or log the expected signature.

Go

In Go, hmac.Equal compares MACs in constant time.

package webhook

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"math"
	"strconv"
	"strings"
	"time"
)

const maxSkew = 300 // seconds

func VerifySafeguard(secret []byte, header string, body []byte) bool {
	if len(header) == 0 || len(header) > 512 {
		return false
	}
	parts := strings.Split(header, ",")
	if parts[0] != "v1" {
		return false
	}
	fields := map[string]string{}
	for _, p := range parts[1:] {
		if k, v, ok := strings.Cut(p, "="); ok {
			fields[strings.TrimSpace(k)] = strings.TrimSpace(v)
		}
	}
	ts, err := strconv.ParseInt(fields["t"], 10, 64)
	if err != nil || fields["s"] == "" {
		return false
	}
	if math.Abs(float64(time.Now().Unix()-ts)) > maxSkew {
		return false
	}
	mac := hmac.New(sha256.New, secret)
	mac.Write([]byte(fields["t"] + "."))
	mac.Write(body)
	expected := []byte(hex.EncodeToString(mac.Sum(nil)))
	return hmac.Equal(expected, []byte(fields["s"]))
}

Read the body with io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) so an oversized request cannot exhaust memory, then verify, then parse.

Testing your verifier before a real event arrives

The docs describe synthetic test events that do not change any real state and carry "test": true. Use them once your receiver is reachable. The docs do not show where in the UI to trigger them, so check the Webhooks page in your tenant. Before that, you can exercise the verifier locally by signing a sample payload yourself with the same construction. This is our suggestion, adapted from the approach Safeguard's Guard webhook docs show for Guard:

SECRET="$(cat ./staging-webhook-secret)"
BODY='{"id":"evt_local_test_1","type":"scan.completed","created_at":"2026-11-04T09:00:00.000Z","data":{},"test":true}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -i -X POST http://localhost:8080/hooks/safeguard \
  -H "Content-Type: application/json" \
  -H "X-Safeguard-Signature: v1,t=$TS,s=$SIG" \
  -H "X-Safeguard-Event-Id: evt_local_test_1" \
  -H "X-Safeguard-Delivery-Attempt: 1" \
  -d "$BODY"

Then run the negative cases: an altered body, a timestamp ten minutes old, a v2, prefix and a truncated signature should each return 401, never a crash. A self-signed request proves your verifier works, not that the subscription is configured correctly; Safeguard's test events cover that.

Rotating the signing secret

The Webhooks and Events docs say a subscription has a signing secret, but do not describe a dual-secret rotation window. Safeguard's Guard webhook docs, for the separate Guard system, say a rotated secret invalidates verification immediately with no grace period. Plan for the same behaviour unless the docs say otherwise. A pattern we suggest for zero-downtime rotation on platform webhooks:

  1. Create a second subscription to the same receiver with the same event types and filter, and a new secret.
  2. Teach the receiver to accept either secret (try the new one, then the old one).
  3. Confirm deliveries verify with the new secret on the Webhooks page.
  4. Delete the old subscription, then remove the old secret from the receiver.

During the overlap the receiver gets each event twice. That is harmless, because the receiver already deduplicates by event ID.

Retries, duplicates and ordering

The reliability section of the Webhooks and Events docs is short and every line of it shapes your receiver.

What Safeguard does

  • At-least-once delivery. "Consumers must handle duplicates by X-Safeguard-Event-Id."
  • Exponential backoff. The docs describe 11 attempts over 24 hours and list the steps as 1s, 5s, 30s, 1m, 5m, 15m, 1h, 3h, 6h, 12h and 24h.
  • A dead-letter queue. Events that are never delivered land in a DLQ that you can replay from the UI.
  • Per-endpoint observability. The Webhooks page shows delivery attempts (success, retry, failed), average latency, DLQ size, and recent deliveries with request and response bodies, scrubbed of secrets.

What that means for your receiver

Expect duplicates and make them harmless. At-least-once is the normal contract for event delivery, not a Safeguard quirk. The CloudEvents specification, a CNCF project, says producers must make source plus id unique for each distinct event and that a re-sent duplicate may carry the same id. The Amazon Kinesis Data Streams developer guide tells applications to "anticipate and appropriately handle processing individual records multiple times." Google Cloud Pub/Sub offers an exactly-once feature precisely because its default delivery can redeliver. The fix is the same everywhere: record each processed event ID and skip repeats.

Keep dedupe records longer than the retry window. Read literally, the listed backoff steps add up to more than a day if each is a gap between attempts. Our suggestion is to keep processed event IDs for at least several days, and for as long as you might replay from the DLQ. A table keyed on the event ID with a timestamp column and a scheduled cleanup is enough.

Acknowledge fast, process later. The general webhook docs do not state a per-attempt timeout or which status codes count as success. Guard's separate webhook system treats any status below 400 as delivered and times out each attempt after 15 seconds. Our suggestion for platform webhooks is to verify the signature, write the event to a durable queue, return a 2xx immediately, and do the ticketing or SIEM call from a worker. A receiver that calls a slow ticketing API inline turns a slow dependency into retries, and retries into duplicates.

Return honest status codes. 401 for a bad signature, 2xx once queued, 5xx if your queue is down so the event is retried. RFC 9110 defines Retry-After and RFC 6585 defines 429 Too Many Requests, but Safeguard's docs do not say whether its sender reads them, so do not rely on them.

Use the attempt header for monitoring, not logic. X-Safeguard-Delivery-Attempt greater than 1 means an earlier attempt failed or timed out. Count those in your metrics. A rising rate of second attempts is an early warning that your receiver is slow, well before anything lands in the DLQ.

Respect the narrow ordering guarantee. Within one event type, created_at increases monotonically for a given subject. That means two findings.vulnerability.severity_changed events for the same finding can be ordered by created_at, even if a retry delivers the older one second. Store the last created_at you applied per subject and event type, and ignore anything older. Across different types there is no ordering guarantee, so a resolved can arrive before the severity_changed that preceded it. Our suggestion is to make the resolved handler win, so a resolved finding is not reopened by a late severity update.

A dedupe and ordering sketch

CREATE TABLE sg_processed_events (
  event_id     TEXT PRIMARY KEY,
  event_type   TEXT NOT NULL,
  received_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE sg_subject_state (
  subject_key  TEXT NOT NULL,   -- for example the finding id from data
  event_type   TEXT NOT NULL,
  last_created TIMESTAMPTZ NOT NULL,
  PRIMARY KEY (subject_key, event_type)
);

The worker inserts into sg_processed_events with ON CONFLICT DO NOTHING and stops if no row was inserted. It then upserts sg_subject_state only when the incoming created_at is newer, and applies the change only in that case. Both tables are our suggestion; field names inside data should come from the payloads you actually receive, not from this post.

Draining the dead-letter queue

After an outage on your side, fix the receiver, check DLQ size on the Webhooks page and replay from the UI. Because the receiver deduplicates and orders per subject, an overlapping replay is safe. Our suggestion: alert whenever the DLQ is not empty.

Streams instead of HTTP

Some teams would rather consume a stream than run an HTTP receiver. The Webhooks and Events docs list these delivery options:

TargetHow the docs describe it
AWS Kinesis Data Streams or FirehoseDirect integration
Azure Event HubsDirect integration
Google Cloud Pub/SubDirect integration
Confluent Cloud or Apache KafkaVia webhook sink
NATSVia webhook sink
Redis StreamsVia webhook sink

"Via webhook sink" means the events still arrive as HTTP webhooks, at a sink component that writes them into your stream. Whichever path you use, the delivery semantics of the stream are added to Safeguard's. Kinesis documents duplicates from both producer and consumer retries, Pub/Sub redelivers unless exactly-once is enabled on the subscription, and Event Hubs keeps events for a time-based retention period so consumers can re-read. So the dedupe-by-event-ID rule applies at the consumer of the stream too.

Our suggestion for a webhook sink: verify the signature in the sink, then write the raw body plus the headers you need (X-Safeguard-Event-Id, X-Safeguard-Api-Version) as the record, keyed on the event ID. Downstream consumers then never need the signing secret, and anything in the stream is known to be verified.

Guard webhooks are a separate system

Safeguard Guard, which applies your policy to MCP server and AI agent traffic, has its own webhook system for alert firings. The Guard Webhooks docs are explicit: the two systems have "different signature schemes, retry schedules, and event catalogs," and verification code written for one should not be reused for the other.

What Guard sends

Guard webhooks fire when an alert rule matches. Alert rules watch signals such as a jailbreak score, an anomaly score, the blocked-request ratio, server reachability and an overall request risk score. Today these firings use one event name, alert.triggered, and a webhook can subscribe to it or to *. Each registered endpoint has a name, URL, secret, event list and an active flag, and records last_delivered_at and failure_count. The secret is write-only: it is never shown again after you set it, so if you lose it you rotate it.

The body carries event and a data object with the alert's ID, the full rule that matched, a severity, a human-readable message and triggered_at. The docs say not to expect per-request detail such as the agent or server ID in the payload; correlate with Guard's Audit Trail and Alerts by time and signal instead.

How Guard signs and retries

Guard webhooksPlatform webhooks
Signature headerX-Safeguard-Signature: sha256=<hex> plus a separate X-Safeguard-TimestampOne header, X-Safeguard-Signature: v1,t=<unix>,s=<hex>
Signed message<timestamp>. followed by the raw body<t>. followed by the raw body
Event headerX-Safeguard-EventX-Safeguard-Event-Id (plus attempt and API version headers)
SuccessAny status below 400; 15 second timeout per attemptNot stated in the docs; we suggest a fast 2xx
RetriesFour attempts in total: the original plus retries after 1s, 5s and 30s, then abandonedExponential backoff over 24 hours, then the DLQ
Idempotency keydata.idX-Safeguard-Event-Id
Durable recordAudit Trail and AlertsDLQ replay, plus your own store

The short retry schedule is the important difference. If your Guard receiver is down for a minute, those alerts are not redelivered later. The Guard docs say to treat the webhook as a low-latency trigger and the Audit Trail as the durable log. Our suggestion: if Guard alerts must reach your SIEM completely, also export from the audit trail on a schedule, and use the webhook only for paging.

Guard has no synthetic test event; its docs show signing a sample payload with openssl, then triggering a real low-threshold rule to prove the full path.

Chat integrations: the no-code path

If the goal is "the team sees it in chat," you may not need a receiver at all. Safeguard's Slack, Microsoft Teams and Discord integrations are configured under Settings, Integrations, in the ESCM, Portal and TPRM products.

PlatformAuthentication optionsWhere messages go
SlackBot Token (xoxb-...) or Incoming WebhookOne default channel picked at setup (bot) or the webhook's bound channel
Microsoft TeamsIncoming Webhook or Bot Framework (tenant ID, app ID, app secret)One default team and channel (Bot Framework) or the webhook's bound channel
DiscordBot Token or WebhookOne default channel in a chosen server (bot) or the webhook's bound channel

Credentials are validated before any channel picker appears, and bad credentials stop the flow with an inline error. The most common snag, per the docs, is a Slack bot that has not been invited to any channel, so the picker is empty.

Every chat integration sends to its single default channel. Teams is notification-only. If you need "criticals to the on-call channel, everything else to the team channel," the docs point to filtering upstream through webhook filters. In practice that means either several chat integrations fed by workflows with conditions, or a platform webhook into your own small router. Our earlier post on Safeguard alerts in Slack, Microsoft Teams and Discord covers chat setup and noise control in depth; this post stays on the event plumbing underneath.

Workflows: Safeguard's own consumer of these events

The Webhooks and Events docs describe Workflows as "Safeguard's own consumer of these events." A workflow is a YAML definition with a trigger, ordered steps, step-level if conditions and a concurrency limit. It lives in a Git repository or the Safeguard UI and runs on Safeguard's managed cloud runner or a self-hosted runner inside your network.

Workflow triggers include on.vulnerability.published, on.policy.violation, on.image.pushed, on.repo.pushed, on.sbom.received, on.vendor.sbom_updated, on.schedule (cron in UTC) and on.manual. The built-in step library includes notification steps (slack-notify, teams-notify, email-owners, pagerduty-trigger) and ticket steps (jira-create, jira-comment, linear-create). This is the docs' own example of a response to a blocked production admission:

This is a Safeguard Workflows definition. It runs in Safeguard's runner, not in GitHub Actions.

apiVersion: safeguard.sh/workflows/v1
kind: Workflow
metadata:
  name: prod-admission-gate
spec:
  on:
    policy.violation:
      filters:
        policy: prod-admission-policy
        decision: BLOCK
  steps:
    - id: open-ticket
      uses: safeguard/jira-create@v2
      with:
        project: SEC
        summary: "Blocked production admission: ${{ trigger.image }}"
        severity: ${{ trigger.severity }}
    - id: page-oncall
      uses: safeguard/pagerduty-trigger@v1
      with:
        service: prod-admission-failures
      if: ${{ trigger.severity == 'critical' }}

Every run produces a step timeline, structured logs, OpenTelemetry spans to your configured OTLP endpoint, and audit records signed by Safeguard for every external side effect, such as a ticket created or a notification sent. Workflow runs also emit workflow.run.started, workflow.run.completed and workflow.run.failed as platform events, so your SIEM can watch the automation itself.

When should you use a workflow and when a webhook? Our rule of thumb: if the response is a Safeguard action followed by a notification (scan, check reachability, open a fix pull request, tell the team), use a workflow. If the destination is a system Safeguard does not have a step for, or you need your own enrichment, ordering and storage, use a webhook. The Workflows documentation covers triggers, steps, runners and promotion between environments in full.

Pull when you need to ask a question

Webhooks answer "tell me when something changes." Some integrations also need to ask "what is the current state?" For that, Safeguard's API Reference names the hosted MCP server at https://mcp.safeguard.sh/mcp as the supported programmatic surface. Clients connect by URL and sign in with OAuth, or send an MCP key created under Settings, Developer as a bearer token. Its read-only tools list products, projects, vulnerabilities and findings, and fetch a single project or CVE record, with scope applied from the account you connect with.

A pattern that combines both: the webhook tells your ticket bridge that findings.vulnerability.severity_changed fired, and an agent with read-only MCP access fetches the current vulnerability record to write a better ticket description. Safeguard publishes no language SDK, and its SDKs page says to treat any package that claims to be a Safeguard SDK as untrusted. For receivers that is no loss, because signature verification needs only the standard library code shown above.

If you also run AI coding agents, the companion post on sending Claude Code and Codex telemetry to Safeguard with OpenTelemetry covers the opposite direction: data flowing into Safeguard.

Step by step: build a production receiver

This is a staged plan we suggest, assuming an HTTP receiver behind your normal ingress, a durable queue and a small worker.

  1. Decide the consumers. List each destination (SIEM, ticketing, on-call, data lake) and the event types it needs, using the routing map above. One subscription per consumer.
  2. Stand up the receiver in staging. It does four things only: enforce HTTPS and a body size limit, verify the signature against the raw body, write the raw body and key headers to a durable queue keyed on the event ID, and return 2xx. Bad signatures get 401, with nothing secret logged.
  3. Unit test the verifier with the self-signed curl request and every negative case above.
  4. Create the staging subscription on the Webhooks page: endpoint URL, event types, a new signing secret from your secret manager, and a filter if this consumer needs one.
  5. Send Safeguard test events. Confirm they verify, reach the queue and are dropped by the worker because of "test": true, and that the Webhooks page shows first-attempt success.
  6. Build the worker. It deduplicates on the event ID, applies the per-subject ordering check, routes on type and calls the destination. Destination failures retry inside your worker, never as failed deliveries to Safeguard.
  7. Make ticket sync idempotent at the destination too. Store the Safeguard finding identifier in the ticket's external ID or a custom field, search before creating, close on resolved, comment on suppressed, reopen on policy.exception_expired. This keeps ticket counts honest after a DLQ replay.
  8. Ship the SIEM feed. Send audit.action, policy.*, attestation.verification_failed, scan.failed and workflow.run.failed with the raw payload, the API version and a "signature verified" flag. SP 800-92's advice on log integrity applies: the SIEM copy is the one nobody should be able to quietly edit.
  9. Monitor the plumbing. Alert on a non-empty DLQ, a rising share of deliveries with attempt greater than 1, spikes in signature failures (a rotated secret or someone probing the endpoint) and unknown type or api_version values.
  10. Go to production and widen. Create the production subscription, watch the Webhooks page for a week, then add consumers one at a time.

Securing the receiver itself

A webhook receiver is an internet-facing service that can create tickets, page people and write to your SIEM, so treat it like any other production service. Serve it over HTTPS only, because payloads describe your vulnerabilities. Load the signing secret from a secret manager and keep it out of logs and crash reports. Cap body and header size before verification, and do not parse, look up or log anything from the payload until the signature passes. Give the worker's ticketing token rights to create and update issues in the right projects and nothing more. And keep the receiver's own dependencies current: its web framework, JSON parser, HTTP client and base image are open source components too, and a vulnerable one is a door into every system the receiver writes to.

Common mistakes

  • Verifying a re-serialised body. The framework parsed the JSON, the code re-stringified it, and every signature fails. Verify the raw bytes.
  • Comparing signatures with ==, or letting timingSafeEqual throw on a short signature. Use a constant-time compare with a length check and return 401.
  • Skipping the timestamp check. Without it, a captured delivery can be replayed forever.
  • Processing inline. Calling the ticketing API before responding turns a slow dependency into retries and duplicates. Queue, then acknowledge.
  • No deduplication. At-least-once delivery means duplicate tickets and duplicate pages unless you dedupe on the event ID.
  • Assuming global order. Only per-subject, per-type order is guaranteed. We suggest that a late severity_changed should not reopen a resolved finding.
  • Listening only to created. Tickets never close and suppressions leave no trail.
  • One secret for every consumer, so a leak in one bridge lets an attacker forge events to all of them.
  • Reusing platform verification code for Guard. Different headers, retries and idempotency key.
  • Never looking at the DLQ after an outage.
  • Installing a package that claims to be a Safeguard SDK. Safeguard publishes none.

FAQ

How do I verify a Safeguard webhook signature?

Read the raw request body, split X-Safeguard-Signature into its v1, t and s parts, reject the request if t is more than 300 seconds from your clock, compute HMAC-SHA256 over t, a literal dot and the raw body using your subscription's signing secret, hex encode it, and compare it to s in constant time.

Does Safeguard retry failed webhook deliveries?

Yes. Platform webhooks use exponential backoff over 24 hours, with steps from 1 second up to 24 hours listed in the docs, and undelivered events go to a dead-letter queue you can replay from the UI. Guard webhooks retry only three times, after 1, 5 and 30 seconds, and then give up.

Can I receive the same Safeguard event twice?

Yes. Delivery is at least once. Deduplicate on X-Safeguard-Event-Id, which is stable across retries of the same event.

Are Safeguard webhook events delivered in order?

Only within one event type for a given subject, where created_at increases monotonically. Across types there is no ordering guarantee, so compare created_at per subject and type and, as our suggestion, let terminal states such as resolved win.

Which Safeguard events should go to a SIEM?

A sensible starting set is audit.action, all policy.* events, attestation.verification_failed, scan.failed and workflow.run.failed, plus the vulnerability lifecycle events if your SOC correlates them with other data. Filter the SIEM subscription lightly so the record stays complete.

How do I send Safeguard findings to Jira or ServiceNow?

Two ways. A workflow can create Jira or Linear issues as a step, with conditions on severity and reachability. For other ticketing systems, or for full lifecycle sync, subscribe a webhook receiver to the findings.vulnerability.* events and create, update and close tickets from a worker, keyed on the finding identifier.

Can I filter which events a webhook receives?

Yes. Each subscription can carry a server-side filter written in a subset of Safeguard's policy expression language, for example severity, environment and KEV status.

Is there a Safeguard SDK or CLI command for webhooks?

No. Subscriptions are created, edited and deleted on the Webhooks page, and Safeguard publishes no language SDK. Signature verification needs only your language's standard HMAC functions.

How do I test a Safeguard webhook endpoint?

Use Safeguard's synthetic test events, which carry "test": true and do not change real state. Before your endpoint is reachable, sign a sample payload yourself with openssl and post it with curl to test the verifier, including the failure cases.

Fixing what the events tell you

Events tell you quickly that something is wrong; they do not fix it. Most of what arrives on findings.vulnerability.created is an open source dependency or a container image with a known flaw, and the receiver you just built, with its web framework, JSON parser and base image, is made of the same kind of components.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Griffin, Safeguard's security model, opens a policy gated pull request that moves the affected dependency to the safe version, and your own CI runs your tests on it before anyone merges. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Auto-merge is a separate switch and stays off by default, so your team decides what merges on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.

3. Gold Registry: start from components that are already fixed

Safeguard Gold rebuilds patched versions of open-source packages and images from source, signs each build, attaches an SBOM, SLSA provenance and VEX, and serves them from registry.safeguard.sh to your organization. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened build of a specific package. Ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

Webhooks push events out; sometimes a person or an agent needs to ask a follow-up question about the finding behind an event. This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude may use, the same read-only path a ticket bridge can use to enrich what a webhook delivered.

Click through it at your own pace, or open it full screen.

The second demo is a real Safeguard CLI session. A developer asks the assistant to fix a lodash CVE; it reads the finding and advisory, asks before any command that changes something, updates the dependency, runs the install and tests with approval, and pushes nothing. It is what a developer does after a findings.vulnerability.created ticket lands in their queue.

Click through it at your own pace, or open it full screen.

The third demo sets up the local runner on your own machine and scans from the terminal. Jobs run where the code lives and only the SBOM and findings go back, and those findings then flow out through the same event stream to your systems.

Click through it at your own pace, or open it full screen.

To try Safeguard yourself, download the apps or read the Webhooks and Events documentation. For every other system Safeguard connects to, see every Safeguard integration explained.

Quick check

Your receiver parses JSON, then verifies the signature over JSON.stringify of the parsed body. Most deliveries fail. Why?

The signature covers the raw bytes Safeguard sent. Re-serialising can change whitespace and member order, so the bytes differ. Verify against the raw body before parsing.

The same findings.vulnerability.created event arrives twice, with X-Safeguard-Delivery-Attempt 1 and then 2. Is something broken?

Not necessarily. Delivery is at least once, so a slow or failed first response leads to a retry with the same event ID. Deduplicate on X-Safeguard-Event-Id, and look at why the first attempt failed or was slow.

A Guard alert receiver was down for two minutes. Will the alerts be redelivered later from a dead-letter queue?

No. Guard webhooks retry after 1, 5 and 30 seconds and then give up. Treat Guard's Audit Trail and Alerts as the durable record and use the webhook for low-latency paging.

Sources

Last reviewed: 27 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

We email you a link to confirm, and only then add you to the list. Unsubscribe in one click. Privacy

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.