de

Webhooks

Lassen Sie sich benachrichtigen, wenn in Crisphive etwas passiert — statt zu pollen, senden wir Ereignisse per POST an Ihre URL.

So funktioniert es

1
Registrieren Sie Ihren Endpunkt

Ein Teammitglied registriert Ihren HTTPS-Endpunkt im Crisphive-Dashboard und wählt aus, welche Ereignisse empfangen werden sollen (oder alle). Sie erhalten einmalig ein Signatur-Secret (whsec_…) — bewahren Sie es auf. Sie wählen hier auch die Laufzeit des Secrets — standardmäßig 30 Tage, von 1 bis 365 (siehe Secret-Laufzeit & Rotation).

2
Verifizierungs-Ping

Die Registrierung sendet sofort einen signierten Verifizierungs-Ping. Antworten Sie mit 2xx, und der Endpunkt wird direkt active; andernfalls bleibt er pending_verification (empfängt nichts), bis er die Verify-Prüfung aus dem Dashboard besteht. Ein Ändern der URL erfordert eine erneute Verifizierung.

3
Wir senden Ereignisse per POST

Wenn ein Ereignis eintritt, sendet Crisphive einen POST an Ihre URL mit einem JSON-Body und einem Crisphive-Signature-Header.

4
Verifizieren & bestätigen

Ihr Endpunkt verifiziert die Signatur, antwortet schnell mit 2xx und verarbeitet asynchron.

5
Wiederholungen & automatische Deaktivierung

Non-2xx / Timeout ⇒ wir wiederholen mit Backoff (1m, 5m, 30m, 2h, 6h). Nach 5 aufeinanderfolgenden Fehlern (jeder Erfolg setzt den Zähler zurück) wird der Endpunkt automatisch deaktiviert und die Inhaber/Administratoren des Unternehmens werden per E-Mail benachrichtigt. Die Reaktivierung erfordert eine erfolgreiche Verify-Prüfung, die auch den Zähler zurücksetzt.

6
Zustellverlauf

Jede Zustellung wird protokolliert — das Dashboard zeigt pro Endpunkt den vollständigen Zustellverlauf (Status, Versuche, letzter HTTP-Code/Fehler).

At-least-once-Zustellung. Eine Zustellung kann sich wiederholen — deduplizieren Sie anhand der Ereignis-id (Upsert, niemals blind anhängen).

Secret-Laufzeit & Rotation

Webhook-Signatur-Secrets laufen ab und werden an Ort und Stelle rotiert. Sie wählen die Laufzeit des Secrets, wenn Sie den Endpunkt registrieren — standardmäßig 30 Tage, von 1 bis 365. Anders als bei einem API-Schlüssel ist ein Webhook-Endpunkt eine einzelne registrierte URL und lässt sich nicht parallel duplizieren (dieselbe URL zweimal zu registrieren würde jedes Ereignis doppelt an Sie zustellen). Statt eines zweiten Endpunkts rotieren Sie daher das Secret:

Developers → Webhooks → Rotate secret stellt ein neues whsec_ aus und signiert 24 Stunden lang sowohl mit dem neuen als auch mit dem vorherigen Secret. Die Rotation startet außerdem die Laufzeit neu — Sie wählen beim Rotieren eine frische (standardmäßig 30 Tage, von 1 bis 365) — und ist damit zugleich der Erneuerungsweg.

Die Inhaber und Administratoren des Unternehmens werden 7 Tage bevor ein Signatur-Secret abläuft per E-Mail benachrichtigt, und erneut, sobald es abgelaufen ist. Endpunkte, die registriert wurden, bevor es Secret-Laufzeiten gab, tragen kein secret_expires_at und signieren weiter wie bisher — ihre erste Rotation bringt sie auf die Standard-Laufzeit.

Lässt man ein Secret ablaufen, wird der Endpunkt deaktiviert und empfängt keine Zustellungen mehr. Rotieren Sie zuerst und führen Sie dann Verify aus, um die Zustellungen wieder aufzunehmen — Verify auf einem abgelaufenen Secret liefert 409 WEBHOOK_SECRET_EXPIRED, statt den Endpunkt wieder scharfzuschalten.

Ereignis-Katalog

