Webhooks & Notifications

How do you configure webhooks to send Arthur alerts to Slack, Jira, or your own incident management system? You create a webhook in Arthur that points to your destination URL, optionally customize the payload using Jinja2 templates, and Arthur will POST alert data to that endpoint every time an alert fires. This page walks you through the complete setup for both Slack and Jira, including working payload examples and template syntax.

Overview

Webhooks let Arthur POST alert data to any HTTP endpoint when an alert rule fires — routing notifications to Slack, Jira, PagerDuty, or any system you already use for incidents.

Webhooks are workspace-level resources. You create them once in your workspace and then attach them to one or more alert rules by ID. The same webhook can be reused across multiple alert rules.


How Webhooks Work

sequenceDiagram
    participant Arthur
    participant WebhookEngine
    participant Destination

    Arthur->>WebhookEngine: Alert fires
    WebhookEngine->>WebhookEngine: Render Jinja2 body template
    WebhookEngine->>Destination: HTTP POST (rendered JSON)
    Destination-->>WebhookEngine: 2xx OK
    WebhookEngine->>Arthur: Delivery recorded

When an alert rule fires, Arthur:

  1. Selects all webhooks attached to that alert rule
  2. Renders each webhook's Jinja2 body template with alert context variables
  3. POSTs the rendered body to the configured URL with the configured headers
  4. Records the delivery result — status code and response body

Create a Webhook (UI)

Navigate to your workspace and open the Webhooks section (/workspaces/:id/webhooks). Click New Webhook. The wizard has two steps.

Step 1 — Name

Enter a human-readable name for the webhook (e.g., slack-ml-alerts, jira-incidents). This is how the webhook appears in the alert rule configuration.

Step 2 — Configure

FieldRequiredDescription
URLYesThe HTTP/HTTPS endpoint Arthur will POST to
HeadersNoKey-value pairs added to every request (e.g., Authorization, Content-Type)
BodyYesJinja2 template that renders the POST body — see Webhook Body Templates

The Test button is available once you have entered a valid URL. It sends a test POST to your endpoint and shows the response status code and body — use this before saving to confirm delivery works.


Webhook Body Templates

Arthur uses Jinja2 for webhook body templates. When an alert fires, Arthur renders your template with an alert context object and POSTs the result.

⚠️

The exact template variables injected by Arthur's backend should be verified against your deployment. The variables below are based on the alert and model context and are representative, but confirm with the Test button and your Arthur administrator before relying on specific variable names in production.

Template Variables

VariableTypeDescription
alert_rule_namestringName of the alert rule that fired
alert_rule_idstringUUID of the alert rule
model_namestringDisplay name of the monitored model
model_idstringUUID of the model
metric_valuefloatThe observed metric_value that triggered the alert
thresholdfloatThe threshold configured on the alert rule
boundstringupper_bound or lower_bound
alert_idstringUUID of the fired alert instance
fired_atstringISO 8601 timestamp when the alert fired
workspace_idstringUUID of the workspace

Template Syntax

