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, deals.read pour les deals, appointments.read pour les rendez-vous, tasks.read pour les tâches, work_orders.read pour les bons d'intervention, quotes.read pour les devis, invoices.read pour les factures et les avoirs, orders.read pour les commandes de la boutique en ligne, messages.read pour les conversations et les messages et reviews.read pour les avis. Pour tout sauf les clients, les conversations et les messages, l'entreprise doit aussi disposer de la fonctionnalité correspondante. Une note suit le client ou le deal auquel elle appartient. L'endpoint reste lié à cette clé : si vous la révoquez ou lui retirez un scope de lecture, l'endpoint ne reçoit plus ces événements. Modifier, désactiver, supprimer ou renouveler le secret nécessite une clé qui peut lire tout ce que l'endpoint reçoit, et un webhook ne peut jamais pointer vers Klantly lui-même, même via une adresse IP. Les administrateurs reçoivent un e-mail lorsqu'un endpoint est ajouté, change d'adresse ou reçoit un nouveau secret.

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
customer.deleted Un client a été supprimé. Supprimez-le aussi dans votre propre système : une suppression n'apparaît jamais via filter[updated_since]. 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.labels_changed Les étiquettes d’une affaire ont changé ; la nouvelle liste est dans labels. deals.read
deal.won Un deal a été gagné. deals.read
deal.lost Un deal a été perdu. deals.read
deal.deleted Un deal a été supprimé. 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
appointment.created Un rendez-vous a été planifié : dans l’agenda, via la page de réservation, l’application ou l’API. appointments.read
appointment.updated Les données d’un rendez-vous ont changé, par exemple son heure (déplacé). appointments.read
appointment.confirmed Un rendez-vous a été confirmé. appointments.read
appointment.cancelled Un rendez-vous a été annulé. appointments.read
appointment.completed Un rendez-vous a été terminé. appointments.read
appointment.deleted Un rendez-vous a été supprimé. appointments.read
task.created Une tâche a été créée. tasks.read
task.updated Les données d’une tâche ont changé, comme le titre, l’échéance, la priorité ou les liens. tasks.read
task.moved Une tâche a été déplacée vers une autre colonne. tasks.read
task.labels_changed Les étiquettes propres d’une tâche ont changé ; la nouvelle liste est dans labels. tasks.read
task.completed Une tâche a été terminée. tasks.read
task.deleted Une tâche a été supprimée. tasks.read
quote.created Un devis a été rédigé : dans Klantly, via le configurateur ou via l'API. quotes.read
quote.updated Les données ou les lignes d'un devis ont changé. quotes.read
quote.sent Un devis a été envoyé au client. quotes.read
quote.accepted Un devis a été accepté. quotes.read
quote.rejected Un devis a été refusé. quotes.read
quote.deleted Un devis a été supprimé. quotes.read
invoice.created Une facture a été créée : dans Klantly, à partir d'un devis ou d'un bon d'intervention, ou via l'API. Un nouvel avoir arrive aussi comme cet événement, avec type credit_note. invoices.read
invoice.updated Les données ou les lignes d'une facture ont changé. invoices.read
invoice.sent Une facture a été envoyée au client. invoices.read
invoice.paid Une facture a été entièrement payée — que ce soit enregistré en ligne, à la main ou via l'API. invoices.read
invoice.cancelled Une facture a été annulée. invoices.read
invoice.credited Un avoir a été créé sur une facture. L'événement porte sur la facture (avec credited_amount, credit_note_ids et le nouveau statut) ; l'avoir lui-même arrive comme invoice.created avec type credit_note. invoices.read
invoice.deleted Une facture a été supprimée. invoices.read
order.created Une commande a été passée dans la boutique ou le configurateur. orders.read
order.updated Une commande a été modifiée, par exemple son suivi de colis. orders.read
order.status_changed Une commande a changé de statut. orders.read
order.paid Une commande a été payée. orders.read
work_order.created Un bon d’intervention a été créé. work_orders.read
work_order.updated Les données d’un bon d’intervention ont changé, comme les lignes, la check-list ou la planification. work_orders.read
work_order.status_changed Le statut d’un bon d’intervention a changé. work_orders.read
work_order.completed Un bon d’intervention a été terminé. work_orders.read
work_order.signed Un bon d’intervention a été signé par le client. work_orders.read
work_order.deleted Un bon d’intervention a été supprimé. work_orders.read
service.created Une demande de service a été créée. services.read
service.updated Des informations d’une demande de service ont changé, comme la description, l’attribution ou la planification. services.read
service.status_changed Le statut d’une demande de service a changé. services.read
service.completed Une demande de service a été clôturée. services.read
service.signed Une demande de service a été signée par le client. services.read
service.deleted Une demande de service a été supprimée. services.read
conversation.created Une conversation a commencé : par un client via le chat, WhatsApp ou l'e-mail, ou par l'entreprise. messages.read
conversation.closed Une conversation a été clôturée. messages.read
message.received Un client a envoyé un message. messages.read
message.sent L'entreprise a envoyé un message, depuis la boîte de réception, l'application, l'API ou l'agent IA. messages.read
message.not_delivered WhatsApp n'a pas remis un message envoyé. Meta le signale parfois quelques minutes plus tard ; le message apparaît comme non remis dans la conversation. Souvent le numéro ne peut pas recevoir WhatsApp, ou le dernier message du client date de plus de 24 heures. messages.read
review.received Un avis est arrivé : via une demande d'avis, depuis Google ou saisi par l'entreprise. reviews.read
review.replied L'entreprise a répondu à un avis. reviews.read
review.updated Un avis a changé : masqué, affiché à nouveau, ou son texte, sa note ou le consentement a été modifié. Un miroir sur votre site doit le reprendre. reviews.read
review.deleted Un avis a été supprimé. reviews.read
conversation.reopened Une conversation fermée a été rouverte, par exemple parce que le client a finalement répondu. messages.read
conversation.deleted Une conversation a été supprimée. messages.read
meta_copilot.alert_created Le copilote Meta a une nouvelle alerte : réactions perdues, refus, pic de dépenses, diffusion arrêtée, connexion expirante ou modification hors de Klantly. meta_copilot.read
meta_copilot.stopped Le copilote Meta a été arrêté par l'arrêt d'urgence, dans l'application ou via l'API. meta_copilot.read
meta_copilot.opportunity_created Le copilote Meta a trouvé une nouvelle opportunité : quelque chose qui économise ou rapporte de l'argent, avec les chiffres et l'étape qu'il propose. meta_copilot.read
meta_copilot.approval_requested Le copilote Meta a préparé une proposition qui attend la décision de l'entreprise, avec une échéance. meta_copilot.read
meta_copilot.approval_expiring Une proposition du copilote Meta est sur le point d'expirer sans décision. meta_copilot.read
meta_copilot.action_applied Une proposition du copilote Meta a été appliquée chez Meta (par l'entreprise, via l'API, le lien dans l'e-mail ou de façon autonome dans le cadre du mandat). meta_copilot.read
meta_copilot.action_failed Une proposition du copilote Meta n'a pas pu être appliquée ; la raison figure dans error. meta_copilot.read
meta_copilot.briefing_created Le copilote Meta a rédigé le briefing du matin d'hier : dépenses, leads, devis et affaires gagnées comparés à la moyenne hebdomadaire, avec le type de journée et les jalons du matin. meta_copilot.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 18 septembre 2026