Webhooks
Hoor van elke wijziging — voeg een endpoint toe, controleer de handtekening, antwoord snel, en weet wat er gebeurt als je server plat ligt.
Een webhook vertelt je eigen systeem wat er in Schedulinq verandert, op het moment dat
het gebeurt: een afspraak geboekt, verzet of geannuleerd, een uitnodiging geboekt of
verlopen, een teamlid dat erbij kwam. Schedulinq stuurt een ondertekende POST naar
een adres van jou, een endpoint, met de gewijzigde afspraak, uitnodiging of het
gewijzigde teamlid in dezelfde vorm als de API die uitleest. Welke gebeurtenissen er
zijn, staat in Webhook-gebeurtenissen.
Een endpoint toevoegen
Open Integraties → API → Webhooks
Alleen beheerders zien deze pagina. Klik op Nieuw webhook-endpoint.
Vul het endpoint in
- URL: het adres van je ontvanger. Alleen
https; een adres in een privénetwerk wordt onder het veld geweigerd. - Omschrijving: voor jezelf, bijvoorbeeld "CRM productie".
- Gebeurtenissen: gegroepeerd als Afspraken, Uitnodigingen en Teamleden, elk met een regel over wanneer die wordt verstuurd. Er wordt alleen verstuurd wat je aanvinkt.
- URL: het adres van je ontvanger. Alleen
Sla op en kopieer het ondertekeningsgeheim
De kaart Ondertekeningsgeheim toont het geheim achter Toon. Je ontvanger heeft het nodig om elk bericht te controleren; zie De handtekening controleren.
Een organisatie heeft hoogstens tien endpoints; uitgeschakelde tellen mee. Elke beheerder krijgt een e-mail als een endpoint wordt toegevoegd, verwijderd, uitgeschakeld of naar een andere URL wordt gezet. De mail noemt alleen de host, nooit het pad of de query, waarin een token van jezelf kan staan.


Testen
Testbericht versturen stuurt meteen een ping naar de opgeslagen URL, los van de
aangevinkte gebeurtenissen, en toont het resultaat onder de knop: de HTTP-status en hoe
lang het duurde, of waarom het mislukte. Het werkt ook bij een uitgeschakeld endpoint,
zodat je een oplossing kunt controleren voordat je het weer inschakelt.

