> ## Documentation Index
> Fetch the complete documentation index at: https://docs.siftstack.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook settings

> Settings, options, and behaviors for Webhooks in Sift.

export const MintTable = ({columns = [], rows = [], columnWidths = []}) => {
  const pushTextWithLineBreaks = (parts, text, keyBase) => {
    const segments = String(text).split(/\\n|\n/);
    segments.forEach((segment, idx) => {
      if (segment) {
        parts.push(<span key={`${keyBase}-text-${idx}`}>{segment}</span>);
      }
      if (idx < segments.length - 1) {
        parts.push(<br key={`${keyBase}-br-${idx}`} />);
      }
    });
  };
  const parseMarkdown = text => {
    if (text === null || text === undefined) return "";
    const str = String(text);
    const parts = [];
    let lastIndex = 0;
    const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[([^\]]+)\]\(([^)]+)\))/g;
    let match;
    while (true) {
      match = pattern.exec(str);
      if (match === null) {
        break;
      }
      if (match.index > lastIndex) {
        pushTextWithLineBreaks(parts, str.substring(lastIndex, match.index), `before-${lastIndex}`);
      }
      const fullMatch = match[0];
      if (fullMatch.startsWith("`") && fullMatch.endsWith("`")) {
        parts.push(<code key={match.index}>{fullMatch.slice(1, -1)}</code>);
      } else if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
        parts.push(<strong key={match.index}>{fullMatch.slice(2, -2)}</strong>);
      } else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
        parts.push(<em key={match.index}>{fullMatch.slice(1, -1)}</em>);
      } else if (fullMatch.startsWith("[")) {
        const linkText = match[2];
        const linkUrl = match[3];
        parts.push(<a key={match.index} href={linkUrl} className="text-black-600 dark:text-black-400">
            {linkText}
          </a>);
      }
      lastIndex = pattern.lastIndex;
    }
    if (lastIndex < str.length) {
      pushTextWithLineBreaks(parts, str.substring(lastIndex), `tail-${lastIndex}`);
    }
    if (parts.length > 0) {
      return parts;
    }
    const plainParts = [];
    pushTextWithLineBreaks(plainParts, str, "plain");
    return plainParts.length ? plainParts : str;
  };
  const safeColumns = Array.isArray(columns) ? columns : [];
  const safeRows = Array.isArray(rows) ? rows : [];
  const safeColumnWidths = Array.isArray(columnWidths) ? columnWidths : [];
  const hasColumnWidths = safeColumnWidths.some(w => w !== null && w !== undefined && w !== "");
  const toCssWidth = width => typeof width === "number" ? `${width}px` : String(width);
  const getColumnStyle = idx => {
    const rawWidth = safeColumnWidths[idx];
    if (rawWidth === null || rawWidth === undefined || rawWidth === "") {
      return undefined;
    }
    const width = toCssWidth(rawWidth);
    return {
      width,
      minWidth: width
    };
  };
  const containerStyle = hasColumnWidths ? undefined : {
    overflowX: "auto"
  };
  const tableStyle = hasColumnWidths ? {
    tableLayout: "fixed",
    width: "100%"
  } : {
    width: "max-content",
    minWidth: "100%"
  };
  if (!Array.isArray(columns) || !Array.isArray(rows) || !Array.isArray(columnWidths)) {
    console.warn("MintTable received invalid props:", {
      columns,
      rows,
      columnWidths
    });
  }
  if (!safeColumns.length && !safeRows.length) {
    return null;
  }
  return <div className="mint-table-container" style={containerStyle}>
      <table style={tableStyle}>
        {hasColumnWidths && <colgroup>
            {safeColumns.map((_, idx) => {
    const style = getColumnStyle(idx);
    return <col key={idx} style={style} />;
  })}
          </colgroup>}
        <thead>
          <tr>
            {safeColumns.map((col, idx) => <th key={idx} className="text-left" style={getColumnStyle(idx)}>
                <b>{parseMarkdown(col)}</b>
              </th>)}
          </tr>
        </thead>
        <tbody>
          {safeRows.map((row, rIdx) => {
    const safeRow = Array.isArray(row) ? row : [];
    return <tr key={rIdx}>
                {safeRow.map((cell, cIdx) => <td key={cIdx} style={getColumnStyle(cIdx)}>
                    {parseMarkdown(cell)}
                  </td>)}
              </tr>;
  })}
        </tbody>
      </table>
    </div>;
};

