Safeguard.sh Documentation Center

Guard Webhooks

Deliver Guard alert events — jailbreak scores, anomaly detection, blocked-request ratios, and unreachable servers — to your own endpoint over a signed HTTP webhook.

Guard Webhooks

Guard has its own, dedicated webhook system, separate from the general-purpose Webhooks & Events pipeline that covers findings, scans, remediation, and the rest of the Safeguard platform. If you're building an integration that only cares about Guard — policy enforcement signals, jailbreak/anomaly scoring, or server reachability — register a Guard webhook directly rather than filtering the general event stream for Guard-related events.

Guard webhooks and the platform-wide webhooks described in Webhooks & Events are two different systems with different signature schemes, retry schedules, and event catalogs. Don't assume the two are interchangeable, and don't reuse verification code written for one against the other — see How this differs from the general webhook system below.

What triggers a Guard webhook

Guard webhooks fire when an alert rule matches. Alert rules watch live signals coming off your MCP servers and agents as Guard analyzes traffic, including:

SignalWhat it reflects
Jailbreak scoreHow strongly a request resembles a jailbreak / prompt-injection attempt. This is a per-request signal, distinct from the separate, on-demand tool-poisoning detection check, which analyzes a tool's own definition rather than request content.
Anomaly scoreHow unusual a request looks relative to a server or agent's normal behavior.
Blocked-request ratioThe share of recent requests your policy has blocked outright — a spike here usually means a policy is actively stopping something, or a client is misbehaving.
Server reachabilityWhether an MCP server Guard monitors has gone unreachable.
Risk scoreGuard's overall risk assessment of a request. This is the signal behind an alert rule whose trigger_type is rate_limit — despite the name, that rule type compares risk score against a threshold, not a raw request count. Don't confuse it with the rate_limit throttle you can attach to a policy rule (see Guard Policies); the two are unrelated settings that happen to share a name.

When one of these crosses the threshold configured on an alert rule, Guard fires the rule, and that firing is what dispatches the webhook. In other words, Guard webhooks don't have separate event types for "policy block" or "tool poisoning" — those conditions feed into the scoring above, and it's the resulting alert that reaches your endpoint. For how to define thresholds, severities, and which signals map to which rules, see Audit Trail & Alerts.

Every Guard webhook you register subscribes to one or more event names from its events list, or to * to receive everything. Today, alert firings are published under a single event name:

EventWhen
alert.triggeredAn alert rule's threshold condition is met for a server or agent in your org.

Registering an endpoint

Guard webhooks are configured per organization from your Safeguard workspace, alongside your Guard policies and alert rules. Each registered webhook has:

FieldDescription
NameA label to help you tell endpoints apart.
URLWhere Guard sends the signed POST request.
SecretUsed to sign every delivery to this endpoint via HMAC — see Verifying deliveries.
EventsWhich event names this endpoint receives (currently alert.triggered, or * for all Guard events).
ActiveWhether the endpoint currently receives deliveries. Deactivate an endpoint instead of deleting it if you want to pause delivery temporarily.

Each webhook record also has last_delivered_at and failure_count fields. A registered webhook, with its secret omitted, looks like this:

{
  "id": "b6e1c2b0-3e4a-4f1b-9c2d-1a2b3c4d5e6f",
  "org_id": "3fa0e6d2-2c1a-4b9b-9a2e-8e0c1d6f7a90",
  "name": "PagerDuty bridge",
  "url": "https://example.com/hooks/guard",
  "events": ["alert.triggered"],
  "is_active": true,
  "last_delivered_at": null,
  "failure_count": 0,
  "created_at": "2026-06-01T12:00:00Z",
  "updated_at": "2026-06-01T12:00:00Z"
}

The secret is deliberately not one of those fields. Like other write-only credentials in Safeguard, it's set (or reset) when you create or rotate the webhook, but it's never shown back to you afterward — not in the dashboard, not in any export. If you lose track of it, rotate the secret and update your verification code, rather than trying to retrieve the old value.

Rotate a webhook's secret if you suspect it's leaked. Because the secret is used to sign every payload, an old secret left in place after rotation will simply stop validating — there's no grace period, so update your verification code and the value stored in Safeguard at the same time.

What gets delivered

Each delivery is a JSON body describing the event, plus the alert itself:

