Klantly Developers

Webhooks

Erhalten Sie sofort eine Nachricht auf Ihrem Server, sobald sich in Klantly etwas ändert, und prüfen Sie, dass sie wirklich von Klantly kommt.

Mit Webhooks müssen Sie nicht ständig nachfragen, ob sich etwas geändert hat. Klantly sendet selbst eine Nachricht an eine Adresse auf Ihrem Server, zum Beispiel sobald ein Lead eingeht oder ein Deal gewonnen wird.

Klantly folgt dem offenen Standard Standard Webhooks. Nutzen Sie bereits eine Bibliothek, die ihn unterstützt, können Sie diese unverändert einsetzen.

Einen Endpunkt erstellen

Ein Endpunkt ist die Adresse, an die die Nachrichten gesendet werden. Sie erstellen ihn auf einem von zwei Wegen:

  • In Klantly: Gehen Sie zu Integrationen → API, öffnen Sie den Tab Webhooks und wählen Sie Neuer Endpunkt.
  • Über die API: mit Webhook-Endpunkt erstellen und einem Schlüssel mit dem Scope webhooks.manage.

Wählen Sie, welche Events der Endpunkt empfängt, oder wählen Sie alle Events. Danach sehen Sie einmalig das Secret, das mit whsec_ beginnt. Damit prüfen Sie die Signatur. Bewahren Sie es so sorgfältig auf wie einen API-Schlüssel.

Hinweis

Ein Endpunkt empfängt nie mehr, als die Person sehen darf, die ihn erstellt hat. Erstellen Sie ihn in Klantly, gelten Ihre eigenen Rechte. Über die API gelten die Lese-Scopes des Schlüssels: customers.read für Kunden, deals.read für Deals, appointments.read für Termine, tasks.read für Aufgaben, work_orders.read für Arbeitsaufträge, quotes.read für Angebote, invoices.read für Rechnungen und Gutschriften, orders.read für Webshop-Bestellungen, messages.read für Unterhaltungen und Nachrichten und reviews.read für Bewertungen. Für alles außer Kunden, Unterhaltungen und Nachrichten braucht das Unternehmen auch die jeweilige Funktion. Eine Notiz folgt dem Kunden oder Deal, zu dem sie gehört. Der Endpunkt bleibt an diesen Schlüssel gebunden: Widerrufen Sie ihn oder entfernen Sie einen Lese-Scope, erhält der Endpunkt diese Events nicht mehr. Ändern, Ausschalten, Löschen oder ein neues Secret erfordert einen Schlüssel, der alles lesen kann, was der Endpunkt erhält, und ein Webhook kann nie auf Klantly selbst zeigen, auch nicht über eine IP-Adresse. Administratoren erhalten eine E-Mail, wenn ein Endpunkt hinzukommt, eine andere Adresse oder ein neues Secret erhält.

Pro Unternehmen können Sie höchstens 10 Endpunkte erstellen. Die Adresse muss https verwenden, auf Port 443, 80 oder 8443, und darf nicht auf ein internes Netzwerk zeigen.

Was Sie empfangen

Jede Nachricht ist ein POST mit einem JSON-Body in derselben Form wie Event abrufen. Unter data.object steht das Objekt genau so, wie die REST-API es zurückgibt.

customer.created
{
  "object": "event",
  "id": "evt_01j7zs1a2b3c4d5e6f7g8h9j0k",
  "type": "customer.created",
  "created_at": "2026-09-14T10:15:00Z",
  "data": {
    "object": {
      "object": "customer",
      "id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
      "type": "business",
      "status": "lead",
      "name": "Jan de Vries",
      "email": "jan@example.com",
      "phone": "+31 6 12345678",
      "company_name": "De Vries Bouw",
      "vat_number": null,
      "coc_number": null,
      "address": null,
      "postal_code": null,
      "city": "Utrecht",
      "country": "NL",
      "email_unsubscribed": false,
      "converted_at": null,
      "last_activity_at": "2026-09-14T10:15:00Z",
      "created_at": "2026-09-14T10:15:00Z",
      "updated_at": "2026-09-14T10:15:00Z"
    }
  }
}

Zu jeder Nachricht gehören diese Header:

