Skip to content

Notifications and webhooks

Sift’s alerts leave the deployment only through webhook endpoints you add under the gear icon (Administration) › Notifications. You need the admin role. With none configured, nothing is sent anywhere and alerts are shown only in the console. Each alert is sent once when it begins and once when it ends, signed with the endpoint’s secret. This page covers adding endpoints, what Sift sends, and how a receiver verifies it.

An alert says which filter or part of the deployment is affected, the condition, and a link back to the console. It never carries an event, a score, a verdict, a filter’s intent, or anything an agent wrote. See Health, Actions and alerts for the conditions themselves.

The top panel shows the deployment settings every endpoint is held to. They are environment settings, read-only here, so that a stolen admin session cannot widen them.

RuleSettingDefault
SchemeHTTPS only. http is accepted only when SIFT_ENVIRONMENT=development.HTTPS only
Private networksSIFT_WEBHOOK_PRIVATE_NETWORKS: comma-separated private (RFC 1918, fc00::/7) or CGNAT (100.64.0.0/10) ranges an endpoint may reach, for an on-premises receiver.None: public addresses only
LinksSIFT_CONSOLE_URL: the console’s address as people reach it, such as https://sift.example.com. Unset, every link in an alert is empty.Unset
Deployment nameSIFT_DEPLOYMENT_NAME: names this deployment in every alert, for a receiver that hears from several.Unset

Loopback, link-local and cloud metadata addresses are refused whatever SIFT_WEBHOOK_PRIVATE_NETWORKS says, and so is localhost. A delivery that was allowed only because of that setting is marked private network. Set these in the environment; see Configure Sift.

You can have at most 10 endpoints.

  1. In Add an endpoint, enter a Name (unique, up to 100 characters).
  2. Enter the URL (up to 2,000 characters).
  3. If the receiver authenticates by a static header, fill in Header (optional): a name (letters, digits and hyphens) and a value.
  4. Select Generate beside Signing secret, or paste your own (16 to 256 characters). Select Copy and put the secret in your receiver now.
  5. Under Sends, choose what this endpoint hears (at least one).
  6. Leave Include filter names off unless the receiver should see them.
  7. Leave Enabled on. A disabled endpoint is sent no alerts, though you can still send it a test.
  8. Select Save endpoint.

Sift checks the URL when you save (its host must resolve, within 5 seconds, to an address a webhook may reach) and again before every delivery. The page then shows “Saved {name}.” and clears the URL, header and secret from the page.

  1. Select Send a test on the new endpoint.

You should see “The receiver accepted the test ({status}).”

Choice under SendsAPI nameWhat it covers
Filter faultsfilter_faultA filter is not doing what the console says it is: deliveries failing, polling failing, an alarm while it keeps filtering, a held queue losing events, a filter Sift moved to its safer point.
Filters waiting on someonefilter_attentionA gold set to review, a threshold to approve, a filter that fell back to shadow, an agent budget at its bound or a held queue filling.
DeploymentsystemA deployment warning, such as a scoring backlog or agent runs waiting on the Kindo rate limit, and the Kindo connection failing or degraded.

New endpoints start with Filter faults and Deployment.

A filter’s name is text someone typed, such as “CEO mailbox rules”, and the receiver is outside the deployment. Without Include filter names an alert names the filter by its id (flt_…) only.

Each endpoint card shows its URL (redacted), Sends, Filter names, Header, Signing secret, who created it, and its Last delivery. Badges show enabled or disabled, Kindo agent, and private network.

ButtonWhat it does
EditChange the name, URL (leave empty to keep it), header (or remove it), what it sends, filter names, and Enabled. Turning an endpoint off abandons everything queued for it; nothing is held for later.
Rotate secretEnter a new signing secret. For 24 hours, every delivery carries two signatures, one under the new secret and one under the old, so you can move the receiver over without missing an alert. Then only the new one signs.
Send a testSends a webhook.test now, once, with no retry. Works on a disabled endpoint too, so you can check one before enabling it.
DeliveriesThe last 100 deliveries: Queued, Event, Alert (code and subject), Status (pending, retrying, delivered, abandoned, and when the next attempt is due), Attempts, Answer (the receiver’s status and Sift’s explanation; never the receiver’s body).
DeleteRemoves the endpoint and abandons anything queued for it. Its delivery log stays for 30 days under its name.

