SchedulinqDocs
Naar Schedulinq

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

  1. Open Integraties → API → Webhooks

    Alleen beheerders zien deze pagina. Klik op Nieuw webhook-endpoint.

  2. 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.
  3. 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.

Het tabblad Webhooks met drie endpoints: hun URL en omschrijving, het aantal gebeurtenissen, de laatste bezorging en de status; het onderste is uitgeschakeld.
De pagina van een endpoint met de URL en omschrijving, de gekozen gebeurtenissen, rechts het ondertekeningsgeheim en het testbericht, en onderaan de bezorgingen.

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.

De knop Testbericht versturen met eronder het resultaat: bezorgd, met HTTP 200 en de duur.

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-idevt_docs_example
webhook-timestamp1790843400
webhook-signaturev1,7GSiRxTS1D5qTXrlLqBicxVBeWUju4nR1/HZ0lp+/wE=

Geheim (voorbeeld, niet echt)

whsec_c2NoZWR1bGlucS1kb2NzLWV4YW1wbGUtc2VjcmV0

Ondertekende 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:

  1. Neem het deel van het geheim na whsec_ en decodeer het uit base64. Dat is de sleutel.
  2. Bereken HMAC-SHA256 met die sleutel over <webhook-id>.<webhook-timestamp>.<body>, en codeer het resultaat in base64.
  3. De header webhook-signature bevat een of meer items, gescheiden door spaties, elk v1,<signature>. Accepteer het bericht als een ervan gelijk is aan de jouwe, vergeleken in constante tijd.
  4. Weiger een webhook-timestamp die 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:

PogingNa
15 seconden
25 minuten
330 minuten
42 uur
55 uur
610 uur
710 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.

Een uitgeschakeld endpoint met bovenaan de melding dat het is uitgeschakeld omdat bezorgingen bleven mislukken, en de knop om het weer in te schakelen.

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.

De kaart Ondertekeningsgeheim na Toon, met het voorbeeldgeheim, een knop om het te kopiëren en de knop Geheim vernieuwen.

Regels voor je endpoint

  • Alleen https, hoogstens 2.048 tekens, zonder gebruikersnaam, wachtwoord of #fragment erin.
  • 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.

De lijst met bezorgingen van een endpoint: per bezorging de gebeurtenis, het tijdstip, de status, de HTTP-code, het aantal pogingen en de volgende poging.
Een geopende bezorging met de status, het verzonden bericht en het begin van het antwoord van je server, en de knop om hem opnieuw te versturen.

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.

Laatst bijgewerkt op 2 oktober 2026