Header Inhalt
webhook-id Die ID des Events (evt_…). Bei jedem neuen Versuch gleich.
webhook-timestamp Der Zeitpunkt des Versands, in Sekunden seit 1970 (Unix-Zeit).
webhook-signature Die Signatur, zum Beispiel v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=.
Content-Type application/json
User-Agent Klantly-Webhooks/1.0

Events

Event Beschreibung Scope
customer.created Ein Kunde oder Lead wurde erstellt: in Klantly, über ein Formular oder über die API. customers.read
customer.updated Daten eines Kunden wurden geändert. customers.read
customer.converted Ein Lead wurde zum Kunden. customers.read
customer.deleted Ein Kunde wurde gelöscht. Entfernen Sie ihn auch in Ihrem eigenen System: über filter[updated_since] ist eine Löschung nie sichtbar. customers.read
deal.created Ein Deal wurde auf das Pipeline-Board gesetzt. deals.read
deal.updated Daten eines Deals wurden geändert, etwa Titel, Wert oder Verantwortlicher. deals.read
deal.stage_changed Ein Deal wurde in eine andere Phase verschoben. deals.read
deal.labels_changed Die Labels eines Deals wurden geändert; die neue Liste steht in labels. deals.read
deal.won Ein Deal wurde gewonnen. deals.read
deal.lost Ein Deal wurde verloren. deals.read
deal.deleted Ein Deal wurde gelöscht. deals.read
note.created Eine Notiz wurde zu einem Kunden oder Deal hinzugefügt. customers.read, deals.read
note.updated Eine Notiz wurde geändert. customers.read, deals.read
note.deleted Eine Notiz wurde gelöscht. customers.read, deals.read
appointment.created Ein Termin wurde geplant: im Kalender, über die Buchungsseite, die App oder die API. appointments.read
appointment.updated Daten eines Termins wurden geändert, zum Beispiel die Zeit (verschoben). appointments.read
appointment.confirmed Ein Termin wurde bestätigt. appointments.read
appointment.cancelled Ein Termin wurde abgesagt. appointments.read
appointment.completed Ein Termin wurde abgeschlossen. appointments.read
appointment.deleted Ein Termin wurde gelöscht. appointments.read
task.created Eine Aufgabe wurde erstellt. tasks.read
task.updated Daten einer Aufgabe wurden geändert, etwa Titel, Frist, Priorität oder Verknüpfungen. tasks.read
task.moved Eine Aufgabe wurde in eine andere Spalte verschoben. tasks.read
task.labels_changed Die eigenen Labels einer Aufgabe wurden geändert; die neue Liste steht in labels. tasks.read
task.completed Eine Aufgabe wurde abgeschlossen. tasks.read
task.deleted Eine Aufgabe wurde gelöscht. tasks.read
quote.created Ein Angebot wurde erstellt: in Klantly, über den Konfigurator oder über die API. quotes.read
quote.updated Daten oder Positionen eines Angebots wurden geändert. quotes.read
quote.sent Ein Angebot wurde an den Kunden gesendet. quotes.read
quote.accepted Ein Angebot wurde angenommen. quotes.read
quote.rejected Ein Angebot wurde abgelehnt. quotes.read
quote.deleted Ein Angebot wurde gelöscht. quotes.read
invoice.created Eine Rechnung wurde erstellt: in Klantly, aus einem Angebot oder Arbeitsauftrag, oder über die API. Eine neue Gutschrift kommt ebenfalls als dieses Event, mit type credit_note. invoices.read
invoice.updated Daten oder Positionen einer Rechnung wurden geändert. invoices.read
invoice.sent Eine Rechnung wurde an den Kunden gesendet. invoices.read
invoice.paid Eine Rechnung wurde vollständig bezahlt — ob online, von Hand oder über die API erfasst. invoices.read
invoice.cancelled Eine Rechnung wurde storniert. invoices.read
invoice.credited Zu einer Rechnung wurde eine Gutschrift erstellt. Das Event betrifft die Rechnung (mit credited_amount, credit_note_ids und dem neuen Status); die Gutschrift selbst kommt als invoice.created mit type credit_note. invoices.read
invoice.deleted Eine Rechnung wurde gelöscht. invoices.read
order.created Eine Bestellung wurde im Webshop oder Konfigurator aufgegeben. orders.read
order.updated Eine Bestellung wurde geändert, zum Beispiel die Sendungsverfolgung. orders.read
order.status_changed Eine Bestellung hat einen anderen Status erhalten. orders.read
order.paid Eine Bestellung wurde bezahlt. orders.read
work_order.created Ein Arbeitsauftrag wurde erstellt. work_orders.read
work_order.updated Daten eines Arbeitsauftrags wurden geändert, etwa Positionen, Checkliste oder Planung. work_orders.read
work_order.status_changed Der Status eines Arbeitsauftrags wurde geändert. work_orders.read
work_order.completed Ein Arbeitsauftrag wurde abgeschlossen. work_orders.read
work_order.signed Ein Arbeitsauftrag wurde vom Kunden unterschrieben. work_orders.read
work_order.deleted Ein Arbeitsauftrag wurde gelöscht. work_orders.read
service.created Eine Servicemeldung wurde erstellt. services.read
service.updated Angaben einer Servicemeldung haben sich geändert, etwa die Beschreibung, die Zuweisung oder die Planung. services.read
service.status_changed Der Status einer Servicemeldung hat sich geändert. services.read
service.completed Eine Servicemeldung wurde abgeschlossen. services.read
service.signed Eine Servicemeldung wurde vom Kunden unterschrieben. services.read
service.deleted Eine Servicemeldung wurde gelöscht. services.read
conversation.created Eine Unterhaltung hat begonnen: durch einen Kunden über Chat, WhatsApp oder E-Mail, oder durch das Unternehmen. messages.read
conversation.closed Eine Unterhaltung wurde geschlossen. messages.read
message.received Ein Kunde hat eine Nachricht gesendet. messages.read
message.sent Das Unternehmen hat eine Nachricht gesendet, aus dem Posteingang, der App, der API oder vom KI-Agenten. messages.read
message.not_delivered WhatsApp hat eine gesendete Nachricht nicht zugestellt. Meta meldet das manchmal erst Minuten später; die Nachricht steht im Gespräch als nicht zugestellt. Meist kann die Nummer kein WhatsApp empfangen, oder die letzte Nachricht des Kunden ist mehr als 24 Stunden her. messages.read
review.received Eine Bewertung ist eingegangen: über eine Bewertungsanfrage, aus Google oder selbst erfasst. reviews.read
review.replied Das Unternehmen hat auf eine Bewertung geantwortet. reviews.read
review.updated Eine Bewertung wurde geändert: ausgeblendet, wieder angezeigt, oder Text, Bewertung oder Einwilligung wurden angepasst. Eine Spiegelung auf Ihrer Website übernimmt das. reviews.read
review.deleted Eine Bewertung wurde gelöscht. reviews.read
conversation.reopened Eine geschlossene Unterhaltung wurde wieder geöffnet, zum Beispiel weil der Kunde doch noch geantwortet hat. messages.read
conversation.deleted Eine Unterhaltung wurde gelöscht. messages.read
meta_copilot.alert_created Der Meta-Copilot hat eine neue Meldung: verlorene Reaktionen, eine Ablehnung, ein Ausgabensprung, gestoppte Auslieferung, eine ablaufende Verbindung oder eine Änderung außerhalb von Klantly. meta_copilot.read
meta_copilot.stopped Der Meta-Copilot wurde per Notstopp angehalten, in der App oder über die API. meta_copilot.read
meta_copilot.opportunity_created Der Meta-Copilot hat eine neue Chance gefunden: etwas, das Geld spart oder einbringt, mit den Zahlen und dem vorgeschlagenen Schritt. meta_copilot.read
meta_copilot.approval_requested Der Meta-Copilot hat einen Vorschlag bereitgestellt, der auf die Entscheidung des Unternehmens wartet, mit Frist. meta_copilot.read
meta_copilot.approval_expiring Ein Vorschlag des Meta-Copiloten läuft bald ohne Entscheidung ab. meta_copilot.read
meta_copilot.action_applied Ein Vorschlag des Meta-Copiloten wurde bei Meta umgesetzt (durch das Unternehmen, über die API, den Mail-Link oder autonom innerhalb des Mandats). meta_copilot.read
meta_copilot.action_failed Ein Vorschlag des Meta-Copiloten konnte nicht umgesetzt werden; der Grund steht in error. meta_copilot.read
meta_copilot.briefing_created Der Meta-Copilot hat das Morgenbriefing von gestern erstellt: Ausgaben, Leads, Angebote und gewonnene Aufträge gegen den Wochendurchschnitt, mit der Art des Tages und den Meilensteinen des Morgens. meta_copilot.read