EreignistypWann es ausgelöst wird
job_request.createdEine Auftragsanfrage (Buchung) wurde erstellt — über die API, die öffentliche Buchungsseite oder das Dashboard.
job_request.confirmedDer Kunde hat ein Zeitfenster bestätigt; der Auftrag ist eingeplant (und ein Techniker/Team wurde zugewiesen).
job_request.assignedEin Techniker/Team wurde dem Auftrag zugewiesen oder neu zugewiesen.
job_request.completedDer Auftrag hat seinen endgültigen abgeschlossenen Status erreicht.
job_request.archivedDer Auftrag wurde archiviert (storniert / geschlossen).
job_request.status_changedJeder andere Workflow-Statusübergang (z. B. on_the_way, arrived, benutzerdefiniert). Enthält previous_status.
job_request.priority_changedDie Priorität des Auftrags wurde geändert.
job_request.rescheduledDer Auftrag wurde auf eine andere Zeit oder einen anderen Techniker/ein anderes Team verschoben.
customer.createdEin Kundendatensatz wurde erstellt.
customer.updatedDas Profil, der Kontakt, die Stufe oder der Status eines Kunden hat sich geändert.
customer.deletedEin Kundendatensatz wurde gelöscht.
technician.createdEin Techniker wurde in den Personalbestand aufgenommen (einschließlich des erneuten Hinzufügens eines zuvor entfernten Mitglieds).
technician.updatedDas Profil, die Rollengruppe, die Stufe oder der Status eines Technikers hat sich geändert. Reine Beziehungs-Schreibvorgänge (Buddies/Fahrzeuge/Skills/Servicegebiete) lösen keine Ereignisse aus.
technician.deletedEin Techniker wurde aus dem Personalbestand entfernt.

Eine Zustandsänderung löst genau ein Ereignis aus (den spezifischen Meilenstein, falls vorhanden, sonst status_changed). Der Katalog ist nur erweiterbar — neue Ereignistypen können hinzukommen; bestehende ändern niemals ihre Bedeutung.

Payload

Jeder Zustell-Body ist dieses Envelope. data.object ist die Ressource in derselben Form wie das passende /v1-GET (rufen Sie bei Bedarf /v1 für vollständige/aktuelle Details ab).

{
  "id": "evt_2b9c…",                       // unique event id — dedupe on this
  "type": "job_request.completed",
  "created_at": "2026-06-30T10:00:00Z",
  "business_id": "b1f0…",
  "environment": "live",                    // or "sandbox"
  "data": { "object": { "id": "job_…", "short_code": "REQ-…", "status": "completed" } },
  "previous_status": "arrived"              // only on job_request.status_changed
}

Signatur verifizieren

Header-Format: Crisphive-Signature: t=<unix>,v1=<hex> wobei v1 = HMAC_SHA256(secret, t + "." + rawBody). Berechnen Sie ihn über den rohen Request-Body, vergleichen Sie in konstanter Zeit und lehnen Sie ab, wenn t zu alt ist (z. B. > 5 Min.), um Replays zu verhindern.

Verifizieren Sie gegen eine Liste von Signaturen, nicht gegen einen einzelnen Wert. Während des 24-stündigen Rotations-Übergangsfensters enthält der Header zwei v1-Einträge:

Crisphive-Signature: t=1753857711,v1=<signature-new>,v1=<signature-old>

v1 ist die Version des Signaturalgorithmus, kein Secret-Identifikator. Berechnen Sie den HMAC mit Ihrem gespeicherten Secret und akzeptieren Sie die Zustellung, wenn irgendein v1-Eintrag übereinstimmt. Ein Empfänger, der fest verdrahtet nur den ersten v1-Eintrag liest, beginnt mitten in einer Rotation, Ereignisse abzulehnen.

Node.js

const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const parts = header.split(',').map(kv => kv.split('='));
  const t = parts.find(([k]) => k === 't')[1];
  // two v1 entries during the 24h rotation window — accept any match
  const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
  const expected = crypto.createHmac('sha256', secret)
    .update(t + '.' + rawBody).digest('hex');
  return sigs.some(v1 => v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)));
}

Python

import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
    pairs = [kv.split('=', 1) for kv in header.split(',')]
    t = next(v for k, v in pairs if k == 't')
    # two v1 entries during the 24h rotation window — accept any match
    sigs = [v for k, v in pairs if k == 'v1']
    expected = hmac.new(secret.encode(), t.encode() + b'.' + raw_body,
                        hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, v1) for v1 in sigs)