Webhooks in Sift enable sending real-time notifications to external systems when specific events occur, such as when a Rule is violated or resolved. Each webhook includes a trigger event, a customizable payload, and a destination URL that receives the data via an HTTP POST request.

Webhooks only fire during live Rule evaluations and do not execute when Rules are run on historical data.

## Settings

The following table describes each setting available when creating or editing a webhook.

<MintTable
  columns={['Setting', 'Required', 'Description']}
  columnWidths={['25%', '10%', '65%']}
  rows={[
['Webhook name', 'Yes', 'A user-defined name to identify the webhook.'],
['Trigger event type', 'Yes', 'Specifies which event will cause the webhook to fire (for example, Rule Violation). Determines which built-in variables are available.'],
['Destination URL', 'Yes', 'The endpoint that will receive the POST request. Must be a valid HTTPS URL.'],
['Custom HTTP headers', 'No', 'Optional key-value pairs added to the HTTP request header. Can be used for auth tokens or content type declarations. See [Custom HTTP headers](#custom-http-headers).'],
['Payload config template', 'Yes', 'Defines the initial structure of the webhook payload based on the selected trigger. Determines whether you start with a blank template or a preformatted payload for a specific integration (for example, Slack).'],
['Payload body', 'Yes', 'The content sent to the endpoint. May include built-in variables depending on the trigger event.'],
]}
/>

## Trigger event types

When creating a webhook, you must select a trigger event type. The following trigger event type is available:

<MintTable
  columns={['Trigger event type', 'Description', 'Example']}
  columnWidths={['20%', '40%', '40%']}
  rows={[
['Rule Violation', 'The webhook fires each time a Rule associated with it is either violated or resolved during live data ingestion.', 'A Rule monitors a temperature Channel and has a webhook configured with this trigger. During live data ingestion, if the temperature exceeds a defined threshold, the Rule enters a violated state. When the temperature falls back below the threshold, the Rule transitions to a resolved state. In both cases, the webhook fires automatically.'],
]}
/>

## Built-in variables

Each trigger event type includes a set of built-in variables available when creating or editing a webhook. These variables can be used to customize the webhook payload with dynamic, event-specific data.

### Rule Violation

The following table lists the built-in variables available for the Rule Violation trigger event type.

<MintTable
  columns={['Variable', 'Type', 'Description']}
  columnWidths={['25%', '15%', '60%']}
  rows={[
['`{{.RuleName}}`', 'string', 'The name of the Rule that is being triggered.'],
['`{{.Status}}`', '"resolved" or "violated"', 'Indicates whether the Rule is in a resolved or violated state.'],
['`{{.WebhookId}}`', 'string', 'The unique identifier of the webhook.'],
['`{{.EventId}}`', 'string', 'The unique identifier of the event.'],
['`{{.RuleId}}`', 'string', 'The unique identifier of the Rule that was triggered.'],
['`{{.RuleVersion}}`', 'number', 'The version number of the Rule that was triggered.'],
['`{{.AssetName}}`', 'string', 'The name of the Asset associated with the triggered Rule.'],
['`{{.RunId}}`', 'string', 'The unique identifier of the Run associated with the event.'],
['`{{.SentAt}}`', 'string', 'The timestamp indicating when the webhook was sent, formatted in RFC3339.'],
]}
/>

## Webhook payloads

Webhook payloads define the content sent to the destination URL when a webhook fires. Payloads are fully customizable and can be formatted as either plain text or JSON. Each payload can include built-in variables specific to the selected trigger event type, allowing dynamic insertion of relevant data such as Rule names, statuses, and timestamps.