Innerhalb von Version 1 kommen nur Events hinzu. Ein Endpunkt mit allen Events (*) empfängt neue Events automatisch; gehen Sie daher mit einem unbekannten Typ sauber um.

Die Signatur prüfen

Prüfen Sie immer zuerst die Signatur, bevor Sie etwas mit einer Nachricht tun. So wissen Sie sicher, dass sie von Klantly kommt und unterwegs nicht verändert wurde.

  1. Nehmen Sie den Body genau so, wie Sie ihn empfangen haben, bevor Sie ihn als JSON einlesen. Nach dem Umwandeln stimmt die Signatur nicht mehr.
  2. Bilden Sie den Text {webhook-id}.{webhook-timestamp}.{body}.
  3. Berechnen Sie darüber einen HMAC-SHA256. Der Schlüssel ist der Teil des Secrets nach whsec_, aus Base64 dekodiert.
  4. Kodieren Sie das Ergebnis in Base64 und vergleichen Sie es mit den Signaturen in webhook-signature. Dort stehen eine oder mehrere Signaturen der Form v1,<Signatur>, durch ein Leerzeichen getrennt. Stimmt eine davon, ist die Nachricht echt.
  5. Lehnen Sie eine Nachricht ab, wenn webhook-timestamp mehr als 5 Minuten von Ihrer eigenen Uhr abweicht. So kann niemand eine alte Nachricht erneut abspielen.

