Black Friday in 15 Tagen: Können Ihre Kunden dann bezahlen? 25 % Rabatt im ersten Jahr und kostenlose Einrichtung, bis 30. NovemberZum Angebot →

Integration

Einen Pathly-Webhook empfangen und prüfen

Ein vollständiger Empfänger in Node und PHP, die Signatur am Roh-Body geprüft, und was Sie einplanen müssen, da Pathly jedes Ereignis nur einmal sendet.

Meinen API-Schlüssel holenOpenAPI-Vertrag

Vollständiges Dokument, ohne Authentifizierung, bereit für Ihren Client-Generator.

Was Pathly sendet

Jedes Ereignis ist ein POST an die Adresse Ihres Webhooks, mit einem JSON-Body in UTF-8 und dem Header Content-Type: application/json. Der Body wird kompakt in einer einzigen Zeile gesendet: Das Beispiel unten ist nur zur besseren Lesbarkeit eingerückt. Ein Webhook ohne Ereignisliste erhält run.failed und run.recovered.

Vier Header begleiten den Body. X-Pathly-Id ist die eindeutige Kennung der Zustellung und steht auch im Feld id. X-Pathly-Event nennt das Ereignis. X-Pathly-Timestamp gibt den Sendezeitpunkt in Unix-Sekunden an. X-Pathly-Signature lautet sha256= gefolgt vom hexadezimalen HMAC-SHA256 über Zeitstempel, Punkt und Roh-Body, berechnet mit dem Geheimnis des Webhooks.

Bei run.failed und run.recovered enthält der Body das Szenario (monitorName, monitorId), die Ausführung (runId), ihren Zustand (status) und den vorherigen Zustand (previousStatus), die Meldung, den betroffenen Schritt (failedStep), die Latenz in Millisekunden (latencyMs), die Region der Sonde (region) und den Namen der Organisation (orgName). Ein run.recovered trägt immer den status ok. failedStep kann null sein, und ein Feld ohne Wert entfällt: Setzen Sie kein optionales Feld als vorhanden voraus.

incident.opened und incident.resolved kommen mit denselben Headern und derselben Signatur, aber mit einem anderen Body: occurredAt, incidentId, status, title, openedAt, resolvedAt, durationSec, scenarioId, scenarioName, scenarioType, siteUrl, failedStep, message, runId, runUrl, scenarioUrl und screenshotUrl. Verzweigen Sie anhand von X-Pathly-Event, bevor Sie den Body lesen.

Die Signatur im Beispiel ist echt. Sie wird über den kompakten Body des Beispiels berechnet, Schlüssel in derselben Reihenfolge und ohne Leerzeichen, mit dem Beispielgeheimnis whsec_exemple und dem gezeigten Zeitstempel. Prüfen Sie damit Ihre eigene Berechnung, bevor Sie einen echten Webhook anschließen.

Body von run.failed

{
  "id": "5b0c2f0e-6d0a-4f5e-9a51-2f1c7d3e8a10",
  "event": "run.failed",
  "monitorName": "Checkout",
  "monitorId": "0b7e1c52-3f0d-4c1b-8f8e-6a2d9c4e7f31",
  "runId": "c1a4e8d2-9b3f-4e6a-a7d5-3f2b1c0e9d84",
  "status": "fail",
  "previousStatus": "ok",
  "message": "Element #place-order not found",
  "failedStep": "click",
  "latencyMs": 8421,
  "region": "eu-west",
  "orgName": "Example Studio"
}

Header dieser Zustellung

POST /hooks/pathly HTTP/1.1
Content-Type: application/json
X-Pathly-Id: 5b0c2f0e-6d0a-4f5e-9a51-2f1c7d3e8a10
X-Pathly-Event: run.failed
X-Pathly-Timestamp: 1794301200
X-Pathly-Signature: sha256=3e65396afd37e66ac4eeda02dd9fd343e454f19a725d3b25aebcd33bb1cb638b

Ein vollständiger Minimal-Empfänger

Die Seite Webhooks zeigt nur die Prüffunktion. Hier ist der ganze Server, ohne Abhängigkeit. Er tut vier Dinge in dieser Reihenfolge: den Roh-Body lesen, einen zu alten Zeitstempel ablehnen, die Signatur in konstanter Zeit prüfen und mit 2xx antworten, bevor er arbeitet.

Lesen Sie die empfangenen Bytes vor jedem JSON.parse. Die Signatur deckt genau den Body ab, den Pathly gesendet hat. Ein Body, den Ihr Framework dekodiert und neu geschrieben hat, besteht nicht mehr aus denselben Bytes, und die Prüfung schlägt fehl. Fordern Sie bei Express oder Fastify für diese Route den Roh-Body an.

Vergleichen Sie Signaturen mit timingSafeEqual in Node oder hash_equals in PHP, nie mit einem gewöhnlichen Gleichheitsvergleich. Ein gewöhnlicher Vergleich bricht beim ersten abweichenden Zeichen ab, und diese Laufzeit verrät einem Angreifer etwas über die erwartete Signatur.

