Webhooks
Ontvang direct een bericht op je server zodra er in Klantly iets verandert, en controleer dat het echt van Klantly komt.
Met webhooks hoef je niet steeds te vragen of er iets is veranderd. Klantly stuurt zelf een bericht naar een adres op jouw server, bijvoorbeeld zodra er een lead binnenkomt of een deal wordt gewonnen.
Klantly volgt de open standaard Standard Webhooks. Gebruik je al een bibliotheek die daarmee werkt, dan kun je die gewoon inzetten.
Een endpoint aanmaken
Een endpoint is het adres waar de berichten naartoe gaan. Je maakt het op een van twee manieren aan:
- In Klantly: ga naar Integraties → API, open het tabblad Webhooks en kies Nieuw endpoint.
- Via de API: met Webhook-endpoint aanmaken en een sleutel met de scope
webhooks.manage.
Kies welke events het endpoint ontvangt, of kies alle events. Daarna zie je eenmalig het secret, dat begint met whsec_. Daarmee controleer je de handtekening. Bewaar het net zo zorgvuldig als een API-sleutel.
Let op
Een endpoint ontvangt nooit meer dan degene die hem aanmaakte mag zien. Maak je hem in Klantly aan, dan gelden jouw rechten. Via de API gelden de leesscopes van de sleutel: customers.read voor klanten en deals.read voor deals. Een notitie volgt de klant of deal waar hij bij hoort.
Per bedrijf kun je maximaal 10 endpoints aanmaken. Het adres moet https gebruiken, op poort 443, 80 of 8443, en mag niet naar een intern netwerk wijzen.
Wat je ontvangt
Elk bericht is een POST met een JSON-body in dezelfde vorm als Event ophalen. Onder data.object staat het object precies zoals de REST-API het teruggeeft.
{
"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"
}
}
}Bij elk bericht horen deze headers:
| Header | Inhoud |
|---|---|
webhook-id |
De id van het event (evt_…). Bij elke nieuwe poging hetzelfde. |
webhook-timestamp |
Het moment van versturen, in seconden sinds 1970 (Unix-tijd). |
webhook-signature |
De handtekening, bijvoorbeeld v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=. |
Content-Type |
application/json |
User-Agent |
Klantly-Webhooks/1.0 |
Events
| Event | Omschrijving | Scope |
|---|---|---|
customer.created |
Een klant of lead is aangemaakt: in Klantly, via een formulier of via de API. | customers.read |
customer.updated |
Gegevens van een klant zijn gewijzigd. | customers.read |
customer.converted |
Een lead is klant geworden. | customers.read |
deal.created |
Een deal is op het pipelinebord gezet. | deals.read |
deal.updated |
Gegevens van een deal zijn gewijzigd, zoals titel, waarde of eigenaar. | deals.read |
deal.stage_changed |
Een deal is naar een andere fase verplaatst. | deals.read |
deal.won |
Een deal is gewonnen. | deals.read |
deal.lost |
Een deal is verloren. | deals.read |
note.created |
Een notitie is geplaatst bij een klant of deal. | customers.read, deals.read |
note.updated |
Een notitie is gewijzigd. | customers.read, deals.read |
note.deleted |
Een notitie is verwijderd. | customers.read, deals.read |
Binnen versie 1 komen er alleen events bij. Een endpoint met alle events (*) ontvangt nieuwe events vanzelf, dus vang een onbekend type netjes op.
De handtekening controleren
Controleer altijd eerst de handtekening, voordat je iets met een bericht doet. Zo weet je zeker dat het van Klantly komt en onderweg niet is veranderd.
- Neem de body precies zoals je hem ontving, voordat je hem als JSON leest. Na het omzetten klopt de handtekening niet meer.
- Maak de tekst
{webhook-id}.{webhook-timestamp}.{body}. - Bereken daarover een HMAC-SHA256. De sleutel is het deel van het secret na
whsec_, gedecodeerd uit base64. - Codeer de uitkomst in base64 en vergelijk hem met de handtekeningen in
webhook-signature. Daar staan er een of meer in, van de vormv1,<handtekening>en gescheiden door een spatie. Klopt er één, dan is het bericht echt. - Weiger een bericht als
webhook-timestampmeer dan 5 minuten van je eigen klok afwijkt. Zo kan niemand een oud bericht opnieuw afspelen.
Vergelijk in constante tijd, met hash_equals, crypto.timingSafeEqual of hmac.compare_digest. Dan is de handtekening niet te raden aan de hand van de reactietijd.
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);
// Verwerk hier $event['type'] en $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: de body blijft zoals hij binnenkwam.
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'));
// Verwerk hier event.type en 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()
# Verwerk hier event["type"] en event["data"]["object"].
return "", 204Antwoorden en nieuwe pogingen
Antwoord binnen 10 seconden met een status in de 200-reeks. Doe het echte werk liever daarna, bijvoorbeeld via een wachtrij: een te laat antwoord telt als mislukt. Redirects volgt Klantly niet.
Mislukt de aflevering, dan probeert Klantly het opnieuw na 5 seconden, 5 minuten, 30 minuten, 2 uur, 5 uur, 10 uur en nog eens 10 uur. Dat zijn acht pogingen in ongeveer 27 uur.
Geeft een endpoint 5 dagen lang alleen fouten, dan zet Klantly hem uit en krijgen de beheerders van het bedrijf een e-mail. Zet hem weer aan in Klantly, of met Webhook-endpoint bijwerken en "status": "active". In Klantly zie je per endpoint de afleverpogingen van de afgelopen 30 dagen, en kun je een bericht opnieuw versturen.
Dubbele berichten en volgorde
- Een bericht kan meer dan eens aankomen, bijvoorbeeld als je antwoord onderweg verloren ging. Onthoud de
webhook-idvan verwerkte berichten en sla een id over die je al kent. - De volgorde is niet gegarandeerd: een nieuwe poging kan na een later event aankomen. Vergelijk
updated_atvan het object, of haal het object op via de API als je zeker wilt zijn van de laatste stand.
Gemiste events inhalen
Lag je server eruit? Met Events opvragen haal je op wat je hebt gemist. Events blijven 30 dagen bewaard.
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"De sleutel heeft daarvoor de scope events.read nodig en ziet alleen events over gegevens waar hij leesrecht op heeft. Het testbericht ping staat er nooit tussen.
Een testbericht versturen
Met Testen in Klantly, of met Testbericht versturen via de API, stuurt Klantly direct het 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"
}
}
}Een testbericht gaat ook naar een uitgeschakeld endpoint en wordt niet herhaald. Zo controleer je of je server weer werkt, voordat je het endpoint aanzet.
Het secret vernieuwen
Is het secret uitgelekt, of wil je het gewoon regelmatig vervangen? Vernieuw het in Klantly of met Secret vernieuwen, en kies een overlap. Zolang die loopt, staan er in webhook-signature twee handtekeningen: een met het oude en een met het nieuwe secret. Zet het nieuwe secret in je ontvanger voordat de overlap voorbij is. Bij een uitgelekt secret kies je geen overlap.
Laatst bijgewerkt op 15 september 2026