Zum Inhalt springen
Kaspar

Doku

Webhooks

Kaspar kann jeden Alarm als signierten JSON-Request an deinen eigenen Endpunkt schicken, für Uptime-Monitore, Cron-Heartbeats und Broken-Link-Scans.

Webhook einrichten

Webhooks sind Teil des Pro-Plans. Öffne in Kaspar Alarme, trag unter Webhook einen Namen und die URL deines Endpunkts ein und füge ihn hinzu. Wie jeder Alarmkanal kann ein Webhook Standardkanal des Workspaces sein oder pro Monitor ausgewählt werden.

  • Die URL muss https:// verwenden. Adressen, die auf private, Loopback-, Link-Local- oder andere interne Netze zeigen, werden abgelehnt, beim Hinzufügen und noch einmal vor jedem Request.
  • Jeder Webhook bekommt ein eigenes Signing-Secret, das mit whsec_ beginnt. Du kannst es in Kaspar anzeigen und kopieren und jederzeit ein neues erzeugen. Ein neues Secret ersetzt das alte sofort.
  • Mit Test-Webhook senden schickst du ein webhook.test-Event. Kaspar zeigt neben dem Webhook den Status der letzten Zustellung und den HTTP-Statuscode.

Der Request

Kaspar sendet einen POST-Request mit JSON-Body (Content-Type: application/json) und diesen Headern:

HeaderInhalt
Kaspar-Signaturet=<Unix-Zeitstempel>,v1=<Signatur>, siehe Prüfung
Kaspar-EventDas Event, z. B. monitor.down (wie event im Body)
Kaspar-DeliveryEine UUID pro Zustellung. Bei Wiederholungen bleibt sie gleich, damit kannst du Duplikate erkennen.
User-AgentKaspar-Webhooks/1.0

Events

EventWann
monitor.downEin Uptime-Monitor ist down (auch bei fehlgeschlagener Keyword-Prüfung).
monitor.still_downErinnerung, solange ein Uptime-Monitor down bleibt.
monitor.upEin Uptime-Monitor ist wieder erreichbar.
monitors.downMehrere Monitore sind gleichzeitig ausgefallen; kommt statt einzelner Alarme. monitor ist null, data.monitors enthält die Namen.
ssl.expiringDas SSL-Zertifikat läuft bald ab oder ist abgelaufen.
cron.missedEin Cron-Job hat sich nicht innerhalb der erwarteten Zeit und Karenzzeit gemeldet.
cron.failedEin Cron-Job hat einen Fehler gemeldet.
cron.recoveredEin Cron-Job ist wieder OK.
links.brokenEin Link-Scan hat defekte Links gefunden, die noch nicht gemeldet wurden.
links.fixedAlle zuvor defekten Links sind behoben.
webhook.testKommt von Test-Webhook senden.

Payload

Jeder Body hat dieselbe Struktur. version ist die Schema-Version (aktuell 1). Innerhalb einer Version kommen nur Felder hinzu, keine werden umbenannt oder entfernt. Ignoriere also Felder, die du nicht kennst.

{
  "version": 1,
  "id": "0b5c1e0e-6f1e-4c39-9d43-2f2a9e1c7a51",
  "event": "monitor.down",
  "created_at": "2026-10-14T16:35:12Z",
  "workspace": { "id": "Wk7pQ2rX9aLm", "name": "Acme" },
  "monitor": {
    "id": "Mn3xV8sK1qTe",
    "name": "Shop",
    "type": "uptime",
    "url": "https://shop.example.com"
  },
  "data": {
    "incident": {
      "type": "down",
      "cause": "Unexpected status code 503",
      "started_at": "2026-10-14T16:34:40Z",
      "resolved_at": null,
      "duration_seconds": null
    }
  },
  "alert": {
    "level": "down",
    "title": "Shop is down",
    "lines": ["…"]
  },
  "dashboard_url": "https://kaspar.app/acme/monitors/Mn3xV8sK1qTe"
}
FeldBeschreibung
idUUID der Zustellung, identisch mit dem Header Kaspar-Delivery.
eventName des Events, siehe oben.
created_atZeitpunkt des Alarms, ISO 8601 in UTC.
workspaceID und Name des Workspaces.
monitorID, Name, Typ (uptime, cron oder links) und geprüfte URL des Monitors (url ist bei Cron-Jobs null). null bei monitors.down und webhook.test.
dataDetails zum Event: incident (Typ, Ursache, Beginn, Ende, Dauer in Sekunden) bei Monitor- und Cron-Events, last_ping_at bei cron.missed, days_left und expires_at bei ssl.expiring, broken_count, new_count und bis zu 10 links bei links.broken, fixed_count bei links.fixed.
alertDer Alarm, wie ihn Menschen per E-Mail, Slack oder Telegram sehen: Level (down, up, warning, info), Titel und Zeilen, in der Sprache des Workspaces. Praktisch zum Weiterleiten; für deine Logik nimm event und data.
dashboard_urlLink zum Monitor (oder zum Dashboard) in Kaspar.

