Klantly Developers

Webhooks

Recevez immédiatement un message sur votre serveur dès qu'un changement a lieu dans Klantly, et vérifiez qu'il provient bien de Klantly.

Avec les webhooks, plus besoin de demander sans cesse si quelque chose a changé. Klantly envoie lui-même un message à une adresse de votre serveur, par exemple dès qu'un prospect arrive ou qu'un deal est gagné.

Klantly suit le standard ouvert Standard Webhooks. Si vous utilisez déjà une bibliothèque compatible, vous pouvez l'employer telle quelle.

Créer un endpoint

Un endpoint est l'adresse à laquelle les messages sont envoyés. Vous le créez de l'une de ces deux façons :

  • Dans Klantly : allez dans Intégrations → API, ouvrez l'onglet Webhooks et choisissez Nouvel endpoint.
  • Via l'API : avec Créer un endpoint de webhook et une clé disposant du scope webhooks.manage.

Choisissez les événements que l'endpoint reçoit, ou tous les événements. Vous voyez ensuite une seule fois le secret, qui commence par whsec_. Il sert à vérifier la signature. Conservez-le aussi soigneusement qu'une clé API.

Remarque

Un endpoint ne reçoit jamais plus que ce que son créateur a le droit de voir. Si vous le créez dans Klantly, vos propres droits s'appliquent. Via l'API, ce sont les scopes de lecture de la clé : customers.read pour les clients et deals.read pour les deals. Une note suit le client ou le deal auquel elle appartient.

Vous pouvez créer 10 endpoints au maximum par entreprise. L'adresse doit utiliser https, sur le port 443, 80 ou 8443, et ne doit pas pointer vers un réseau interne.

Ce que vous recevez

Chaque message est un POST avec un corps JSON de la même forme que Récupérer un événement. Sous data.object figure l'objet exactement tel que l'API REST le renvoie.

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"
    }
  }
}

Chaque message est accompagné de ces en-têtes :

En-tête Contenu
webhook-id L'id de l'événement (evt_…). Identique à chaque nouvelle tentative.
webhook-timestamp Le moment de l'envoi, en secondes depuis 1970 (heure Unix).
webhook-signature La signature, par exemple v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=.
Content-Type application/json
User-Agent Klantly-Webhooks/1.0

Événements

Événement Description Scope
customer.created Un client ou un prospect a été créé : dans Klantly, via un formulaire ou via l'API. customers.read
customer.updated Les données d'un client ont changé. customers.read
customer.converted Un prospect est devenu client. customers.read
deal.created Un deal a été placé sur le tableau de pipeline. deals.read
deal.updated Les données d'un deal ont changé, comme le titre, la valeur ou le responsable. deals.read
deal.stage_changed Un deal a été déplacé vers une autre étape. deals.read
deal.won Un deal a été gagné. deals.read
deal.lost Un deal a été perdu. deals.read
note.created Une note a été ajoutée à un client ou à un deal. customers.read, deals.read
note.updated Une note a été modifiée. customers.read, deals.read
note.deleted Une note a été supprimée. customers.read, deals.read

Dans la version 1, des événements peuvent uniquement s'ajouter. Un endpoint avec tous les événements (*) reçoit automatiquement les nouveaux : traitez donc proprement un type inconnu.

Vérifier la signature

Vérifiez toujours la signature en premier, avant de faire quoi que ce soit avec un message. Vous êtes ainsi certain qu'il provient de Klantly et qu'il n'a pas été modifié en route.

  1. Prenez le corps exactement tel que vous l'avez reçu, avant de le lire comme JSON. Après conversion, la signature ne correspond plus.
  2. Construisez le texte {webhook-id}.{webhook-timestamp}.{body}.
  3. Calculez dessus un HMAC-SHA256. La clé est la partie du secret après whsec_, décodée depuis le base64.
  4. Encodez le résultat en base64 et comparez-le aux signatures de webhook-signature. Cet en-tête contient une ou plusieurs signatures de la forme v1,<signature>, séparées par une espace. Si l'une d'elles correspond, le message est authentique.
  5. Refusez un message si webhook-timestamp s'écarte de plus de 5 minutes de votre propre horloge. Ainsi, personne ne peut rejouer un ancien message.

Comparez en temps constant, avec hash_equals, crypto.timingSafeEqual ou hmac.compare_digest. La signature ne peut alors pas être devinée à partir du temps de réponse.

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);
// Traitez ici $event['type'] et $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 garde le corps tel qu'il est arrivé.
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'));
  // Traitez ici event.type et 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()
    # Traitez ici event["type"] et event["data"]["object"].
    return "", 204

Réponses et nouvelles tentatives

Répondez dans les 10 secondes avec un statut de la plage 200. Faites de préférence le vrai travail ensuite, par exemple via une file d'attente : une réponse trop tardive compte comme un échec. Klantly ne suit pas les redirections.

Si la livraison échoue, Klantly réessaie après 5 secondes, 5 minutes, 30 minutes, 2 heures, 5 heures, 10 heures et encore 10 heures. Cela fait huit tentatives en 27 heures environ.

Si un endpoint ne renvoie que des erreurs pendant 5 jours, Klantly le désactive et les administrateurs de l'entreprise reçoivent un e-mail. Réactivez-le dans Klantly, ou avec Modifier un endpoint de webhook et "status": "active". Dans Klantly, vous voyez pour chaque endpoint les tentatives de livraison des 30 derniers jours, et vous pouvez renvoyer un message.

Messages en double et ordre

  • Un message peut arriver plus d'une fois, par exemple si votre réponse s'est perdue en route. Retenez le webhook-id des messages traités et ignorez un id que vous connaissez déjà.
  • L'ordre n'est pas garanti : une nouvelle tentative peut arriver après un événement plus récent. Comparez le updated_at de l'objet, ou récupérez l'objet via l'API si vous voulez être sûr de son dernier état.

Rattraper les événements manqués

Votre serveur était-il indisponible ? Avec Lister les événements, vous récupérez ce que vous avez manqué. Les événements sont conservés 30 jours.

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"

La clé a besoin pour cela du scope events.read et ne voit que les événements portant sur des données qu'elle peut lire. Le message de test ping n'y figure jamais.

Envoyer un message de test

Avec Tester dans Klantly, ou avec Envoyer un message de test via l'API, Klantly envoie immédiatement l'événement 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"
    }
  }
}

Un message de test est aussi envoyé à un endpoint désactivé et n'est pas renvoyé en cas d'échec. Vous vérifiez ainsi que votre serveur fonctionne à nouveau avant de réactiver l'endpoint.

Renouveler le secret

Le secret a fuité, ou vous souhaitez simplement le remplacer régulièrement ? Renouvelez-le dans Klantly ou avec Renouveler le secret, et choisissez un chevauchement. Tant qu'il dure, webhook-signature contient deux signatures : l'une avec l'ancien secret, l'autre avec le nouveau. Placez le nouveau secret dans votre récepteur avant la fin du chevauchement. Si le secret a fuité, ne choisissez pas de chevauchement.

Dernière mise à jour le 15 septembre 2026