Go

func Verify(rawBody []byte, header, secret string) bool {
    var t string
    var sigs []string // two v1 entries during the 24h rotation window
    for _, kv := range strings.Split(header, ",") {
        kvp := strings.SplitN(kv, "=", 2)
        switch kvp[0] {
        case "t": t = kvp[1]
        case "v1": sigs = append(sigs, kvp[1])
        }
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(t + "."))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))
    for _, v1 := range sigs {
        if hmac.Equal([]byte(expected), []byte(v1)) {
            return true
        }
    }
    return false
}

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.*;

public static boolean verify(byte[] rawBody, String header, String secret) throws Exception {
    String t = null;
    List<String> sigs = new ArrayList<>(); // two v1 entries during the 24h rotation window
    for (String kv : header.split(",")) {
        String[] x = kv.split("=", 2);
        if (x[0].equals("t")) t = x[1];
        if (x[0].equals("v1")) sigs.add(x[1]);
    }
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
    mac.update((t + ".").getBytes());
    byte[] sig = mac.doFinal(rawBody);
    StringBuilder hex = new StringBuilder();
    for (byte b : sig) hex.append(String.format("%02x", b));
    byte[] expected = hex.toString().getBytes();
    for (String v1 : sigs) {
        if (MessageDigest.isEqual(expected, v1.getBytes())) return true;
    }
    return false;
}

C# / .NET

using System;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Text;

public static bool Verify(byte[] rawBody, string header, string secret) {
    string t = null;
    var sigs = new List<string>(); // two v1 entries during the 24h rotation window
    foreach (var kv in header.Split(',')) {
        var x = kv.Split('=', 2);
        if (x[0] == "t") t = x[1];
        if (x[0] == "v1") sigs.Add(x[1]);
    }
    var prefix = Encoding.UTF8.GetBytes(t + ".");
    var input = new byte[prefix.Length + rawBody.Length];
    Buffer.BlockCopy(prefix, 0, input, 0, prefix.Length);
    Buffer.BlockCopy(rawBody, 0, input, prefix.Length, rawBody.Length);
    using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var expected = Encoding.UTF8.GetBytes(
        Convert.ToHexString(mac.ComputeHash(input)).ToLowerInvariant());
    foreach (var v1 in sigs) {
        if (CryptographicOperations.FixedTimeEquals(
                expected, Encoding.UTF8.GetBytes(v1))) return true;
    }
    return false;
}

PHP

<?php
function verify(string $rawBody, string $header, string $secret): bool {
    $t = null;
    $sigs = []; // two v1 entries during the 24h rotation window
    foreach (explode(',', $header) as $kv) {
        [$k, $v] = explode('=', $kv, 2);
        if ($k === 't') { $t = $v; }
        if ($k === 'v1') { $sigs[] = $v; }
    }
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($sigs as $v1) {
        if (hash_equals($expected, $v1)) { return true; }
    }
    return false;
}

Ruby

require 'openssl'
def verify(raw_body, header, secret)
  pairs = header.split(',').map { |kv| kv.split('=', 2) }
  t = pairs.find { |k, _| k == 't' }[1]
  # two v1 entries during the 24h rotation window — accept any match
  sigs = pairs.select { |k, _| k == 'v1' }.map { |_, v| v }
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, t + '.' + raw_body)
  # constant-time compare (Rack::Utils.secure_compare / ActiveSupport::SecurityUtils.secure_compare)
  sigs.any? { |v1| Rack::Utils.secure_compare(expected, v1) }
end

Testen

Verwenden Sie Send test an Ihrem Endpunkt im Dashboard, um ein signiertes ping-Ereignis zu empfangen und zu bestätigen, dass Ihr Empfänger + die Signaturprüfung durchgängig funktionieren (nur diagnostisch — der Status ändert sich nie). Verwenden Sie Verify, um einen pending_verification- oder deaktivierten Endpunkt zu aktivieren: derselbe signierte Ping, aber ein 2xx schaltet ihn auf active und setzt den Fehlerzähler zurück. Ist das Signatur-Secret abgelaufen, verweigert Verify mit WEBHOOK_SECRET_EXPIRED — rotieren Sie zuerst das Secret.