# Guide to Kibana Webhooks: Features and Best Practices

Kibana webhooks let your Elastic alerting rules call any service you run. When a rule detects something, such as an error rate threshold crossed, a log pattern matched or a host gone quiet, its action can send an HTTP request through a Webhook connector, so you can open tickets, trigger remediation or notify a tool that Kibana has no dedicated connector for.

This guide covers how the Webhook connector and rule actions fit together, how to authenticate requests with Basic authentication, how to design the body template, and the practices that keep a Kibana receiver reliable.

## What are Kibana webhooks?

Kibana separates **connectors** from **actions**. A Webhook connector holds the connection details: the URL, the HTTP method, the authentication and any extra headers. An alerting rule's action uses a connector and supplies the request body, written as a Mustache template that Kibana fills in with details of the rule and the alert when the action runs.

Kibana does not sign these requests and adds no event type. Everything your endpoint learns about the alert comes from the template you write. The connector's authentication setting is **None**, **Basic**, **SSL** (a client certificate) or, since Elastic Stack 9.2, **OAuth 2.0**. With Basic, Kibana sends your username and password in an `Authorization` header with each request.

This guide covers the Webhook connector, not the separate **Webhook - Case Management** connector.

## Kibana webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Stack Management > Connectors**, or while creating a rule |
| Method | `POST` (default), `PUT`, and since Elastic Stack 9.3 `PATCH`, `GET` and `DELETE` |
| URL | `http://` or `https://`. Must be in `xpack.actions.allowedHosts` if you use that setting |
| Authentication | None, Basic, SSL client certificate, or OAuth 2.0 (9.2+) |
| Custom headers | Config type (plain text) or, since 9.2, secret type |
| Body | A Mustache template per rule action, sent with `POST`, `PUT` or `PATCH` |
| Escaping | Variables are escaped so the result stays valid JSON |
| Signature and timestamp | None |
| Event type | None. Defined by your template |
| Testing | Test the connector while creating or editing it |
| Timeout, retries, source IPs | Not documented on the connector page |

## Designing the body

The body is whatever your action's template renders. Kibana's own example is:

```text
{
  "short_description": "{{context.rule.name}}",
  "description": "{{context.rule.description}}",
  ...
}

```

Every rule type passes general variables such as `rule.id`, `rule.name`, `rule.url`, `rule.tags` and `date`. Actions that run for each alert also get `alert.id`, `alert.uuid`, `alert.actionGroup` and `alert.flapping`, and each rule type adds its own `context` variables. A template for your own receiver might be:

```text
{
  "source": "kibana",
  "rule_id": "{{rule.id}}",
  "rule_name": "{{rule.name}}",
  "rule_url": "{{rule.url}}",
  "alert_uuid": "{{alert.uuid}}",
  "action_group": "{{alert.actionGroup}}",
  "scheduled_at": "{{date}}",
  "reason": "{{context.reason}}"
}

```

Kibana escapes characters that are invalid in JSON string values, so a rule name with quotes in it does not break the body. Triple braces, such as `{{{context.reason}}}`, turn escaping off and can produce invalid JSON, so avoid them in webhook bodies. To see every variable available to a rule, temporarily set the body to `{{{.}}}`, which renders the whole context as JSON. `rule.url` is empty unless `server.publicBaseUrl` is configured.

`alert.uuid` stays the same each time the rule runs while the alert is active, which makes it the natural key for tracking one alert across notifications.

> **Inspect Kibana webhooks as they arrive.** Point a Webhook connector at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL and run a rule, or render `{{{.}}}`, to see exactly what your template produces, with no account or setup required.

## Setting up Kibana webhooks

Choose a username and a long random password first, for example with `openssl rand -hex 32` for the password.

1. In Kibana, go to **Stack Management > Connectors** and create a **Webhook** connector.
2. Choose the **Method**. Keep `POST` unless your receiver needs another.
3. Enter the **URL** of your HTTPS endpoint.
4. Choose **Basic** authentication and enter the username and password.
5. Save, and test the connector.
6. Create or edit an alerting rule, add an action that uses the connector, and write the body template.

To route through Hookdeck, create a Hookdeck source with the **Kibana** type and use its URL as the connector URL. Choose Basic authentication on the Kibana Webhook connector. The source asks for a **Username** and **Password**: enter the same pair. Hookdeck never answers with a `401` challenge, so the credentials must arrive with each request, which is what the Webhook connector does with Basic authentication.

The source pre-selects `POST` but leaves the method open, because the connector lets you choose. If your connector uses `PUT`, `PATCH`, `GET` or `DELETE`, add that method to the source's allowed HTTP methods as well. `GET` and `DELETE` requests carry no body, so the template only applies to `POST`, `PUT` and `PATCH`.

## Securing Kibana webhooks

With Basic authentication, every request carries `Authorization: Basic <base64(username:password)>`. Decode it, split on the first colon, and compare the username and password with your stored values in constant time. Check it for every method your connector uses.