Signatur prüfen

Der Header Kaspar-Signature belegt, dass ein Request von Kaspar kommt und nicht verändert wurde. Er enthält einen Unix-Zeitstempel t und die Signatur v1: den hex-kodierten HMAC-SHA256 von "{t}.{body}", mit dem kompletten Signing-Secret (inklusive whsec_) als Schlüssel.

  1. Lies den rohen Request-Body, bevor du JSON parst.
  2. Nimm t und v1 aus dem Header.
  3. Berechne HMAC-SHA256 aus t, einem Punkt und dem rohen Body mit deinem Secret.
  4. Vergleiche das Ergebnis mit v1, und zwar in konstanter Zeit.
  5. Lehne den Request ab, wenn t mehr als fünf Minuten von deiner Uhrzeit abweicht. Das schützt vor Replays.

Jede Wiederholung wird mit neuem Zeitstempel neu signiert.

PHP

<?php
// Verify a Kaspar webhook (plain PHP; works the same in Laravel or Symfony).
$secret = getenv('KASPAR_WEBHOOK_SECRET'); // whsec_…
$body   = file_get_contents('php://input'); // the raw body, before any JSON decoding
$header = $_SERVER['HTTP_KASPAR_SIGNATURE'] ?? '';

function kaspar_verify(string $body, string $header, string $secret, int $tolerance = 300): bool
{
    $timestamp = null;
    $signatures = [];

    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($key === 't' && ctype_digit($value)) {
            $timestamp = (int) $value;
        } elseif ($key === 'v1' && $value !== '') {
            $signatures[] = $value;
        }
    }

    if ($timestamp === null || abs(time() - $timestamp) > $tolerance) {
        return false;
    }

    $expected = hash_hmac('sha256', $timestamp.'.'.$body, $secret);

    foreach ($signatures as $signature) {
        if (hash_equals($expected, $signature)) {
            return true;
        }
    }

    return false;
}

if (! kaspar_verify($body, $header, $secret)) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
// e.g. if ($event['event'] === 'monitor.down') { … }

http_response_code(204);

Node.js

// Verify a Kaspar webhook (Node.js 18+, Express).
import crypto from 'node:crypto';
import express from 'express';

const secret = process.env.KASPAR_WEBHOOK_SECRET; // whsec_…

function verifyKaspar(rawBody, header, secret, toleranceSeconds = 300) {
  let timestamp = null;
  const signatures = [];

  for (const part of (header ?? '').split(',')) {
    const [key, value = ''] = part.trim().split('=');
    if (key === 't' && /^\d+$/.test(value)) timestamp = Number(value);
    if (key === 'v1' && value) signatures.push(value);
  }

  if (timestamp === null || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
    return false;
  }

  const expected = crypto.createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody) // Buffer with the raw request body
    .digest('hex');

  return signatures.some((signature) =>
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)));
}

const app = express();

// express.raw keeps the body as a Buffer; parsing JSON first would change the bytes.
app.post('/kaspar-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyKaspar(req.body, req.get('Kaspar-Signature'), secret)) {
    return res.sendStatus(400);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // e.g. if (event.event === 'monitor.down') { … }

  res.sendStatus(204);
});

app.listen(3000);

Zustellung und Wiederholungen

  • Antworte innerhalb von 10 Sekunden mit einem 2xx-Statuscode. Kaspar liest den Response-Body nicht.
  • Jeder andere Statuscode, ein Timeout oder ein Verbindungsfehler gilt als fehlgeschlagen. Kaspar versucht es insgesamt bis zu dreimal: sofort, nach etwa 30 Sekunden und nach etwa zwei weiteren Minuten.
  • Weiterleitungen werden nicht verfolgt; eine 3xx-Antwort gilt als fehlgeschlagen.
  • Zustellungen laufen über eine Warteschlange. Erledige langsame Arbeit also erst, nachdem du geantwortet hast.