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 und deals.read für Deals. Eine Notiz folgt dem Kunden oder Deal, zu dem sie gehört.

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
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.won Ein Deal wurde gewonnen. deals.read
deal.lost Ein Deal wurde verloren. 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

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 15. September 2026