```javascript
const crypto = require('crypto');
const express = require('express');

const USERNAME = process.env.KIBANA_WEBHOOK_USERNAME;
const PASSWORD = process.env.KIBANA_WEBHOOK_PASSWORD;

function safeEqual(a, b) {
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyBasicAuth(header) {
  if (!USERNAME || !PASSWORD || typeof header !== 'string') return false;
  const match = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(header);
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64');
  const colon = decoded.indexOf(0x3a); // split on the first colon
  if (colon === -1) return false;
  const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(USERNAME, 'utf8'));
  const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(PASSWORD, 'utf8'));
  return userOk && passOk;
}

const app = express();

app.post('/webhooks/kibana', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyBasicAuth(req.get('authorization'))) {
    return res.sendStatus(401);
  }

  let alert;
  try {
    alert = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Fields are whatever your template defines, e.g. alert.rule_id and alert.alert_uuid.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import binascii
import hmac
import os

USERNAME = os.environ.get("KIBANA_WEBHOOK_USERNAME", "")
PASSWORD = os.environ.get("KIBANA_WEBHOOK_PASSWORD", "")

def verify_basic_auth(header) -> bool:
    if not USERNAME or not PASSWORD or not header:
        return False
    scheme, _, encoded = header.partition(" ")
    if scheme.lower() != "basic":
        return False
    try:
        decoded = base64.b64decode(encoded.strip(), validate=True)
    except (binascii.Error, ValueError):
        return False
    user, sep, password = decoded.partition(b":")
    if not sep:
        return False
    user_ok = hmac.compare_digest(user, USERNAME.encode("utf-8"))
    pass_ok = hmac.compare_digest(password, PASSWORD.encode("utf-8"))
    return user_ok and pass_ok

```

Basic credentials are static. They authenticate the sender, not the body, so a modified payload still passes, and with no timestamp a captured request can be replayed. Serve the endpoint over HTTPS only, use a long random password, and change it on the connector and in your environment together if it might have leaked.

> **Make Kibana webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the Basic credentials on every request, queues each alert notification durably, and retries delivery to your service on a schedule you control.

## Kibana webhook limitations and pain points

### Only Basic authentication fits the source type

**The Problem:** A connector set up with OAuth 2.0, an SSL client certificate or a secret header never verifies in Hookdeck's Kibana source.

**Why It Happens:** The Kibana source checks HTTP Basic credentials only. OAuth 2.0 tokens change as they are refreshed, a client certificate is checked during the TLS handshake rather than in a header, and custom headers are not part of this source type.

**Workarounds:**

* Choose **Basic** authentication on connectors that point at Hookdeck.
* If you are tied to another method, check it in your own receiver instead.

**How Hookdeck Can Help:** With Basic authentication, the Kibana source compares the username and password you enter. Hookdeck answers Kibana with a `200` either way, but requests without matching credentials are recorded as failed verification and never reach your service.

### The template is the contract

**The Problem:** Someone edits a rule's action and your receiver starts failing, or two rules send the same endpoint different shapes.

**Why It Happens:** The body is defined per action, so every rule that uses the connector can send something different, and a variable that does not exist for a rule type renders as an empty string.

**Workarounds:**

* Agree one template for every action that targets the same endpoint, and treat changes to it like an API change.
* Include constant fields such as `"source": "kibana"` and a schema version.
* Use `{{{.}}}` once per rule type to check which `context` variables really exist.

**How Hookdeck Can Help:** Hookdeck keeps every request with its full body, so you can see exactly what a changed template sent and replay it against your fixed handler. [Transformations](/docs/transformations) can normalise differing shapes before they reach your service.

### No documented delivery guarantees

**The Problem:** You cannot tell from the connector docs how long Kibana waits for your endpoint, whether it retries, or what happens to a notification when your service is down.

**Why It Happens:** The Webhook connector page covers configuration and the body, not delivery behaviour.

**Workarounds:**

* Respond quickly and process afterwards, so a slow handler never becomes a lost alert.
* Watch the rule's execution history in Kibana for failed actions.

**How Hookdeck Can Help:** Hookdeck accepts each request as soon as it arrives and stores it, then delivers to your service with [retries](/docs/retries) you configure. You can replay any notification after a fix, and [issues](/docs/issues) alert you when deliveries to your service fail.

### Repeated notifications for one alert

**The Problem:** One alert produces a request every time the rule runs, and your receiver opens duplicate tickets.

**Why It Happens:** Depending on the action's frequency settings, an action can run on every rule execution while an alert stays active.

**Workarounds:**

* Include `alert.uuid` in the template and upsert on it.
* Set the action to run only when the alert status changes, if that fits your workflow.

**How Hookdeck Can Help:** Hookdeck [deduplication](/docs/deduplication) can drop repeated notifications based on fields you choose, such as `alert_uuid` and `action_group`.

## Best practices

### Design the template for your receiver

Include the rule ID, the alert UUID, the action group, the date and a link back to Kibana. Keep double braces so Kibana escapes the values.

### Make handlers idempotent

Key on `alert.uuid` plus the action group, and store that key before you act. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Keep secrets out of plain headers

If you do add custom headers, use the secret type (9.2+) for anything sensitive. Config-type header values are stored and sent as plain text.

### Acknowledge fast and process asynchronously

Verify the credentials, store the notification, return `200`, and do the work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Kibana's Webhook connector sends whatever body your rule action's Mustache template renders, with no signature and no event type. Choose Basic authentication, compare the decoded username and password in constant time over HTTPS, put the rule ID and alert UUID in the template, allow every method your connector uses, and build the handler to cope with repeats and undocumented delivery behaviour.

[Hookdeck Event Gateway](https://hookdeck.com) checks the Basic credentials, keeps every notification, and retries and replays delivery to your service, so an outage on your side does not mean a missed alert.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Kibana webhooks reliably in minutes.