Hoe een bezorging eruitziet
Een POST met Content-Type: application/json; charset=utf-8 en drie headers:
webhook-id— het id van de gebeurtenis (evt_en 32 hexadecimale tekens). Dat is hetzelfde bij elke nieuwe poging en elke herhaling van de gebeurtenis, dus het is je sleutel om dubbele berichten te herkennen.webhook-timestamp— wanneer deze poging is verstuurd, in Unix-seconden.webhook-signature— de handtekening; zie hieronder.
De body is een envelop met type, timestamp en data. Zie
Webhook-gebeurtenissen.
De handtekening controleren
Elk bericht is ondertekend volgens Standard Webhooks, dus een bibliotheek voor je programmeertaal kan het werk doen. Controleer je eigen controle eerst met dit ondertekende voorbeeld: die moet het accepteren als je klok op het tijdstempel ervan staat.
| Headers | |
|---|---|
webhook-id | evt_docs_example |
webhook-timestamp | 1790843400 |
webhook-signature | v1,7GSiRxTS1D5qTXrlLqBicxVBeWUju4nR1/HZ0lp+/wE= |
Geheim (voorbeeld, niet echt)
whsec_c2NoZWR1bGlucS1kb2NzLWV4YW1wbGUtc2VjcmV0Ondertekende inhoud
evt_docs_example.1790843400.{
"type": "ping",
"timestamp": "2026-10-01T08:30:00Z",
"data": {
"endpointId": "00000000-0000-0000-0000-000000000001"
}
}Het algoritme, als je het zelf schrijft:
- Neem het deel van het geheim na
whsec_en decodeer het uit base64. Dat is de sleutel. - Bereken HMAC-SHA256 met die sleutel over
<webhook-id>.<webhook-timestamp>.<body>, en codeer het resultaat in base64. - De header
webhook-signaturebevat een of meer items, gescheiden door spaties, elkv1,<signature>. Accepteer het bericht als een ervan gelijk is aan de jouwe, vergeleken in constante tijd. - Weiger een
webhook-timestampdie meer dan vijf minuten van je klok afwijkt, zodat een oud bericht niet opnieuw kan worden afgespeeld.
Twee regels gelden in elke taal:
- Controleer de ruwe body, precies de bytes die binnenkwamen. De JSON inlezen en weer uitschrijven verandert de bytes en breekt de handtekening.
- Antwoord 400 op een bericht dat niet klopt, ook als headers ontbreken of misvormd zijn — laat het nooit uitlopen op een 500.
Node.js
// Node.js (Express) — npm install express standardwebhooks
import express from "express";
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.SCHEDULINQ_WEBHOOK_SECRET); // whsec_…
const app = express();
app.post("/schedulinq/webhooks", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = wh.verify(req.body.toString("utf8"), req.headers);
} catch {
return res.status(400).end();
}
// Deduplicate on req.headers["webhook-id"], queue the work, answer fast.
res.status(204).end();
});
app.listen(3000);
PHP
<?php // composer require standard-webhooks/standard-webhooks
require 'vendor/autoload.php';
$wh = new \StandardWebhooks\Webhook(getenv('SCHEDULINQ_WEBHOOK_SECRET'));
$payload = file_get_contents('php://input');
$headers = array_change_key_case(getallheaders(), CASE_LOWER); // the library reads lower-case keys
try {
$event = $wh->verify($payload, $headers);
} catch (\Exception $e) {
http_response_code(400);
exit;
}
// Deduplicate on $headers['webhook-id'], queue the work, answer fast.
http_response_code(204);
Draai het met display_errors uit, zoals in productie. Bij een misvormde
handtekeningheader geeft de bibliotheek een PHP-waarschuwing voordat ze het bericht
weigert; een waarschuwing die in het antwoord belandt, verstuurt de headers te vroeg, en
de 400 gaat dan als 200 de deur uit.
Python
# Python (Flask) — pip install flask standardwebhooks
import os
from flask import Flask, request, abort
from standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["SCHEDULINQ_WEBHOOK_SECRET"])
app = Flask(__name__)
@app.post("/schedulinq/webhooks")
def schedulinq_webhook():
try:
event = wh.verify(request.get_data(), dict(request.headers))
except Exception: # WebhookVerificationError, or a ValueError on a malformed header
abort(400)
# Deduplicate on request.headers["webhook-id"], queue the work, answer fast.
return "", 204
C#
// C# (ASP.NET Core) — no package: the algorithm itself
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
static bool Verify(string secret, string id, string timestamp, string body, string signatures)
{
// Missing or malformed headers are a refusal, never an exception (a 400, not a 500).
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(signatures))
return false;
if (!long.TryParse(timestamp, NumberStyles.None, CultureInfo.InvariantCulture, out var sent))
return false;
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - sent) > 300) return false;
var key = Convert.FromBase64String(secret.StartsWith("whsec_") ? secret[6..] : secret);
using var hmac = new HMACSHA256(key);
var expected = Encoding.UTF8.GetBytes(Convert.ToBase64String(
hmac.ComputeHash(Encoding.UTF8.GetBytes($"{id}.{timestamp}.{body}"))));
foreach (var entry in signatures.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
var parts = entry.Split(',', 2);
if (parts.Length == 2 && parts[0] == "v1" &&
CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(parts[1]), expected))
return true;
}
return false;
}
app.MapPost("/schedulinq/webhooks", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var body = await reader.ReadToEndAsync();
// ToString() gives "" for an absent header, never null.
var ok = Verify(Environment.GetEnvironmentVariable("SCHEDULINQ_WEBHOOK_SECRET")!,
request.Headers["webhook-id"].ToString(), request.Headers["webhook-timestamp"].ToString(), body,
request.Headers["webhook-signature"].ToString());
return ok ? Results.NoContent() : Results.BadRequest();
});
app.Run();
NumberStyles.None accepteert alleen cijfers, dus een vreemd tijdstempel wordt
geweigerd voordat een berekening kan overlopen.
Snel antwoorden
Elk 2xx-antwoord binnen 10 seconden telt als bezorgd. Al het andere is mislukt: een
andere status, een time-out, of een doorverwijzing, die niet wordt gevolgd. Controleer
dus de handtekening, sla het bericht op of zet het in een wachtrij, antwoord 204, en
doe het echte werk daarna.
Nieuwe pogingen en uitschakelen
Een mislukte bezorging wordt opnieuw geprobeerd na:
| Poging | Na |
|---|---|
| 1 | 5 seconden |
| 2 | 5 minuten |
| 3 | 30 minuten |
| 4 | 2 uur |
| 5 | 5 uur |
| 6 | 10 uur |
| 7 | 10 uur |
Dat zijn acht pogingen in 27 uur en 35 minuten. Na de laatste is de bezorging
Mislukt. Elke poging wordt opnieuw ondertekend, met een eigen webhook-timestamp.
Een endpoint waarbij vijf dagen lang elke bezorging mislukte, wordt uitgeschakeld, en elke beheerder krijgt een e-mail. Uitschakelen laat ook de openstaande bezorgingen mislukken. Weer inschakelen begint opnieuw met versturen, maar stuurt niets na van wat er in de tussentijd gebeurde: verstuur die één voor één opnieuw vanuit de tabel met bezorgingen. Opnieuw versturen kan niet zolang het endpoint uit staat.