Sift provides predefined payload templates for services like Slack, OpsGenie, Jira, and PagerDuty. These can be selected from the **Payload config template** list during webhook creation or editing.

The payload editor supports control flow, including if-else logic for dynamic formatting. For example:

```json theme={null}
{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "{{if eq .Status \"resolved\"}}:white_check_mark: Resolved{{else}}:bangbang: *Alert - {{.Status}}*{{end}}"
      }
    }
  ]
}
```

## Custom HTTP headers

When configuring a webhook, you can optionally define HTTP headers to customize how the receiving system handles the request. These headers are sent along with the webhook payload.

<MintTable
  columns={['Header', 'Purpose', 'Notes']}
  columnWidths={['20%', '40%', '40%']}
  rows={[
['Content-Type', 'Specifies the format of the webhook payload (for example, application/json).', 'Determines how the receiving system interprets the payload.'],
['Authorization', 'Provides credentials to authenticate the webhook request.', 'Use this header if the destination requires a token or API key. The value is obfuscated in the UI and securely encrypted while stored in Sift.'],
]}
/>

## Secure webhooks

Sift supports secure webhook delivery using HMAC-SHA256 signatures. These signatures ensure the webhook was sent by Sift and has not been tampered with in transit. Webhooks are only signed if your Sift environment has a webhook signing key configured.

If no signing key exists, webhooks will not include an `X-Sift-Signature` header and should not be trusted for secure operations.

Once a signing key is configured, each outgoing webhook will include a signature header computed using the following formula:

```
HMAC_SHA256(signing_key, sent_at + raw_payload)
```

The `sentAt` value must be present in the payload to successfully validate the signature. This value is not included automatically. You must explicitly add it using `{{.SentAt}}` in your webhook payload template. Without it, signature verification will fail.

## Role-based access control

Webhooks follow Sift's standard RBAC model. The following table describes the permissions available for each role.

<MintTable
  columns={['Role', 'Permissions']}
  columnWidths={['30%', '70%']}
  rows={[
['Admin / Editor', 'Create, view, and edit webhooks.'],
['Collaborator / View-only', 'View-only access. Cannot create or edit webhooks.'],
]}
/>

<Warning>
  **Insufficient permissions**: Unauthorized users who attempt to access restricted pages by entering the URL directly will be redirected to the Manage page and shown a warning indicating insufficient permissions.
</Warning>

## Behavior

The following table describes known constraints and behaviors to be aware of when working with Webhooks in Sift.

<MintTable
  columns={['Limitation', 'Description']}
  columnWidths={['25%', '75%']}
  rows={[
['Live evaluations only', 'Webhooks only fire during live Rule evaluations. They do not execute when Rules are run on historical data.'],
['Log generation', 'Webhook logs are automatically generated each time a webhook fires or fails to fire. If a webhook fails, the log entry includes the reason for the failure.'],
['Log retention', 'Logs are retained for four weeks, after which they are automatically deleted. Sift does not retain logs generated from test webhook executions.'],
['Rate limit: baseline rate', '2 webhooks per second. The standard rate at which webhooks can be sent without consuming burst tokens.'],
['Rate limit: burst capacity', 'Up to 100 webhooks per second. Maximum temporary throughput allowed when using burst tokens.'],
['Rate limit: burst tokens', '100 tokens per webhook. Available for burst use.'],
['Rate limit: token cost', '1 token per webhook sent beyond the baseline rate.'],
['Rate limit: token recovery', '2 tokens per second when usage is below the baseline.'],
['Rate limit: failure', 'If a webhook exceeds its rate limits and burst tokens are exhausted, it will fail to send. A failure entry will be recorded in the webhook logs.'],
['Retry policy', 'Sift retries a failed webhook once, but only if the failure is due to a reason other than rate-limiting. Rate-limited webhooks are not retried.'],
['Replay attacks', 'To prevent replay attacks, reject any webhook where the sentAt timestamp is older than 5 minutes.'],
['Response', 'Your endpoint should return a 2xx response as soon as possible, before running any complex logic that might delay the response or cause a timeout.'],
]}
/>
