Webhooks instellen en beveiligen
Statuswijzigingen van aanvragen automatisch doorsturen naar je eigen systeem, met HMAC-ondertekening.
Doel
Wanneer een aanvraag die via de Leads-API is binnengekomen van status verandert, kan Plenno dat automatisch doorsturen naar een eigen systeem via een webhook-URL.
Vooraf nodig
- Je bent eigenaar, of teamlid met het lidtype Ontwikkelaar, en Ontwikkelaarsfuncties staat aan (zie De Leads-API in het kort).
- Een HTTPS-eindpunt op je eigen server dat
POST-verzoeken kan ontvangen.
Nog geen beheerscherm. Een webhook-eindpunt instellen kan op dit moment nog niet via een schermpje in Plenno — dat komt nog. Tot die tijd richt je een eindpunt in door, terwijl je in Plenno bent ingelogd, zelf een verzoek te doen naar de eindpunten-API hieronder (bijvoorbeeld via de ontwikkelaarsconsole van je browser, of een klein script dat met je sessie meestuurt). Dit is dezelfde beveiligde, sessiegebonden API die het toekomstige beheerscherm ook zal gebruiken.
Een eindpunt registreren
GET /api/organization-settings/webhooks
POST /api/organization-settings/webhooks
Deze twee routes zijn niet de publieke Leads-API: ze werken met je eigen, ingelogde
Plenno-sessie (dezelfde cookie als de rest van de app), niet met een pln_live_/pln_test_
API-sleutel, en zijn alleen bereikbaar vanaf app.plenno.nl zelf.
POST body:
{
"url": "https://jouw-systeem.example.com/plenno/webhook",
"subscribed_events": ["lead_status_changed", "lead_won", "lead_lost"]
}
urlmoet HTTPS zijn, zonder gebruikersnaam/wachtwoord in de URL, maximaal 2048 tekens.subscribed_eventsis een niet-lege lijst uit de events hieronder.- Het antwoord (HTTP 201) bevat het
secretvoor de HMAC-ondertekening — exact één keer. Plenno bewaart het daarna alleen versleuteld en kan het niet opnieuw tonen; raak je het kwijt, registreer dan een nieuw eindpunt. - Maximaal 10 actieve eindpunten per organisatie.
Een eindpunt weer uitschakelen kan met:
DELETE /api/organization-settings/webhooks/{id}
Dit is onomkeerbaar via deze route (geen "heractiveren") — een nieuw eindpunt aanmaken is de weg terug.
Beschikbare events
| Event | Wanneer |
|---|---|
lead_status_changed |
De pijplijnfase van de aanvraag is gewijzigd. |
lead_won |
De aanvraag is als gewonnen verkoopkans afgesloten. |
lead_lost |
De aanvraag is als verloren verkoopkans afgesloten. |
Alleen gebeurtenissen van aanvragen die via de publieke Leads-API zijn binnengekomen, worden op dit moment doorgestuurd. Testaanvragen (zie Live- en testsleutels) veroorzaken geen webhookgebeurtenis.
De payload
{
"event_id": "3fbe9e2a-0e1e-4b6a-9b7e-2b7b5a5b9b10",
"event_type": "lead_status_changed",
"occurred_at": "2026-09-23T09:14:02.000Z",
"lead_id": "7543de5d-bc88-4ea8-afe3-2696856157e5",
"external_id": "website-form-123456",
"status": "gesprek gepland",
"attribution": {
"utm_source": "google",
"utm_medium": "cpc",
"gclid": "Cj0KCQjw..."
}
}
external_idis de referentie die je zelf meegaf bij het aanmaken (nullals je die niet gebruikte).attributionbevat alleen de zes toegestane sleutels (utm_source,utm_medium,utm_campaign,utm_content,utm_term,gclid) die je destijds inmetadatameestuurde — nooit de rest van je vrijemetadata. Ontbrekende sleutels worden weggelaten.statusis"won"/"lost"bij die events, en anders de naam van de pijplijnfase.
Headers en HMAC-ondertekening
Elk verzoek is POST, Content-Type: application/json, met deze headers:
| Header | Betekenis |
|---|---|
x-plenno-webhook-signature |
HMAC-SHA256 over "${timestamp}.${rawBody}", als hex-string. |
x-plenno-webhook-timestamp |
Tijdstip van verzending, in milliseconden sinds epoch, als string. |
x-plenno-webhook-event-id |
Hetzelfde event_id als in de payload — handig om dubbele ontvangst te herkennen. |
De handtekening is HMAC-SHA256(secret, "${timestamp}.${rawBody}"), waarbij rawBody exact
de bytes zijn die zijn verstuurd — reken de handtekening dus na over de ruwe, ontvangen body, niet
over een opnieuw geserialiseerd object (sleutelvolgorde en spaties kunnen dan verschillen).
Voorbeeld 8 — een webhook ontvangen en verifiëren
// Node.js, Express — pas aan naar je eigen framework.
import crypto from "node:crypto";
const WEBHOOK_SECRET = process.env.PLENNO_WEBHOOK_SECRET;
app.post("/plenno/webhook", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.header("x-plenno-webhook-timestamp");
const signature = req.header("x-plenno-webhook-signature");
const rawBody = req.body; // Buffer — nog niet geparsed als JSON.
if (!timestamp || !signature) return res.status(400).end();
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody.toString("utf8")}`, "utf8")
.digest("hex");
const providedBuffer = Buffer.from(signature, "utf8");
const expectedBuffer = Buffer.from(expected, "utf8");
const isValid =
providedBuffer.length === expectedBuffer.length &&
crypto.timingSafeEqual(providedBuffer, expectedBuffer);
// Optioneel, aanbevolen: wijs een te oud tijdstip af (bv. > 5 minuten), als
// bescherming tegen een replay van een oud, ooit geldig verzoek.
const ageMs = Date.now() - Number(timestamp);
if (!isValid || Number.isNaN(ageMs) || Math.abs(ageMs) > 5 * 60_000) {
return res.status(401).end();
}
const payload = JSON.parse(rawBody.toString("utf8"));
// ... verwerk payload.event_type, payload.lead_id, enzovoort.
res.status(200).end();
});
Belangrijk: lees de body als ruwe bytes (express.raw, niet express.json) vóórdat je hem
parseert — de handtekening rekent over exact die bytes.
Retrygedrag en foutafhandeling
- Plenno wacht maximaal 8 seconden op een antwoord.
- Alleen een HTTP-statuscode 200–299 telt als geslaagd.
- Bij een mislukte poging volgt een nieuwe poging met kwadratisch oplopende wachttijd (5 minuten × pogingnummer², met een plafond van 60 minuten), tot maximaal 8 pogingen; daarna telt de aflevering als definitief mislukt.
- Een eindpunt dat 20 pogingen achter elkaar mislukt, wordt automatisch gepauzeerd. Een nieuw eindpunt aanmaken is dan de weg terug.
- Plenno volgt nooit een HTTP-redirect (3xx) op een webhookaanroep; dat telt als mislukt.
Beveiligingseisen
- Het eindpunt moet HTTPS zijn.
- Verzoeken naar interne, privé- of link-local-adressen (zoals
localhost,127.0.0.1,192.168.x.x, of een cloud-metadata-adres) worden geweigerd, zowel bij het registreren als bij elke aflevering. - Verifieer altijd
x-plenno-webhook-signaturevóórdat je de payload verwerkt. Verwerk nooit een verzoek met een ontbrekende of ongeldige handtekening.
Beperkingen
- Er is nog geen beheerscherm voor webhook-eindpunten — zie hierboven.
- Testwebhooks worden nog niet ondersteund. Een testaanvraag (testsleutel) verandert nooit van status via een echte pijplijn-actie op een manier die een webhook triggert op dit moment.
- Alleen de drie genoemde events zijn beschikbaar; er zijn geen andere events of configuratieopties.
Vervolg
Lees De Leads-API in het kort voor het overzicht, of Live- en testsleutels om een koppeling eerst veilig uit te proberen.
Gerelateerde artikelen
Laatst bijgewerkt: 2026-09-23 · Gebaseerd op Plenno v6.57.0