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.
{
"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.
- 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.
- Bilden Sie den Text
{webhook-id}.{webhook-timestamp}.{body}. - Berechnen Sie darüber einen HMAC-SHA256. Der Schlüssel ist der Teil des Secrets nach
whsec_, aus Base64 dekodiert. - Kodieren Sie das Ergebnis in Base64 und vergleichen Sie es mit den Signaturen in
webhook-signature. Dort stehen eine oder mehrere Signaturen der Formv1,<Signatur>, durch ein Leerzeichen getrennt. Stimmt eine davon, ist die Nachricht echt. - Lehnen Sie eine Nachricht ab, wenn
webhook-timestampmehr 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.
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);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);
});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 "", 204Antworten 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-idverarbeiteter 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_atdes 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 --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:
{
"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