Deliveries are kept 30 days. The raise of an alert that is still open is kept until it clears, so the clear can follow it.

Sift evaluates the console’s alerts every minute, and sends queued deliveries every 15 seconds.

  • A condition is raised once it has been present for 2 minutes. One that comes and goes inside 2 minutes is never sent.

  • It is cleared once it has been absent for 10 minutes. One that returns inside that window stays open.

  • A raise goes to every enabled endpoint subscribed to it. A clear goes only to the endpoints its raise was sent to, so no receiver hears that something ended without having heard it begin.

  • A retired or deleted filter’s alerts clear at once.

  • A filter sends one alert, for the condition that leads its health verdict, except for the agent’s load. Each of these is an alert of its own, even behind another condition, raised and cleared on its own:

    CodeSeveritySent when
    queue.losingfaultThe agent budget’s held queue lost escalations in the last hour: aged out, or pushed out of a full queue.
    loosened.driftfaultSift moved the filter to its safer point on a drift alarm.
    budget.boundattentionThe budget is spent and escalations wait for room.
    queue.fillingattentionThe held queue is near its depth or age limit.
    budget.record_onlyattentionUnder Record only, escalations over the budget were recorded and not sent.
    load.unboundedattentionNo budget, and agent runs in shadow or proposed were over the agent’s capacity for 2 hours. A filter with none of the agent-load settings can send it.

    budget.near (runs near the budget) is sent only when it leads the verdict, so a filter that routinely runs near its budget does not page anyone. The deployment’s agent_runs_backlogged warning is sent like any other warning. See States, badges and health codes.

  • An endpoint added later hears of changes from then on, not of alerts already open.

ConditionWhy
broker_unreachable, worker_pool_silentSift sends alerts through the same background services these warnings report on. When those are down, nothing can be sent.
escalations_undelivered, escalations_failing, cutover_incomplete, cutover_unverified, deliveries_overdueEach restates a filter’s own fault, which is sent as that filter’s alert.
decision_model_directA notice about a deliberate configuration, not a fault.

Because those services cannot announce their own failure, monitor Sift from outside as well: see Monitor Sift.

Each attempt is a POST with a 10-second limit (5 seconds to resolve, 5 to connect). Sift does not follow redirects, does not use a proxy (HTTPS_PROXY is ignored), and reads at most 64 KB of the answer.

Receiver’s answerWhat Sift does
Any 2xxDelivered. Also brings that endpoint’s waiting retries forward to now, since the receiver is back.
408, 429, any 5xx, a timeout, a connection failure, a host that did not resolveRetried after 1, 5 and 15 minutes, then 1, 3, 6 and 12 hours. Abandoned after the 8th attempt.
A 3xxAbandoned: “a redirect, which Sift does not follow.”
The host now resolves to an address a webhook may not reachAbandoned without sending (“Refused before sending: …”).
The URL, header or secret no longer decrypt (SIFT_SECRET_KEY changed)Abandoned without sending.
Any other status (such as 400, 401, 404)Abandoned at once: another attempt would not change it.

Deliveries to one endpoint about one alert go in order, so a clear never arrives before its raise. A failing delivery holds back only its own alert’s later deliveries.

When a delivery has been retrying for more than 15 minutes, or one was abandoned in the last day, Sift raises the webhook_failing warning (“{n} webhook endpoint(s) are not receiving alerts…”); admins get an Open Notifications link on the alerts page.

Every delivery is Content-Type: application/json with these headers:

HeaderValue
X-Sift-Signaturet=<unix seconds>,v1=<hex>, with a second v1= during a rotation’s 24 hours.
X-Sift-Event-IdThe event’s id, equal to the body’s id. The same on every retry of a delivery: use it to discard duplicates.
User-Agentkindo-sift/<version>
Your static headerIf you set one. It cannot replace any of the above, Host, Content-Type, Content-Length, Transfer-Encoding or Connection.

An example raise, for a filter whose source stopped polling, sent to an endpoint without filter names:

{
"schema": "sift.alert.v1",
"id": "dc13a185-a487-5299-8195-851a4a1f54ff",
"type": "alert.raised",
"occurred_at": "2026-10-14T09:42:10Z",
"deployment": {
"name": "prod-east",
"console_url": "https://sift.example.com"
},
"alert": {
"subject": "filter",
"filter_id": "flt_phish01",
"filter_name": null,
"filter_state": "active",
"code": "polling.failing",
"severity": "fault",
"summary": "The source has failed to poll 5 times in a row, so nothing new is being scored.",
"action": "check_polling",
"action_label": "Check the source",
"since": "2026-10-14T09:15:03Z",
"link": "https://sift.example.com/filters/flt_phish01"
},
"text": "[Sift prod-east] flt_phish01 fault: The source has failed to poll 5 times in a row, so nothing new is being scored. Check the source: https://sift.example.com/filters/flt_phish01"
}

Sift sends it compact, with no spaces between keys and values; it is indented here to read.

FieldMeaning
schemaAlways sift.alert.v1. The fields never change under this name; a new field would be a new schema.
idThe event id, stable across retries.
typealert.raised, alert.cleared or webhook.test.
occurred_atFor a raise, when the condition was first seen; for a clear, when it was first seen gone; for a test, when it was sent. UTC.
deployment.name, deployment.console_urlSIFT_DEPLOYMENT_NAME and SIFT_CONSOLE_URL, or null.
alertnull for a test.
alert.subjectfilter, system (a deployment warning) or kindo (the Kindo connection).
alert.filter_id, alert.filter_stateFor a filter alert; otherwise null.
alert.filter_nameOnly for an endpoint with filter names included; otherwise null.
alert.codeThe condition, stable while it holds: a filter health code such as polling.failing or delivery.failing, a deployment warning code such as scoring_backlog, or kindo.failing / kindo.degraded. See States, badges and health codes.
alert.severityfault or attention. A deployment warning that is critical is a fault.
alert.summarySift’s own sentence about the condition, as the console shows it. It can name agent ids and tool names.
alert.action, alert.action_labelWhat to do next, as a code and as the console’s words. May be null.
alert.sinceWhen the underlying problem started, where Sift knows (for polling, the last successful poll); otherwise when Sift first saw the condition.
alert.linkThe filter’s page, or the console’s alerts page; null without SIFT_CONSOLE_URL.
textOne readable line, for a chat receiver or an agent.

A clear has the same shape with "type": "alert.cleared" and a text such as [Sift prod-east] flt_phish01 cleared: polling.failing ended 2026-10-14T10:05:00Z. <link>. A test has "alert": null and the text [Sift prod-east] Test notification: webhook delivery works. This carries no alert. Without SIFT_DEPLOYMENT_NAME, every text starts [Sift].

The signature is HMAC-SHA256, keyed with the secret’s UTF-8 bytes, over the timestamp, a full stop, and the exact request body: "<t>.<body>". The hex digest is the v1 value. Verify against the raw bytes you received, before parsing the JSON. Each attempt is signed afresh with the time it was sent, so you can reject old timestamps to stop replays.

import hashlib
import hmac
import time
TOLERANCE_S = 300 # how old a delivery may be; your choice
def verify(body: bytes, signature_header: str, secret: str) -> bool:
"""True if X-Sift-Signature matches the raw request body under secret."""
timestamp, signatures = None, []
for part in signature_header.split(","):
key, _, value = part.strip().partition("=")
if key == "t":
timestamp = value
elif key == "v1":
signatures.append(value)
if timestamp is None or not timestamp.isdigit() or not signatures:
return False
if abs(time.time() - int(timestamp)) > TOLERANCE_S:
return False
signed = timestamp.encode() + b"." + body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, candidate) for candidate in signatures)

Accept the delivery if any v1 matches, which is what lets a rotation overlap. Then use X-Sift-Event-Id to ignore a delivery you have already handled: after a timeout Sift may send one your receiver did process.

An endpoint can be a Kindo agent’s webhook trigger, so an agent runs on each alert. Its URL has the shape https://<Kindo API host>/webhook/agent/<workflowId>/<token>. Sift recognises one on the Kindo API host of your Kindo connection and badges it Kindo agent.

  • The URL is the whole credential: anyone who has it can start the agent. Treat it like the Kindo key.
  • No header is needed. The signature is sent and ignored by Kindo.
  • The agent reads the body as its first message, and text is written to stand on its own there.
  • Use the agent’s Run Condition in Kindo to decline alerts it does not want.

Every endpoint change is in the audit log: webhook.created, webhook.updated (with the fields that changed), webhook.deleted, webhook.secret_rotated and webhook.test_sent. The audit records never include a URL, header value or secret.