Vergleichen Sie in konstanter Zeit, mit hash_equals, crypto.timingSafeEqual oder hmac.compare_digest. Dann lässt sich die Signatur nicht anhand der Antwortzeit erraten.

PHP
function verifyKlantlyWebhook(string $body, array $headers, string $secret): bool
{
    $id = $headers['webhook-id'] ?? '';
    $timestamp = $headers['webhook-timestamp'] ?? '';
    $signatures = $headers['webhook-signature'] ?? '';

    if (! ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
        return false;
    }

    $key = base64_decode(substr($secret, strlen('whsec_')));
    $expected = base64_encode(hash_hmac('sha256', "{$id}.{$timestamp}.{$body}", $key, true));

    foreach (explode(' ', $signatures) as $signature) {
        [$version, $value] = array_pad(explode(',', $signature, 2), 2, '');

        if ($version === 'v1' && hash_equals($expected, $value)) {
            return true;
        }
    }

    return false;
}

$body = file_get_contents('php://input');
$headers = array_change_key_case(getallheaders(), CASE_LOWER);

if (! verifyKlantlyWebhook($body, $headers, getenv('KLANTLY_WEBHOOK_SECRET'))) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
// Verarbeiten Sie hier $event['type'] und $event['data']['object'].
http_response_code(204);
Node.js
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.KLANTLY_WEBHOOK_SECRET;