{
  "event": "alert.triggered",
  "data": {
    "id": "b6e1c2b0-3e4a-4f1b-9c2d-1a2b3c4d5e6f",
    "org_id": "3fa0e6d2-2c1a-4b9b-9a2e-8e0c1d6f7a90",
    "rule": {
      "id": "2f8a1c3d-...",
      "name": "Jailbreak score spike",
      "trigger_type": "jailbreak_score",
      "threshold": 0.8,
      "window_seconds": 300,
      "severity": "high",
      "is_active": true
    },
    "severity": "high",
    "message": "Jailbreak score 0.91 exceeded threshold 0.80",
    "triggered_at": "2026-07-07T14:02:31Z"
  }
}
  • event is the event name — alert.triggered today.
  • data is the triggered alert: its id, the full rule object that matched (the same shape described in Audit Trail & Alerts), a top-level severity and human-readable message, and triggered_at.

The alert model has a context field reserved for per-request detail like the matching agent_id or server_id, but it isn't populated on alert.triggered deliveries today — don't build parsing logic that expects it to be present, and don't assume it can be joined back to a specific audit event by ID. For score- and ratio-based alerts (jailbreak_score, anomaly_score, blocked_ratio, rate_limit), the closest you can get is using the alert's triggered_at timestamp and severity to narrow down the relevant window in Audit Trail & Alerts — an approximate correlation by time and signal, not an exact key. For server_down alerts there's no single underlying request to look up at all; the reachability check runs independently of request traffic.

What counts as a successful delivery. Guard treats any HTTP response with a status code below 400 as delivered — 2xx and 3xx both count, so redirecting or returning 202 Accepted from an async handler is fine. Each attempt has a 15-second timeout; a slow response counts the same as a 5xx or a connection error. If your handler does real work before responding (calling out to PagerDuty, writing to a queue, etc.), acknowledge the webhook first and do that work asynchronously so a slow downstream dependency doesn't turn a successful delivery into a retried one.

Retries. A failed attempt is retried up to three more times, with increasing delay between attempts — 1 second, 5 seconds, then 30 seconds — for four attempts total (the original plus three retries). If the last retry also fails, Guard gives up on that delivery; it does not queue the event for hours or days the way the general-purpose webhook system does. Because each attempt resends the identical payload, treat data.id as an idempotency key on your end if you can't tell from your own side whether a prior attempt was actually processed before it appeared to fail.

If you have more than one active webhook subscribed to an event, Guard notifies all of them concurrently and independently — one endpoint being slow or down has no effect on delivery (or retries) to your other endpoints.

If your endpoint needs a durable record of every alert regardless of delivery outcome, treat Audit Trail & Alerts as the source of truth and your webhook as a low-latency notification on top of it.

Verifying deliveries

Every Guard webhook request carries three headers:

HeaderValue
X-Safeguard-EventThe event name, e.g. alert.triggered.
X-Safeguard-TimestampUnix timestamp (seconds) at the time of signing.
X-Safeguard-Signaturesha256=<hex hmac>, computed as HMAC-SHA256(secret, "<timestamp>." + raw_body).

Verify the signature by recomputing the HMAC over the timestamp and raw request body, and compare it to the header using a constant-time comparison:

import hmac, hashlib

def verify(secret: str, timestamp: str, signature: str, body: bytes) -> bool:
    mac = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256)
    expected = "sha256=" + mac.hexdigest()
    return hmac.compare_digest(expected, signature)
import { createHmac, timingSafeEqual } from "crypto";