Lehnen Sie eine Zustellung ab, deren Zeitstempel mehr als fünf Minuten von Ihrer Uhr abweicht. Ohne diese Prüfung lässt sich eine abgefangene Anfrage mit ihrer gültigen Signatur beliebig oft wiederholen. Die Dauer legen Sie fest: Halten Sie sie kurz und die Serveruhr synchron.

Antworten Sie mit 401 ohne Details, wenn die Prüfung fehlschlägt. Protokollieren Sie weder das Geheimnis noch die erwartete Signatur. Das Node-Beispiel hält bereits verarbeitete Kennungen im Speicher: Verwenden Sie in Produktion einen eindeutigen Schlüssel in Ihrer Datenbank.

Node — eingebauter http-Server

import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const SECRET = process.env.PATHLY_WEBHOOK_SECRET;
const MAX_AGE_SEC = 5 * 60;
const MAX_BODY_BYTES = 1_000_000;
const seen = new Set(); // X-Pathly-Id already handled (a unique key in your database in production)

function verify(timestamp, rawBody, signature) {
  const expected =
    "sha256=" +
    createHmac("sha256", SECRET)
      .update(timestamp + ".")
      .update(rawBody)
      .digest("hex");
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

function handle(event) {
  if (event.event === "run.failed") console.log("down:", event.monitorName);
  if (event.event === "run.recovered") console.log("recovered:", event.monitorName);
}

createServer((req, res) => {
  if (req.method !== "POST" || req.url !== "/hooks/pathly") {
    res.writeHead(404).end();
    return;
  }
  const chunks = [];
  let size = 0;
  req.on("data", (chunk) => {
    size += chunk.length;
    if (size > MAX_BODY_BYTES) req.destroy();
    else chunks.push(chunk);
  });
  req.on("end", () => {
    // 1. Raw bytes, exactly as received: no JSON.parse yet.
    const rawBody = Buffer.concat(chunks);
    const id = String(req.headers["x-pathly-id"] ?? "");
    const timestamp = String(req.headers["x-pathly-timestamp"] ?? "");
    const signature = String(req.headers["x-pathly-signature"] ?? "");

    // 2. Stale timestamp or wrong signature: same answer, no detail.
    const age = Math.abs(Date.now() / 1000 - Number(timestamp));
    if (!(age <= MAX_AGE_SEC) || !verify(timestamp, rawBody, signature)) {
      res.writeHead(401).end();
      return;
    }

    // 3. Answer 2xx right away.
    res.writeHead(204).end();

    // 4. Then deduplicate and do the work.
    if (seen.has(id)) return;
    seen.add(id);
    const event = JSON.parse(rawBody.toString("utf8"));
    setImmediate(() => handle(event));
  });
}).listen(8080);

PHP — php://input und hash_equals

<?php
// hooks/pathly.php
$secret = getenv('PATHLY_WEBHOOK_SECRET');

// 1. Raw bytes, exactly as received: no json_decode yet.
$rawBody = file_get_contents('php://input');
$id = $_SERVER['HTTP_X_PATHLY_ID'] ?? '';
$timestamp = $_SERVER['HTTP_X_PATHLY_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_PATHLY_SIGNATURE'] ?? '';

// 2. Stale timestamp or wrong signature: same answer, no detail.
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
$fresh = ctype_digit($timestamp) && abs(time() - (int) $timestamp) <= 5 * 60;
if (!$fresh || !hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

// 3. Answer 2xx right away: with PHP-FPM the response leaves here.
http_response_code(204);
if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();
}

// 4. Then deduplicate on $id (a unique key in your database) and do the work.
$event = json_decode($rawBody, true);
if (($event['event'] ?? '') === 'run.failed') {
    error_log('down: ' . ($event['monitorName'] ?? ''));
}

Wie Pathly zustellt

Pathly sendet jedes Ereignis genau einmal an jeden aktiven Webhook, der es abonniert hat. Es gibt keinen automatischen neuen Versuch. Ist Ihr Server nicht erreichbar oder antwortet er mit einem Fehler, wird diese Zustellung nicht wiederholt.

Eine 3xx-Weiterleitung wird nie verfolgt und zählt als Fehlschlag: Hinterlegen Sie die endgültige Adresse Ihres Empfängers. Es werden nur öffentliche https-Adressen akzeptiert.

Pathly wartet auf Ihre Antwort, bevor der nächste Webhook an der Reihe ist. Ein Vorfall-Ereignis wird nach 10 Sekunden ohne Antwort abgebrochen. Antworten Sie also sofort und verarbeiten Sie danach in einer Warteschlange oder einem Hintergrundjob.

Entfernen Sie Duplikate anhand von X-Pathly-Id. In der Konsole sendet die Schaltfläche „Letztes Event erneut senden“ eine Zustellung mit derselben Kennung, einem neuen Zeitstempel und einer neuen Signatur. Speichern Sie die Kennung mit einer Eindeutigkeitsbedingung und ignorieren Sie eine bereits verarbeitete.

Machen Sie den Webhook nicht zu Ihrer einzigen Quelle. Lesen Sie nach einem Ausfall auf Ihrer Seite GET /v1/incidents (Scope incidents:read) oder GET /v1/runs (Scope runs:read) erneut und vergleichen Sie mit dem, was Sie erhalten haben. Ein geplanter Job, der das regelmäßig tut, holt ein verpasstes Ereignis nach.

Ein mit POST /v1/hooks angelegtes Abonnement wird entfernt, wenn sein Ziel auf ein Vorfall-Ereignis mit 410 antwortet. Ein mit POST /v1/webhooks angelegter Webhook wird durch eine Fehlerantwort nie deaktiviert.

Ein verpasstes Ereignis per API-Abfrage nachholen

# Incidents (scope incidents:read)
curl -sS "https://api.pathlyhq.com/v1/incidents?limit=50" \
  -H "Authorization: Bearer $PATHLY_API_TOKEN"

# Runs of one scenario (scope runs:read)
curl -sS "https://api.pathlyhq.com/v1/runs?scenarioId=$SCENARIO_ID&limit=20" \
  -H "Authorization: Bearer $PATHLY_API_TOKEN"

Lokal testen

Sie müssen nicht auf einen echten Ausfall warten. Das Skript unten signiert einen Test-Body so, wie Pathly es tut, und sendet ihn an Ihren Empfänger. Ersetzen Sie die Adresse durch Ihre eigene und das Geheimnis durch das Ihres Test-Webhooks.

printf schreibt den Zeitstempel, einen Punkt und dann den Body, ohne abschließenden Zeilenumbruch. openssl berechnet den HMAC-SHA256 mit dem Geheimnis. curl sendet dank --data-binary genau denselben Body. Das Skript gibt den erhaltenen HTTP-Code aus: Erwartet wird ein 2xx.

Prüfen Sie danach die drei Ablehnungen. Ändern Sie nach dem Signieren ein Zeichen im Body: Der Empfänger muss mit 401 antworten. Setzen Sie einen eine Stunde alten Zeitstempel: ebenfalls 401. Führen Sie das Skript zweimal aus: Die zweite Zustellung trägt dieselbe X-Pathly-Id und darf keine Verarbeitung auslösen.

Für einen Ende-zu-Ende-Test sendet die Schaltfläche „Letztes Event erneut senden“ in den Einstellungen das zuletzt zugestellte Ereignis erneut an Ihren Webhook. Sie hat nichts zu senden, solange noch kein Ereignis an diesen Webhook gegangen ist.

Test-Body signieren und senden

#!/usr/bin/env bash
set -euo pipefail

SECRET="whsec_exemple" # the secret of your test webhook
URL="https://www.example.com/hooks/pathly"
ID="00000000-0000-4000-8000-000000000001"
TS=$(date +%s)
BODY='{"id":"'$ID'","event":"run.failed","monitorName":"Checkout","status":"fail","previousStatus":"ok","message":"test","latencyMs":0,"region":"eu-west","orgName":"Test"}'

# HMAC-SHA256 of "<timestamp>.<raw body>", in hexadecimal
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')

curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$URL" \
  -H "Content-Type: application/json" \
  -H "X-Pathly-Id: $ID" \
  -H "X-Pathly-Event: run.failed" \
  -H "X-Pathly-Timestamp: $TS" \
  -H "X-Pathly-Signature: sha256=$SIG" \
  --data-binary "$BODY"

Das Signaturgeheimnis

Das Geheimnis wird nur einmal ausgegeben: im Feld secret der Antwort auf POST /v1/webhooks oder auf dem Bildschirm, wenn Sie den Webhook in der Konsole anlegen. Es beginnt mit whsec_. Danach liefert es kein Lesezugriff mehr: GET /v1/webhooks meldet nur hasSecret.

Legen Sie es sofort in einem Secret-Manager ab und übergeben Sie es dem Empfänger über eine Umgebungsvariable. Schreiben Sie es nie in ein Protokoll, ein Ticket, ein Repository oder eine CI-Ausgabe. Protokollieren Sie auch nicht die gesamte Antwort der Erstellung: Sie enthält das Geheimnis.

Jeder Webhook hat sein eigenes Geheimnis, und kein Endpunkt erzeugt es neu. Geheimnis verloren oder offengelegt: Löschen Sie den Webhook, legen Sie ihn neu an und geben Sie dem Empfänger das neue Geheimnis. Zwischen Löschen und Anlegen wird nichts zugestellt: Holen Sie diese Lücke per API-Abfrage nach.

Anlegen: Das Geheimnis steht nur in dieser Antwort

curl -sS -X POST https://api.pathlyhq.com/v1/webhooks \
  -H "Authorization: Bearer $PATHLY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.example.com/hooks/pathly",
    "events": ["run.failed", "run.recovered"]
  }'

# 201
# {
#   "id": "…",
#   "events": ["run.failed", "run.recovered"],
#   "enabled": true,
#   "hasSecret": true,
#   "urlFingerprint": "…",
#   "createdAt": "…",
#   "secret": "whsec_…"
# }

Geheimnis verloren: löschen, dann neu anlegen

# Scope alerting:write. Then run the creation call above again.
curl -sS -X DELETE https://api.pathlyhq.com/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $PATHLY_API_TOKEN"