Volgorde en dubbele berichten
Een bericht komt minstens één keer aan, en de volgorde is niet gegarandeerd.
- Herken dubbele berichten aan
webhook-id. - Elke payload heeft
data.version, dat per afspraak, uitnodiging of teamlid oploopt. Bewaar de hoogste versie die je verwerkte, en sla een gebeurtenis met een lagere over: die is ouder dan wat je al hebt. Versies kunnen nummers overslaan.
Het geheim vernieuwen
Geheim vernieuwen maakt een nieuw geheim. Het vorige tekent nog 24 uur mee, dus in
die tijd heeft elk bericht twee handtekeningen in webhook-signature, de nieuwe eerst.
Zet het nieuwe geheim binnen die 24 uur in je ontvanger; een controle die elk passend
v1-item accepteert, blijft al die tijd werken.

Regels voor je endpoint
- Alleen
https, hoogstens 2.048 tekens, zonder gebruikersnaam, wachtwoord of#fragmenterin. - Geen adres in een privénetwerk: privé-, loopback-, link-local-, gedeelde (CGNAT-), multicast- en gereserveerde adressen worden geweigerd. Dat wordt gecontroleerd bij het opslaan, en opnieuw bij elke bezorging.
- Doorverwijzingen worden niet gevolgd.
- Een time-out van 10 seconden.
Vertrouw op de handtekening, niet op IP-adressen: Schedulinq publiceert geen vaste verzendadressen. Gebruik voor lokale ontwikkeling een tunneldienst die je computer een openbaar https-adres geeft.
Bezorgingen in het dashboard
De pagina van het endpoint toont de Bezorgingen: de gebeurtenis, het tijdstip, de status, de HTTP-status, het aantal pogingen en de volgende poging. Een rij opent het verzonden bericht en de eerste 1 KB van je antwoord, en Opnieuw versturen zet het opnieuw in de wachtrij.
Bezorgingen blijven 30 dagen bewaard. De berichten bevatten persoonsgegevens, zoals een adres, en alleen beheerders kunnen ze zien.


Veelgestelde vragen
Krijg ik ook gebeurtenissen over wijzigingen van mijn eigen systeem?
Ja. Een uitnodiging die je systeem verstuurde, of een afspraak die vanuit je
planningslink is geboekt, wordt gemeld zoals elke andere wijziging. data.changedBy
zegt wie het deed: API_KEY met de apiKeyId van de sleutel bij de uitnodiging die je
systeem maakte, en USER bij de afspraak, omdat het teamlid dat de planningslink opende
die boekte.
Mijn server lag een dag plat. Wat heb ik gemist?
Niets, als hij binnen ongeveer 27 uur terug is: elke bezorging wordt zo lang opnieuw geprobeerd. Daarna open je de bezorgingen van het endpoint en verstuur je de mislukte opnieuw, of werk je bij via de lijsten van de API.
Kan ik me abonneren op het testbericht?
Nee. ping wordt alleen verstuurd door Testbericht versturen.