export function verify(secret: string, timestamp: string, signature: string, body: Buffer): boolean {
  const mac = createHmac("sha256", secret).update(`${timestamp}.`).update(body).digest("hex");
  const expected = `sha256=${mac}`;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Always verify against the raw request body, before any JSON parsing or re-serialization — re-encoding the payload can change byte-for-byte formatting (key order, whitespace) and cause a correctly-signed request to fail verification.

Testing your verification code without waiting for a real alert

There's no synthetic "send a test event" action for Guard webhooks the way there is for the general-purpose system's safeguard webhooks test. To exercise your endpoint before a real alert rule fires, sign a sample payload yourself with the same construction Guard uses and send it with curl:

BODY='{"event":"alert.triggered","data":{"id":"test","message":"test alert","severity":"low"}}'
TS=$(date +%s)
SECRET="<your webhook secret>"
SIG="sha256=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')"

curl -i -X POST https://your-endpoint.example.com/guard-webhook \
  -H "Content-Type: application/json" \
  -H "X-Safeguard-Event: alert.triggered" \
  -H "X-Safeguard-Timestamp: $TS" \
  -H "X-Safeguard-Signature: $SIG" \
  -d "$BODY"

A test payload built this way isn't distinguishable from a real one by any field Guard adds — it's a signed request that happens to carry data you made up — so treat it as a way to validate your signature check and response handling, not as evidence a real alert rule works end to end. To confirm the whole path, including rule evaluation, trigger a real alert rule instead (for example, by sending a request that you know will exceed a low jailbreak_score or blocked_ratio threshold on a test rule).

How this differs from the general webhook system

If you already integrated with the platform-wide Webhooks & Events system, note the differences before reusing that code for Guard:

Guard WebhooksWebhooks & Events (general)
Signature headerX-Safeguard-Signature: sha256=<hex> plus a separate X-Safeguard-Timestamp headerSingle X-Safeguard-Signature: v1,t=<unix>,s=<hex> header
Event scopeGuard alert firings onlyFindings, scans, remediation, attestations, workflows, policy, assets, vendors, audit
Retries4 attempts total: the original send plus retries at 1s, 5s, and 30s, then abandonedExponential backoff over 11 attempts across 24 hours, with a dead-letter queue
Event catalogCurrently one event name, alert.triggered (or *)Many namespaced events (<domain>.<subject>.<verb>)
Per-request detail (context)Not populated today — correlate against Audit Trail & Alerts insteadFull data object scoped to the event (finding, asset, workflow run, etc.)

If you need both platform-wide events and Guard alerts in one place, register endpoints in both systems rather than assuming one covers the other.

FAQ & Troubleshooting

My endpoint isn't receiving any deliveries. Confirm the webhook is marked active, and that its events list includes alert.triggered (or *). Then confirm at least one alert rule is configured and would actually match the traffic you expect — a webhook with no matching alert rule never fires. See Audit Trail & Alerts for rule configuration.

Signature verification keeps failing. Make sure you're signing the raw request body exactly as received (no re-serialization), and that you're concatenating the timestamp header, a literal ., and the body — in that order — before computing the HMAC. Also confirm you're using the current secret; a rotated secret invalidates verification immediately with no grace period.

Can I subscribe to policy blocks directly, without going through alerts? Not as a distinct event type today — a spike in blocked requests is exposed through the blocked-ratio signal on an alert rule, so configure a rule against that threshold if you want a webhook to fire on it.

Is delivery guaranteed? No — like most webhook systems, delivery is best-effort with a small number of retries. Don't rely on a Guard webhook as your only record of an alert; use it as a low-latency trigger and treat Audit Trail & Alerts as the durable log.

My payload's data object doesn't have a context key — where did the agent/server detail go? context is reserved in the underlying alert model but isn't populated on deliveries today, so it's simply absent from the JSON rather than present-but-empty. For score- and ratio-based alerts, the alert's triggered_at and severity can narrow down a time window to check in Audit Trail & Alerts, which does carry agent_id, server_id, and per-request scores — but that's an approximate correlation by time, not a direct join, since the alert itself doesn't reference a specific audit event ID. server_down alerts aren't tied to a single request at all.

My endpoint returns 200 eventually, but Guard still logged a failed/retried delivery. Each delivery attempt times out after 15 seconds. If your handler does synchronous work — calling a downstream API, writing to a database — before responding, a slow dependency can push you past that window even though you would have eventually returned success. Acknowledge the webhook immediately with a 2xx/3xx response and do the slow work after you respond (a queue, a background job, etc.).

Is the rate_limit alert trigger the same thing as the rate_limit setting on a policy rule? No, despite sharing a name. A policy rule's rate_limit (see Guard Policies) is a request-count throttle — a limit and a time window — that's part of request enforcement. An alert rule with trigger_type: rate_limit is unrelated: it compares a request's overall risk score against a threshold. Don't assume configuring one affects the other.

I got the same alert delivered twice — is that a bug? Not necessarily. If your endpoint processed a delivery but didn't respond quickly enough (or returned a non-2xx status), Guard has no way to know the work already happened and will retry with the identical payload. Dedupe on data.id if repeated processing of the same alert would cause problems on your end.

  • Audit Trail & Alerts — configure the alert rules and thresholds that trigger Guard webhooks, and look up the request-level detail behind an alert.
  • Guard Policies — the policies whose enforcement outcomes feed Guard's blocked-ratio and scoring signals.
  • Servers & Agents — the server and agent inventory that audit events (and, indirectly, alerts) are scoped to.
  • Webhooks & Events — the general-purpose, platform-wide webhook and event system.

On this page