Webhooks
Ontvang ondertekende meldingen van gebeurtenissen wanneer documenten zijn geïndexeerd, leveringen mislukken of endpoints van status veranderen. Verificatie, pogingen opnieuw doen en testen.
Webhooks sturen gebeurtenisnotificaties naar jouw systemen als HTTPS POST-verzoeken, zodat je kunt reageren op wijzigingen zonder te pollen. Registreer een endpoint vanuit jouw werkruimte-instellingen: de URL moet HTTPS gebruiken, en elke werkruimte kan tot 10 endpoints registreren. Bij het aanmaken ontvang je één keer een ondertekeningsgeheim (voorafgegaan door whsec_, het Standard Webhooks-formaat); het is daarna nooit meer opvraagbaar, dus kopieer het direct naar jouw geheimenopslag.
Elke levering is een JSON-body met dezelfde envelop:
{
"event": "documents.indexed",
"timestamp": "2026-07-23T09:14:07.000Z",
"tenantId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"sequence": 42,
"data": { "count": 12 }
}Payloads zijn opzettelijk licht: alleen ids, tellingen en tijdstempels. Behandel een webhook als een signaal dat er iets is gewijzigd, en haal vervolgens de huidige staat op via de geauthenticeerde API. Documentnamen en -inhoud verschijnen nooit in een webhook-payload.
sequence is een per-endpoint teller, oplopend voor deze eigen stream van het endpoint: opeenvolgende gebeurtenissen die naar dit endpoint worden geleverd, hebben opeenvolgende nummers, dus een ontbrekend nummer betekent een levering die jouw service nooit heeft ontvangen. Leveringsvolgorde is niet gegarandeerd, dus gebruik de volgorde (niet de aankomstvolgorde) om gebeurtenissen te ordenen. Twee kanttekeningen: replays komen per ontwerp buiten volgorde aan (een inhaalevenement behoudt zijn oorspronkelijke, oudere volgorde), dus dedupliceer op webhook-id en gooi een levering nooit weg alleen omdat de volgorde onder jouw hoogwaterstand ligt; en een gat kan ook betekenen dat de ontbrekende gebeurtenis nog aan het herproberen is of wacht in een replaybare staat, dus behandel gaten als "controleer het leveringslogboek", niet als bewijs van verlies.
Gebeurteniscatalogus
Abonneer een endpoint op elke combinatie van de onderstaande gebeurtenistypen. Typen gemarkeerd als planned kunnen al worden geselecteerd, maar er wordt nog niets voor verzonden en hun payloadvorm is nog niet definitief; deze wordt hier gepubliceerd zodra de gebeurtenis live gaat.
| Gebeurtenis | Status | Payload (data) |
|---|---|---|
documents.indexed | Live | Documenten zijn doorzoekbaar geworden. Geaggregeerd per werkruimte: maximaal één gebeurtenis per aggregatiewindow, met een telling. |
documents.failed | Live | Documenten zijn definitief mislukt in verwerking. Geaggregeerd per werkruimte: maximaal één gebeurtenis per aggregatiewindow, met een telling. |
sync.completed | Planned | Een connector-syncrun is voltooid. Payloadvorm is nog niet definitief. |
sync.failed | Planned | Een connector-syncrun is mislukt. Payloadvorm is nog niet definitief. |
ingestion.completed | Planned | Een ingestierun is voltooid. Payloadvorm is nog niet definitief. |
ingestion.failed | Planned | Een ingestierun is mislukt. Payloadvorm is nog niet definitief. |
audit.recorded | Live | De audit trail van de werkruimte heeft nieuwe entries gekregen. Geaggregeerd per werkruimte over een window: een telling en het window om op te halen, nooit de entries zelf. |
endpoint.disabled | Live | Een webhook-endpoint is uitgeschakeld: automatisch na aanhoudende leveringsfouten of een 410 Gone, of door een beheerder. Verzonden naar de andere endpoints van de werkruimte. |
delivery.failing | Live | Leveringen naar een webhook-endpoint mislukken: één keer per foutenwindow verzonden, 30 minuten na de eerste mislukte poging, en verzonden naar de andere endpoints van de werkruimte. |
endpoint.enabled | Live | Een uitgeschakeld webhook-endpoint is weer ingeschakeld. Sluit het incident dat endpoint.disabled heeft geopend; verzonden naar de andere endpoints van de werkruimte. |
delivery.recovered | Live | Een levering is geslaagd op een endpoint waarvoor delivery.failing was verzonden. Sluit dat incident; verzonden naar de andere endpoints van de werkruimte. |
webhook.ping | Live | Gericht testgebeurtenis verzonden via de verzend-testactie van het dashboard naar precies één endpoint. Nooit abonneerbaar; data is altijd leeg. |
Voorbeeld data objecten voor de live gebeurtenissen:
// documents.indexed
{ "count": 12 }
// documents.failed
{ "count": 2 }
// audit.recorded
{
"count": 34,
"windowStart": "2026-07-23T09:05:00.000Z",
"windowEnd": "2026-07-23T09:10:00.000Z"
}
// endpoint.disabled
{
"endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"reason": "sustained_failure",
"disabledAt": "2026-07-23T09:14:07.000Z"
}
// delivery.failing
{
"endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"failingSince": "2026-07-23T09:14:07.000Z"
}
// endpoint.enabled
{
"endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"enabledAt": "2026-07-24T08:02:51.000Z"
}
// delivery.recovered
{
"endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
"failingSince": "2026-07-23T09:14:07.000Z",
"recoveredAt": "2026-07-23T11:40:12.000Z"
}
// webhook.ping
{}De operationele gebeurtenissen (delivery.failing, delivery.recovered,
endpoint.disabled, endpoint.enabled) rapporteren over de
gezondheid van andere endpoints: het endpoint waar een gebeurtenis over gaat, is altijd uitgesloten van de levering van die gebeurtenis, omdat levering daar gegarandeerd ruis zou zijn.
Elke live gebeurtenis wordt ook beschreven in het OpenAPI-document op
/api/openapi.json, onder webhooks: het volledige JSON Schema van de body en de
drie ondertekeningsheaders, zodat je ontvangertypen kunt genereren in plaats van de bovenstaande voorbeelden te kopiëren.
Het schema wordt ook aan onze kant afgedwongen: een payload die er niet aan voldoet, wordt nooit verzonden.
Een audit trail naar een SIEM voeden
audit.recorded is de pushhelft van een SIEM-feed, en het is opzettelijk niet de entries: een audit entry bevat een persoon, hun adres en wat ze hebben aangeraakt, en een webhook-payload wordt opgeslagen in het leveringslogboek en verzonden naar een URL die jij beheert, die overal kan oplossen. De gebeurtenis vertelt je dat een window is gesloten met entries erin; je haalt de entries op via de geauthenticeerde API met een sleutel die de audit:read scope draagt.
Bij elke gebeurtenis doorzoek je de trail met windowStart als startDate en
windowEnd als endDate, waarbij je pagineert op de cursor totdat deze null terugkomt. De ondergrens van de zoekopdracht is inclusief en die van het window niet, dus een pagina kan de entry op precies dat moment herhalen; dedupliceer op entry id, wat je in elk geval nodig hebt omdat levering at-least-once is.
Het catalogusendpoint somt elke actie op die de trail kan vastleggen, met de categorie waartoe elk behoort en een versie die verandert wanneer de vocabulaire verandert. Lees het één keer, vergelijk de versie bij latere lezingen, en behandel een actie die ontbreekt als onbekend in plaats van ongeldig: de trail is append-only en behoudt de spelling waarmee elke entry is geschreven.
Windows zijn aaneengesloten zolang je geabonneerd blijft: elk begint waar het laatste evenement dat je hebt ontvangen eindigde, dus een vertraagde pass maakt het volgende window breder in plaats van te verliezen wat er tussendoor is gebeurd. Een window zonder entries verzendt niets.
Leveringen verifiëren
Elke levering wordt ondertekend volgens de Standard Webhooks-specificatie. Verifieer altijd de handtekening voordat je een verzoek vertrouwt: jouw endpoint-URL is bereikbaar voor iedereen op het internet.
Elk verzoek bevat drie headers:
| Header | Waarde |
|---|---|
webhook-id | Bericht-id. Identiek bij elke herpoging, herverzending en replay van dezelfde gebeurtenis; dedupliceer hierop. |
webhook-timestamp | Unix-tijdstempel in seconden op het moment van verzending. |
webhook-signature | Een of meer handtekeningen, gescheiden door spaties, elk in de vorm v1,{base64}. |
Om te verifiëren:
- Reconstitueer de ondertekende inhoud als
{webhook-id}.{webhook-timestamp}.{raw body}. Gebruik de onbewerkte request-bodybytes precies zoals ontvangen, vóór elke JSON-parsing. - Bereken HMAC-SHA256 over die string. De sleutel is het base64-gedecodeerde deel van het geheim na het
whsec_voorvoegsel (de Standard Webhooks-sleutelafleiding, die is wat de referentiebibliotheken implementeren). - Base64-encodeer de HMAC en vergelijk deze met elke
v1,handtekening in de header met behulp van een constante-tijdvergelijking. De levering is authentiek als er één overeenkomt. - Weiger leveringen waarvan
webhook-timestampmeer dan 5 minuten van jouw huidige tijd afwijkt, in beide richtingen. Dit beperkt het opnieuw afspelen van vastgelegde verzoeken.
De header kan meer dan één handtekening bevatten: terwijl een geheimrotatie-gracewindow open is, worden leveringen ondertekend met zowel het nieuwe als het vorige geheim (v1,NEW_SIG v1,OLD_SIG), zodat verificatie blijft slagen, ongeacht naar welk geheim jouw service is gerold.
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
/**
* @param {string} secret - The whsec_ signing secret from endpoint creation.
* @param {Record<string, string>} headers - Lowercased request headers.
* @param {string} rawBody - The raw request body, unparsed.
*/
export function verifyWebhook(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatureHeader = headers["webhook-signature"];
if (!id || !timestamp || !signatureHeader) return false;
const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(skew) || skew > TOLERANCE_SECONDS) return false;
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest();
return signatureHeader.split(" ").some((entry) => {
const comma = entry.indexOf(",");
if (comma === -1 || entry.slice(0, comma) !== "v1") return false;
const received = Buffer.from(entry.slice(comma + 1), "base64");
return received.length === expected.length && timingSafeEqual(received, expected);
});
}Omdat het formaat de Standard Webhooks-specificatie volgt, werken open-source verificatiebibliotheken voor andere talen direct; geef ze het geheim en de drie headers door.
Herpogingen en mislukking
Jouw endpoint heeft 10 seconden om te reageren, inclusief het opzetten van de verbinding. Een respons met een 2xx-status telt als geleverd; al het andere, inclusief een time-out of een omleiding (leveringen volgen nooit omleidingen), telt als een mislukking en wordt herpoging. De enige uitzondering is 410 Gone: dit vertelt ons dat de route opzettelijk is verwijderd, dus die levering wordt niet herpoging en het endpoint wordt direct uitgeschakeld, met een endpoint.disabled gebeurtenis (reden gone) naar de andere endpoints van de werkruimte en een e-mail naar elke eigenaar en beheerder. Een 410 als antwoord op Send test of een handmatige herverzending mislukt alleen die levering.
Antwoord met 2xx zodra je de gebeurtenis duurzaam hebt geaccepteerd, en verwerk de zware taken asynchroon.
Een mislukte levering wordt herpoging volgens dit schema:
| Poging | Vertraging na de vorige mislukking |
|---|---|
| 1 | direct |
| 2 | 5 seconden |
| 3 | 5 minuten |
| 4 | 30 minuten |
| 5 | 2 uur |
| 6 | 5 uur |
| 7 | 10 uur |
| 8 | 10 uur |
Dat zijn 8 pogingen over ongeveer 27,5 uur, dus een volledige dag uitval aan jouw kant betekent geen verlies van gebeurtenissen. Elke vertraging heeft tot 20% willekeurige jitter in beide richtingen, wat gesynchroniseerde herpogingspieken tegen een herstellend endpoint voorkomt. Als jouw endpoint een Retry-After header retourneert, wordt de gevraagde vertraging gerespecteerd binnen grenzen: deze wordt begrensd tussen de geplande vertraging en tweemaal de geplande vertraging. Responsen met status 429 en time-outpogingen worden niet eerder dan 60 seconden herpoging, ongeacht hoe vroeg het geplande tijdslot valt.
Automatisch uitschakelen
Een endpoint dat blijft mislukken, wordt uiteindelijk uitgeschakeld in plaats van eindeloos te worden belast:
- Wanneer leveringen al 30 minuten mislukken, wordt één keer een
delivery.failinggebeurtenis verzonden (naar de andere endpoints van de werkruimte) en krijgt elke eigenaar en beheerder van de werkruimte een in-app melding. Dit is de vroege waarschuwing; een korte storing die binnen die 30 minuten wordt opgelost door een succes, veroorzaakt niets. - Naarmate het foutenvenster voortduurt, escaleert de waarschuwing: na 3 dagen en opnieuw na 4,5 dagen krijgt elke eigenaar en beheerder een in-app melding en een e-mail met de tijd waarop het endpoint wordt uitgeschakeld als de leveringen nog steeds mislukken. Elke waarschuwing bereikt elke persoon één keer per foutenvenster.
- Een succesvol afgeleverde gebeurtenis reset het foutenvenster. Een testgebeurtenis doet dit niet: het bewijst dat het endpoint reageert, niet dat het gebeurtenissen verwerkt. Als er al een waarschuwing was verzonden, wordt een
delivery.recoveredgebeurtenis verzonden en krijgen eigenaren en beheerders een in-app melding dat het endpoint is hersteld. - Na 5 dagen van opeenvolgende mislukkingen wordt het endpoint uitgeschakeld: een
endpoint.disabledgebeurtenis wordt verzonden (opnieuw naar andere endpoints), en elke eigenaar en beheerder van de werkruimte krijgt een e-mail.
De uitschakeling vereist een mislukte levering na de laatste waarschuwing. Als er niets naar het endpoint was verzonden tegen de tijd dat de laatste waarschuwing werd genoemd, veroorzaakt de volgende mislukking een nieuwe laatste waarschuwing, en wordt het endpoint pas uitgeschakeld als de leveringen 12 uur later nog steeds mislukken.
Het zelf uitschakelen van een endpoint verzendt endpoint.disabled met reden manual naar de andere endpoints van de werkruimte. Een uitgeschakeld endpoint ontvangt geen verdere leveringen, en de send-test- en herverzendacties zijn geblokkeerd. Zodra jouw ontvanger weer gezond is, schakel je het endpoint opnieuw in vanuit de werkruimte-instellingen (klik op de statusbadge): het opnieuw inschakelen wist de foutgeschiedenis, zodat het endpoint een nieuw 5-dagenvenster begint, en een endpoint.enabled gebeurtenis wordt verzonden naar de andere endpoints van de werkruimte. Gebeurtenissen die optreden terwijl een endpoint is uitgeschakeld, worden in het leveringslogboek geregistreerd met de status dropped in plaats van te worden verzonden. Gebruik na het opnieuw inschakelen Send test om te bevestigen dat het endpoint bereikbaar is, en Replay failed + dropped om alles in te halen wat de uitval heeft gekost in één actie.
Handmatige herverzending en testen
Het leveringslogboek in de werkruimte-instellingen toont elke leveringspoging met de statuscode en respons. Vanaf daar:
- Resend verzendt de oorspronkelijke gebeurtenis van een eerdere levering opnieuw (zelfde
webhook-id, dezelfde JSON-inhoud) als één poging. Het komt niet in het herpogingsschema terecht en telt nooit mee voor automatisch uitschakelen, dus het is veilig te gebruiken tijdens het debuggen van een onbetrouwbare ontvanger. De herverzending draagt de oorspronkelijkewebhook-id, dus een ontvanger die de gebeurtenis al eerder heeft verwerkt en daarna met een fout heeft geantwoord, behandelt het als hetzelfde bericht. - Replay failed + dropped herhaalt in bulk elke definitief mislukte en dropped levering van het endpoint, van oud naar nieuw (leveringen die nog in hun automatische herpogingsschema zitten, zijn uitgesloten, dus een replay kan nooit een herpoging dupliceren die sowieso zou slagen). Herhaalde leveringen gebruiken het volledige herpogingsschema en dragen de oorspronkelijke
webhook-id: voor jouw service is een replay hetzelfde bericht opnieuw, dus idempotentieverwerking opwebhook-idmaakt inhaalacties veilig, ongeacht of het origineel ooit is aangekomen. Een gebeurtenis die al is verzonden door een herverzending of een eerdere replay, wordt overgeslagen, net als een gebeurtenis die nog wordt geleverd. Kies hoe ver terug je wilt gaan (de laatste 24 uur, de laatste 7 dagen, of alles wat bewaard blijft), en het dialoogvenster geeft een voorbeeld van hoeveel gebeurtenissen dat bereik zal verzenden voordat je begint; grote bereiken worden automatisch doorgebladerd, met voortgangsweergave tijdens het proces. Replays komen buiten volgorde aan; orden de ingehaalde gebeurtenissen opsequencevan de payload. - Send test verzendt een ondertekende
webhook.pinggebeurtenis met een leegdataobject naar precies dat endpoint, ongeacht de gebeurtenisabonnementen, en toont de uitkomst (de statuscode, of waarom het mislukte) zodra de poging is voltooid. Gebruik het om bereikbaarheid te bevestigen en om jouw handtekeningverificatie van begin tot eind te testen. Een testgebeurtenis opent, verplaatst of reset het foutenvenster nooit.
Best practices
- Dedupliceer op
webhook-id. Automatische herpogingen, herverzendingen en replays gebruiken dezelfde id, dus het opslaan van verwerkte ids geeft je exactly-once verwerking bovenop at-least-once levering. - Behandel een payload nooit als huidige staat. Payloads zijn licht en kunnen laat aankomen; gebruik de gebeurtenis als trigger en lees de huidige staat uit de API.
- Vertrouw niet op volgorde. Herpogingen en parallelle levering betekenen dat gebeurtenissen buiten volgorde kunnen aankomen.
- Antwoord met 2xx voordat je zwaar werk doet. Zet de gebeurtenis in de wachtrij en verwerk deze asynchroon; een handler die zijn werk inline doet, riskeert de 10-seconden time-out en wordt herpoging, wat één gebeurtenis in meerdere dubbele pogingen verandert.
- Roteer geheimen met een grace window. Rotatie vanuit de werkruimte-instellingen houdt het vorige geheim geldig gedurende een window dat jij kiest, en leveringen worden tijdens dit window met beide geheimen ondertekend, zodat een rolling deploy van jouw services nooit een levering mist. Een endpoint houdt één vorig geheim bij, dus een tweede rotatie met een grace window wordt geweigerd totdat het eerste window is afgelopen; een rotatie zonder grace window wordt altijd geaccepteerd en trekt elk oud geheim direct in, wat de weg is voor een gelekt geheim.