Docs / Notifications / Webhooks

Webhooks

Webhooks are outbound HTTP POST requests Mesedi fires when a detector classifies an execution as a failure. Point them at Slack, PagerDuty, Discord, your on-call bot, or any receiver that accepts JSON over HTTPS. A single project can register many webhooks, each with its own URL, severity filter, class filter, and recurrence policy.

When they fire

Every ingested execution is classified against Mesedi's 20+ detectors. When a detector fires, the result is grouped into a failure group — a bucket keyed by (failure_class, signature). Two events can trigger a webhook delivery:

  • failure_group.created — the first time this specific (failure_class, signature)pair appears in the project. Always fires on every matching webhook (a brand-new failure group is interesting news no matter what the webhook's recurrence policy says).
  • failure_group.recurred — an execution matched an existing failure group. Whether this fires is governed by the webhook's recurrence_mode. See the recurrence-modes section below.

Test deliveries (the "Test webhook" button on the dashboard) use the event name failure_group.test and carry synthetic values. They exist so you can verify the receiver is reachable and your signature-verification code works.

Payload shape

The canonical JSON envelope Mesedi sends to a generic receiver:

{
  "version": "1",
  "event": "failure_group.created",
  "project_id": "proj_abc123",
  "webhook_id": "wh_def456",
  "group_id": "grp_789",
  "failure_class": "tool_failures",
  "severity": "critical",
  "signature": "sig_abcdef",
  "sample_execution_id": "exec_ghi012",
  "dashboard_url": "https://app.mesedi.ai",
  "playbook_url": "https://app.mesedi.ai/app/playbooks?class=tool_failures&signature=sig_abcdef",
  "delivery_id": "del-01a2b3c4",
  "timestamp": "2026-07-10T12:00:00Z"
}

version is bumped on breaking schema changes so your parser can switch on it. severityis the resolved severity for this event — either the per-project override you set under Settings → Severity Routing, or the detector's default. Route on it to send critical events to PagerDuty and warning-level events to a Slack channel.

Verifying deliveries (HMAC)

Every generic-receiver delivery carries an X-Mesedi-Signature header. The value is the HMAC-SHA256 of the raw request body computed with your per-webhook signing secret (shown once at webhook creation time, then hashed and never returned). Verify on your side:

# Python (Flask)
import hmac, hashlib
def verify(body_bytes, header_signature, secret):
    expected = hmac.new(
        secret.encode(),
        body_bytes,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, header_signature)

Slack, Discord, and PagerDuty adapters skip HMAC because those services either don't verify signatures or use their own auth model. See the per-receiver section below for what each one expects.

Recurrence modes (fires on repeat failures)

Every webhook has a recurrence_mode. It controls what happens the second, third, hundredth time the same failure group fires. The three modes:

off (default)

Only failure_group.created events fire. Recurrences are silently ignored. This is the low-noise default and matches how most incident-management tools expect notifications to work: one alert per novel failure, then let the human decide when to close the loop. Use this for Slack channels where you never want repeat noise. If a customer never touches recurrence config, they get this behavior.

every_event

Every occurrence fires, including recurrences. Suitable for auditing pipelines that need a durable record of every failure (a data warehouse ingest, a compliance archive). NOT recommended for chat channels — a runaway agent can generate thousands of recurrences per hour and this mode will faithfully deliver every single one. Use severity filters to narrow scope if you enable this on a chat webhook.

throttled (send window)

Fires on the first recurrence in each rolling window, then suppresses further recurrences until the window elapses. You configure the window with recurrence_window_seconds. Minimum 60s (values below 60 are floored). Typical values: 3600 (once per hour), 14400 (once per 4 hours), 86400 (daily digest). Best of both worlds for on-call — you learn about ongoing fires without getting paged every 30 seconds.

Throttle state is per (webhook_id, group_id) pair. A different failure group hitting the same webhook is not throttled against the first; each group has its own baseline. This means one runaway detector can still generate distinct alerts for distinct signatures, which is the correct behavior — the throttle exists to suppress repeats of the same problem, not to suppress unrelated problems.

The throttle baseline advances on every fired attempt, not on delivery success. If your receiver is broken during a storm we don't retry every event the receiver missed after it recovers; you get the next one that lands after the window elapses. This is a deliberate anti-flooding choice: broken receivers should not become firehose targets the moment they come back up.

Per-class filtering

By default a webhook fires on every failure class the project sees. To narrow it down, configure per-class routing under Settings → Severity Routing. That page lets you point specific classes at specific webhooks (e.g., all data_leakage events go to the security-team Slack). Because the taxonomy is 20+ classes and grows, the webhook create form no longer renders class chips inline — the settings page is the single source of truth for the mapping.

Severity filter

Each webhook can be scoped to a subset of severities (critical, warning, info). Leave the filter empty to fire on every severity. Combine severity filter with recurrence modes for a Sentry-style setup: critical + off on your PagerDuty webhook (one page per novel critical failure), warning + throttled at 1 hour on a Slack channel, and info + every_event on a low-priority audit archive.

Native receiver adapters

Mesedi detects three well-known receiver URLs and reshapes the payload into the format that service expects. Nothing for you to configure — point the webhook at the correct URL and the adapter is picked automatically.

  • Slack (hooks.slack.com/services/...): Block Kit body with a header, section fields (severity, class, signature, sample execution link), an optional playbook action, and a footer with the delivery ID.
  • Discord (discord.com/api/webhooks/...): embed with a severity-colored border, title reflecting the event kind, and inline fields for sample execution + playbook links.
  • PagerDuty (events.pagerduty.com/v2/enqueue): Events API v2 trigger event with a stable dedup_key of {project_id}:{group_id}, so recurrences update the same incident instead of creating new ones. Severity maps to PagerDuty's critical / warning / info levels; test deliveries are always forced to info to avoid paging on-call during setup. Requires the integration key — see PagerDuty setup.

Delivery, retries, and the delivery log

Each delivery attempt is recorded in your delivery log (per-webhook page on the dashboard). Retry policy:

  • Up to 3 attempts total (initial + 2 retries).
  • Backoff: 1 second, then 4 seconds.
  • Per-attempt timeout: 10 seconds.
  • 2xx = delivered. 4xx (except 408/429) = permanent failure, no more retries. 5xx / connection errors = retry.
  • Total dispatcher timeout: 60 seconds per failure group across all matching webhooks.

Every attempt (success or failure) writes a row you can inspect. A permanent 4xx from your receiver does NOT disable the webhook — we assume you meant to configure it and want to see the errors. To stop a broken webhook from generating log noise, revoke it from the webhooks page.

Next