function verifyKlantlyWebhook(body, headers) {
  const id = headers['webhook-id'] ?? '';
  const timestamp = headers['webhook-timestamp'] ?? '';
  const signatures = headers['webhook-signature'] ?? '';

  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return false;
  }

  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.`).update(body).digest();

  return signatures.split(' ').some((signature) => {
    const [version, value = ''] = signature.split(',');
    const received = Buffer.from(value, 'base64');

    return version === 'v1' && received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

// express.raw lässt den Body so, wie er angekommen ist.
app.post('/webhooks/klantly', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyKlantlyWebhook(req.body, req.headers)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Verarbeiten Sie hier event.type und event.data.object.
  res.sendStatus(204);
});
Python
import base64
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["KLANTLY_WEBHOOK_SECRET"]


def verify_klantly_webhook(body: bytes, headers) -> bool:
    msg_id = headers.get("webhook-id", "")
    timestamp = headers.get("webhook-timestamp", "")
    signatures = headers.get("webhook-signature", "")

    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False

    key = base64.b64decode(SECRET.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()

    for signature in signatures.split(" "):
        version, _, value = signature.partition(",")
        if version == "v1" and hmac.compare_digest(expected, value):
            return True

    return False


@app.post("/webhooks/klantly")
def klantly_webhook():
    body = request.get_data()
    if not verify_klantly_webhook(body, request.headers):
        abort(401)

    event = request.get_json()
    # Verarbeiten Sie hier event["type"] und event["data"]["object"].
    return "", 204

Antworten und neue Versuche

Antworten Sie innerhalb von 10 Sekunden mit einem Status im 200er-Bereich. Erledigen Sie die eigentliche Arbeit am besten danach, zum Beispiel über eine Warteschlange: Eine zu späte Antwort gilt als fehlgeschlagen. Weiterleitungen folgt Klantly nicht.

Schlägt die Zustellung fehl, versucht Klantly es erneut nach 5 Sekunden, 5 Minuten, 30 Minuten, 2 Stunden, 5 Stunden, 10 Stunden und noch einmal 10 Stunden. Das sind acht Versuche in etwa 27 Stunden.

Liefert ein Endpunkt 5 Tage lang nur Fehler, schaltet Klantly ihn aus, und die Administratoren des Unternehmens erhalten eine E-Mail. Schalten Sie ihn in Klantly wieder ein, oder mit Webhook-Endpunkt bearbeiten und "status": "active". In Klantly sehen Sie pro Endpunkt die Zustellversuche der letzten 30 Tage und können eine Nachricht erneut senden.

Doppelte Nachrichten und Reihenfolge

  • Eine Nachricht kann mehr als einmal ankommen, zum Beispiel wenn Ihre Antwort unterwegs verloren ging. Merken Sie sich die webhook-id verarbeiteter Nachrichten und überspringen Sie eine ID, die Sie schon kennen.
  • Die Reihenfolge ist nicht garantiert: Ein neuer Versuch kann nach einem späteren Event ankommen. Vergleichen Sie updated_at des Objekts, oder rufen Sie das Objekt über die API ab, wenn Sie sich des aktuellen Stands sicher sein wollen.

Verpasste Events nachholen

War Ihr Server nicht erreichbar? Mit Events auflisten holen Sie nach, was Sie verpasst haben. Events werden 30 Tage aufbewahrt.

cURL
curl --globoff "https://app.klantly.com/api/v1/events?sort=created_at&filter[created_since]=2026-09-14T08:00:00Z" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"

Der Schlüssel braucht dafür den Scope events.read und sieht nur Events zu Daten, auf die er Leserechte hat. Die Testnachricht ping ist nie enthalten.

Eine Testnachricht senden

Mit Testen in Klantly, oder mit Testnachricht senden über die API, sendet Klantly sofort das Event ping:

ping
{
  "object": "event",
  "id": "evt_01j7zt4m6n8p0r2t4v6w8y0a2c",
  "type": "ping",
  "created_at": "2026-09-14T10:15:00Z",
  "data": {
    "object": {
      "object": "ping",
      "webhook_endpoint_id": "01j7zr8m2k4n6p8r0t2v4w6y8a",
      "message": "Klantly webhook test"
    }
  }
}

Eine Testnachricht geht auch an einen deaktivierten Endpunkt und wird nicht wiederholt. So prüfen Sie, ob Ihr Server wieder funktioniert, bevor Sie den Endpunkt einschalten.

Das Secret erneuern

Ist das Secret durchgesickert, oder möchten Sie es einfach regelmäßig ersetzen? Erneuern Sie es in Klantly oder mit Secret erneuern, und wählen Sie eine Überlappung. Solange sie läuft, stehen in webhook-signature zwei Signaturen: eine mit dem alten und eine mit dem neuen Secret. Hinterlegen Sie das neue Secret in Ihrem Empfänger, bevor die Überlappung endet. Ist das Secret durchgesickert, wählen Sie keine Überlappung.

Zuletzt aktualisiert am 18. September 2026