{# Basic interpolation #}
{{ alert_rule_name }}

{# With a default fallback #}
{{ model_name | default("unknown model") }}

{# Format a float to 4 decimal places #}
{{ metric_value | round(4) }}
⚠️

Your body template must render to valid JSON after substitution. Numeric variables (metric_value, threshold) must not be quoted; string variables must be quoted.

Minimal Template

{
  "alert_rule_id": "{{ alert_rule_id }}",
  "alert_rule_name": "{{ alert_rule_name }}",
  "model_id": "{{ model_id }}",
  "model_name": "{{ model_name }}",
  "metric_value": {{ metric_value }},
  "threshold": {{ threshold }},
  "fired_at": "{{ fired_at }}"
}

Slack Integration Example

Step 1 — Create a Slack Incoming Webhook

  1. Go to your Slack app settings and enable Incoming Webhooks.
  2. Click Add New Webhook to Workspace, select your target channel, and authorize.
  3. Copy the generated URL (https://hooks.slack.com/services/...).

Step 2 — Configure in Arthur

URL: your Slack incoming webhook URL

Headers:

{
  "Content-Type": ["application/json"]
}

Body (Block Kit):

{
  "blocks": [
    {
      "type": "header",
      "text": {
        "type": "plain_text",
        "text": "Arthur Alert Fired"
      }
    },
    {
      "type": "section",
      "fields": [
        {
          "type": "mrkdwn",
          "text": "*Alert Rule:*\n{{ alert_rule_name }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Model:*\n{{ model_name }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Observed Value:*\n{{ metric_value | round(4) }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Threshold:*\n{{ threshold }}"
        }
      ]
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*Fired At:* {{ fired_at }}\n*Alert ID:* `{{ alert_id }}`"
      }
    }
  ]
}

No Authorization header is needed — the secret is embedded in the Slack webhook URL.


Jira Integration Example

Step 1 — Gather Jira Details

  • Base URL: https://your-org.atlassian.net
  • Project key: e.g., MLOPS
  • API token: generate at id.atlassian.com → Security → API tokens
  • Account email

Step 2 — Build the Authorization Header

Jira Cloud uses HTTP Basic Auth — Base64-encode email:api-token:

echo -n "[email protected]:your-api-token" | base64

Step 3 — Configure in Arthur

URL: https://your-org.atlassian.net/rest/api/3/issue

Headers:

{
  "Authorization": ["Basic <your-base64-token>"],
  "Content-Type": ["application/json"],
  "Accept": ["application/json"]
}

Body:

{
  "fields": {
    "project": { "key": "MLOPS" },
    "summary": "[Arthur] {{ alert_rule_name }} on {{ model_name }}",
    "description": {
      "type": "doc",
      "version": 1,
      "content": [
        {
          "type": "paragraph",
          "content": [
            {
              "type": "text",
              "text": "Alert: {{ alert_rule_name }} | Model: {{ model_name }} | Value: {{ metric_value }} | Threshold: {{ threshold }} | Fired: {{ fired_at }}"
            }
          ]
        }
      ]
    },
    "issuetype": { "name": "Bug" },
    "priority": { "name": "High" },
    "labels": ["arthur-alert"]
  }
}

Note: Jira Cloud uses Atlassian Document Format (ADF) for description. For Jira Server, replace the description value with a plain string: "description": "Alert: {{ alert_rule_name }} fired on {{ model_name }}".


Test a Webhook

Use the Test button in the UI (available in both the create wizard and the webhook detail page) to send a test POST and inspect the response.

You can also test via API:

response = requests.post(
    f"https://your-arthur-instance.example.com/api/v1/workspaces/{WORKSPACE_ID}/webhooks/test",
    headers={"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"},
    json={
        "url": "https://hooks.slack.com/services/...",
        "headers": {"Content-Type": ["application/json"]},
        "body": '{"text": "test"}'
    }
)
print(response.json())  # {"error": null, "response": {"status_code": 200, "response": "ok"}}
const response = await fetch(
  `https://your-arthur-instance.example.com/api/v1/workspaces/${WORKSPACE_ID}/webhooks/test`,
  {
    method: "POST",
    headers: { "Authorization": `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      url: "https://hooks.slack.com/services/...",
      headers: { "Content-Type": ["application/json"] },
      body: '{"text": "test"}'
    })
  }
);
console.log(await response.json());
curl -X POST https://your-arthur-instance.example.com/api/v1/workspaces/your-workspace-id/webhooks/test \
  -H "Authorization: Bearer your-api-token" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-endpoint.example.com", "headers": {}, "body": "{\"text\": \"test\"}"}'

A status code > 299 is treated as a failure.


Create via API

response = requests.post(
    f"https://your-arthur-instance.example.com/api/v1/workspaces/{WORKSPACE_ID}/webhooks",
    headers={"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"},
    json={
        "name": "slack-ml-alerts",
        "url": "https://hooks.slack.com/services/...",
        "headers": {"Content-Type": ["application/json"]},
        "body": '{"text": "{{ alert_rule_name }} fired on {{ model_name }}"}'
    }
)
webhook_id = response.json()["id"]
const response = await fetch(
  `https://your-arthur-instance.example.com/api/v1/workspaces/${WORKSPACE_ID}/webhooks`,
  {
    method: "POST",
    headers: { "Authorization": `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      name: "slack-ml-alerts",
      url: "https://hooks.slack.com/services/...",
      headers: { "Content-Type": ["application/json"] },
      body: '{"text": "{{ alert_rule_name }} fired on {{ model_name }}"}'
    })
  }
);
const { id } = await response.json();
curl -X POST https://your-arthur-instance.example.com/api/v1/workspaces/your-workspace-id/webhooks \
  -H "Authorization: Bearer your-api-token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "slack-ml-alerts",
    "url": "https://hooks.slack.com/services/...",
    "headers": {"Content-Type": ["application/json"]},
    "body": "{\"text\": \"{{ alert_rule_name }} fired on {{ model_name }}\"}"
  }'

Once created, attach the webhook to an alert rule by passing its id in notification_webhook_ids — see Alerts & Alert Rules.


Manage Webhooks

List all webhooks in a workspace

response = requests.get(
    f"https://your-arthur-instance.example.com/api/v1/workspaces/{WORKSPACE_ID}/webhooks",
    headers={"Authorization": f"Bearer {API_TOKEN}"}
)
for w in response.json():
    print(f"{w['id']} — {w['name']}")
const response = await fetch(
  `https://your-arthur-instance.example.com/api/v1/workspaces/${WORKSPACE_ID}/webhooks`,
  { headers: { "Authorization": `Bearer ${API_TOKEN}` } }
);
(await response.json()).forEach(w => console.log(`${w.id} — ${w.name}`));
curl https://your-arthur-instance.example.com/api/v1/workspaces/your-workspace-id/webhooks \
  -H "Authorization: Bearer your-api-token"

Update a webhook

response = requests.patch(
    f"https://your-arthur-instance.example.com/api/v1/webhooks/{WEBHOOK_ID}",
    headers={"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"},
    json={"url": "https://hooks.slack.com/services/NEW_URL"}
)
await fetch(`https://your-arthur-instance.example.com/api/v1/webhooks/${WEBHOOK_ID}`, {
  method: "PATCH",
  headers: { "Authorization": `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ url: "https://hooks.slack.com/services/NEW_URL" })
});
curl -X PATCH https://your-arthur-instance.example.com/api/v1/webhooks/your-webhook-id \
  -H "Authorization: Bearer your-api-token" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://hooks.slack.com/services/NEW_URL"}'

Delete a webhook

requests.delete(
    f"https://your-arthur-instance.example.com/api/v1/webhooks/{WEBHOOK_ID}",
    headers={"Authorization": f"Bearer {API_TOKEN}"}
)
await fetch(`https://your-arthur-instance.example.com/api/v1/webhooks/${WEBHOOK_ID}`, {
  method: "DELETE",
  headers: { "Authorization": `Bearer ${API_TOKEN}` }
});
curl -X DELETE https://your-arthur-instance.example.com/api/v1/webhooks/your-webhook-id \
  -H "Authorization: Bearer your-api-token"

Troubleshoot Webhook Delivery

SymptomLikely CauseFix
HTTP 400 from destinationMalformed JSON after template renderingNumeric variables (metric_value, threshold) must not be quoted in the template
HTTP 401 from destinationMissing or incorrect Authorization headerVerify your API token is current and the header value is correct
HTTP 403 from destinationInsufficient permissions at destinationFor Jira, confirm the API token account has Create Issue permission on the target project
HTTP 404 from destinationWrong URLDouble-check the endpoint URL; for Slack, confirm the webhook URL hasn't been revoked
Template renders empty valuesVariable name typoVariable names are case-sensitive — cross-reference against the table above
Webhook never firesAlert rule not triggeringConfirm the alert rule is enabled and the webhook ID is in notification_webhook_ids

Next Steps