Een webhook stuurt de activiteit van je team naar een URL die van jou is, op het moment dat ze gebeurt. Elke actie die in je Auditlogboek terechtkomt, kan worden doorgestuurd als een ondertekende JSON-POST naar je eigen server, je eigen bot of een intern hulpmiddel - zonder polling, export of het scrapen van het paneel.
Eén regel bepaalt alles op deze pagina. Een webhook bevat precies wat het activiteitenlogboek van je team toont, en niets meer. Geen parallelle feed met een eigen bereik, geen rijkere payload voor machines. Als het niet op die pagina staat, staat het niet in de body.

📍 Waar vind je het
Teaminstellingen → Webhooks, op de eigen pagina van je team. Maximaal drie eindpunten per team, elk met een eigen URL, eigen categorieën en een eigen ondertekeningsgeheim.
Wie het mag instellen: de eigenaar van het team, of een lid wiens rol Een server uit het team verwijderen omvat. Dat is dezelfde regel die het activiteitenlogboek zelf opent: een webhook stuurt door wat die pagina toont, dus het recht om het te lezen is het recht om het door te sturen. Zie Teamrollen en rechten.
🎯 Wat wordt verstuurd, en wat nooit
Je kiest de categorieën. Alleen de categorieën die je aanvinkt, worden bezorgd:
| Categorie | Wat het omvat |
|---|---|
| Functies | Een functie ingeschakeld, uitgeschakeld of de instellingen ervan opgeslagen |
| Servers | Een server toegevoegd aan of verwijderd uit het team |
| Team | Team hernoemd, rollen bewerkt, webhooks gewijzigd |
| Leden | Leden uitgenodigd, toegevoegd, verwijderd of vertrokken |
| Moderatie | Sancties, bezwaren, meldingen, tickets, gewiste geschiedenissen |
| Abonnement | Abonnementswijzigingen van het team |
| Facturatie | Facturatiegebeurtenissen van het team |
| Platformacties op dit team | Wat een platformbeheerder met een van je servers heeft gedaan |
🚫 Drie dingen reizen nooit mee
- IP-adressen. In het activiteitenlogboek wordt een IP getoond aan de persoon van wie het is en aan niemand anders. Een webhook heeft geen lezer om tegen te controleren, dus het veld wordt niet verstuurd: het wordt uit de payload verwijderd, niet leeggemaakt.
- Backofficeacties die niet over jou gaan. Platformbrede administratie heeft geen team, dus niets in de verspreiding kan erbij. De categorie Platformacties op dit team is alleen wat een beheerder met jouw servers heeft gedaan.
- Inloggegevens. Elk veld dat op een token, sleutel of geheim lijkt, komt aan als
••••••••, aan beide kanten van een voor-en-na-paar. Dit gebeurt opnieuw bij het uitgaan, onafhankelijk van het maskeren dat de pagina doet.
Aanmeldingen worden niet aangeboden. Het auditspoor heeft een categorie auth, en die ontbreekt bewust in de lijst hierboven: aanmeldgebeurtenissen onbeheerd en voor onbepaalde tijd naar een server van een derde sturen is een andere daad dan ze tonen op een pagina die iemand moest openen. Het kan later worden toegevoegd.
📦 De bezorging
Elke bezorging is één POST met een JSON-body:
{
"version": 1,
"delivery_id": "0a4b2f0e-8c6e-4a5f-9b3d-2f6c1e0d7a11",
"event": "server.feature.enabled",
"category": "feature",
"team": { "id": 12, "name": "Night Shift" },
"data": {
"id": 918273,
"action": "server.feature.enabled",
"category": "feature",
"description": "Enabled Starboard",
"actor_name": "Ava",
"actor_role": "moderator",
"is_admin_action": false,
"team_id": 12,
"team_name": "Night Shift",
"server_id": 4,
"server_name": "Night Shift HQ",
"subject_label": "Starboard",
"changes": { "threshold": { "before": 3, "after": 5 } },
"metadata": null,
"created_at": "2026-09-06T09:41:12+00:00"
}
}
Die vijftien velden vormen samen heel data - dezelfde vijftien die het activiteitenlogboek toont, min het IP-adres. Elk ervan kan null zijn: een actie zonder server heeft "server_id": null, een actie zonder verschil heeft "changes": null. Lees ze defensief in plaats van per gebeurtenis een vorm aan te nemen.
versionis de versie van de envelop. Hij bestaat zodat je ontvanger erop kan vertakken in plaats van te gokken wanneer de vorm groeit.eventis de geauditeerde actie. Er zijn er meer dan honderddertig en de lijst groeit met het platform - match op een prefix of opcategory, nooit op een uitputtende lijst met namen.delivery_idis stabiel bij elke nieuwe poging voor dezelfde gebeurtenis. Daardoor is ontdubbeling mogelijk; zie hieronder.datais het audititem zelf, hetzelfde object dat het activiteitenlogboek toont.
📨 Headers
| Header | Waarde |
|---|---|
User-Agent |
YAWBDB-Webhooks/1.0 |
X-YAWBDB-Event |
De actienaam, hetzelfde als event in de body |
X-YAWBDB-Delivery |
De bezorg-id, hetzelfde als delivery_id |
X-YAWBDB-Timestamp |
Unix-tijd in seconden, op het moment dat het verzoek werd opgebouwd |
X-YAWBDB-Signature |
v1= gevolgd door de hex-digest |
🔐 De handtekening controleren
Iedereen die de URL van je eindpunt te weten komt, kan er van alles naartoe POSTen. De handtekening is hoe je onze bezorgingen van de hunne onderscheidt.
De digest is HMAC-SHA256 over de timestamp, een punt, en dan de ruwe body van het verzoek, met je ondertekeningsgeheim als sleutel:
signed_string = X-YAWBDB-Timestamp + "." + raw_body
signature = "v1=" + hex( hmac_sha256(signed_string, secret) )
Drie details zijn dragend, en als je er een overslaat, ontstaat er een gat:
- Onderteken de ruwe bytes, vóór elke JSON-parsing. Als je de body opnieuw serialiseert, verandert hij - volgorde van sleutels, geëscapete slashes, unicode - en komt de digest niet meer overeen.
- De timestamp zit in de ondertekende string, niet alleen ernaast. Daardoor kun je replays weigeren: weiger een bezorging waarvan de timestamp meer dan een paar minuten oud is (vijf minuten is een verstandig venster) en een aanvaller kan een verzoek dat hij vorige week heeft onderschept niet opnieuw sturen. Omdat de timestamp is ondertekend, kan hij die ook niet aanpassen.
- Vergelijk in constante tijd. Een gewone
==keert terug bij de eerste afwijkende byte, en dat tijdsverschil is genoeg om een handtekening byte voor byte te achterhalen. Gebruikhash_equals,crypto.timingSafeEqual,hmac.compare_digest- hoe je taal het ook noemt.
PHP
$raw = file_get_contents('php://input');
$timestamp = (int) ($_SERVER['HTTP_X_YAWBDB_TIMESTAMP'] ?? 0);
$given = $_SERVER['HTTP_X_YAWBDB_SIGNATURE'] ?? '';
if (abs(time() - $timestamp) > 300) {
http_response_code(400); // too old, or clock is wrong
exit;
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
http_response_code(200); // accepted
Node.js (Express)
import crypto from 'node:crypto';
// The raw body is required - express.json() alone destroys it.
app.post('/yawbdb', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = Number(req.get('X-YAWBDB-Timestamp'));
const given = req.get('X-YAWBDB-Signature') ?? '';
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(400);
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${req.body}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(given);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
res.sendStatus(200);
});
Het voorvoegsel v1= benoemt het schema. Als het ooit verandert, kan een ontvanger die het voorvoegsel controleert een onbekend schema bewust weigeren in plaats van het stilletjes verkeerd te lezen.
🔑 Het geheim
64 hexadecimale tekens, voor je gegenereerd, precies één keer getoond - in het paneel, direct nadat je het eindpunt aanmaakt of op Nieuw geheim klikt. Daarna leest niets het nog terug: niet de pagina, niet de API, niet een export. Raak je het kwijt, dan genereer je een nieuw geheim.
Roteren werkt onmiddellijk voor alles wat vanaf dat moment wordt verstuurd. Bezorgingen die al in de wachtrij staan, gaan nog ondertekend met het oude geheim de deur uit totdat de wachtrij leeg is, dus verwacht een korte overlap en werk eerst je ontvanger bij.
🔁 Bezorging, nieuwe pogingen en ontdubbeling
- Alleen een
2xxtelt als ontvangen. Al het andere -3xx,4xx,5xx- is een mislukking. Antwoord snel en doe het werk daarna. - Redirects worden niet gevolgd. Een
302is een mislukking, geen tussenstop. Geef ons de definitieve URL. - Time-outs zijn kort: drie seconden om te verbinden, vijf seconden om te antwoorden. Dit is een melding, geen remote procedure call - bevestig eerst, verwerk later.
- Vier pogingen, met tussenpozen van 30 seconden, 2 minuten en dan 10 minuten: ongeveer twaalf minuten van begin tot eind. Dat dekt een deploy, geen storing.
- Nieuwe pogingen hergebruiken dezelfde
delivery_id. Een ontvanger die elkePOSTals nieuw behandelt, gaat dubbel tellen op de dag dat onze eerste poging aan jouw kant slaagt maar aan de onze een time-out krijgt. Sla de id op en negeer er een die je al hebt afgehandeld. - Volgorde is niet gegarandeerd. Bezorgingen worden onafhankelijk in de wachtrij gezet; een opnieuw geprobeerde komt aan na gebeurtenissen die later kwamen. Gebruik
data.created_atals de volgorde voor je telt.
⛔ Automatisch uitschakelen
Na 20 opeenvolgende mislukkingen wordt het eindpunt uitgeschakeld en meldt het paneel dat op de kaart. Een eindpunt dat niet meer antwoordt, is meestal voorgoed gestopt, en niemand komt het ons vertellen - eindeloos opnieuw proberen zou per actie een verzoek en een geschiedenisrij kosten, voor onbepaalde tijd.
Om het te herstarten: herstel je ontvanger, Bewerk dan de webhook en vink Gebeurtenissen naar dit adres bezorgen weer aan. Weer aanzetten wist de teller, zodat een hersteld eindpunt niet één mislukking verwijderd is van opnieuw uitvallen.
🧪 Een test versturen
Test versturen stuurt meteen één echt, ondertekend verzoek naar je eindpunt en toont je de statuscode en de rondetijd. Het is geen simulatie: het gaat door dezelfde code als elke echte bezorging, met dezelfde headers, zodat een eindpunt dat de test doorstaat niet daarna productieverkeer kan weigeren vanwege een header die de test nooit verstuurde.
De testbody bevat "event": "panel.webhook.test" en "category": "test", zodat je ontvanger hem kan herkennen.
Verwacht twee aankomsten, niet één. Op de knop drukken is zelf een geauditeerde teamactie. Dus je eindpunt krijgt de testbezorging en - als het de categorie Team volgt - kort daarna een tweede, gewone bezorging voor
team.webhook.tested. Dat klopt, en het is geen lus.
Een test telt nooit mee voor de grens van 20 mislukkingen en wist hem ook nooit. Een eindpunt debuggen kan het dus niet uitschakelen.
📜 Recente pogingen
Recente pogingen toont de laatste 20 pogingen voor dat eindpunt: wanneer, welke gebeurtenis, welk pogingnummer, de statuscode of de fout, en hoe lang het duurde. Mislukkingen bevatten een kort fragment van wat je server antwoordde - meestal de snelste manier om erachter te komen dat een reverse proxy, en niet jouw code, degene is die nee zegt.
Pogingen worden 30 dagen bewaard en 's nachts opgeruimd. Dit is debugafval, geen archief: het auditspoor zelf heeft een eigen, veel langere bewaartermijn.
🛡️ Waar we weigeren te versturen
Een eindpunt-URL moet naar het openbare internet wijzen. Een URL die verwijst naar een loopback-adres, een privébereik, een link-local-adres of een cloud-metadata-adres wordt geweigerd - wanneer je hem opslaat, en opnieuw vóór elke afzonderlijke verzending.
De tweede controle is degene die er op termijn toe doet. Een naam die de dag dat hij werd getypt naar iets openbaars wees, kan een maand later naar 127.0.0.1 wijzen, en een webhook vuurt zolang hij bestaat. Een weigering telt als een mislukking voor het eindpunt en wordt niet opnieuw geprobeerd: dezelfde vraag nog drie keer aan de resolver stellen zou niets veranderen.
Geef de voorkeur aan een https-eindpunt. De handtekening bewijst wie de body stuurde, maar verbergt hem niet - via gewone http reist je activiteit in het klaar.
💬 Rechtstreeks in een Discord- of Slack-kanaal
Plak een Discord-webhook-URL (https://discord.com/api/webhooks/…) als eindpunt en het paneel stuurt een Discord-bericht in plaats van de JSON-envelop: één embed per item, met de beschrijving als titel, met de gebeurtenis, categorie, actor, team, server, onderwerp en - als die er zijn - de wijzigingen als korte diff. De tijdstempel is die van het item en de voettekst bevat de bezorg-id.
De embed wordt opgebouwd uit dezelfde envelop die hierboven is beschreven, dus hij erft zijn twee regels: geen IP-adres, inloggegevens gemaskeerd. Vermeldingen zijn aan de kant van Discord uitgeschakeld, dus een kanaal of rol met de naam @everyone in een item wordt als tekst geplaatst en pingt niemand. De X-YAWBDB-*-headers en de handtekening worden nog steeds verstuurd; Discord negeert ze gewoon.
Discord antwoordt 204 No Content op een bericht dat het heeft geaccepteerd, en dat is wat een groene rij in Recente pogingen ervoor toont.
Hetzelfde geldt voor een Slack incoming webhook-URL (https://hooks.slack.com/services/…): het paneel plaatst één Slack-bericht per item - de beschrijving vetgedrukt, dan één regel per feit, de wijzigingen in een codeblok, en eronder de tijdstempel en bezorg-id. Het is opgebouwd uit dezelfde envelop, dus dezelfde twee regels gelden, en de drie tekens die Slack voor zijn eigen commando's reserveert (<, >, &) worden geëscaped, zodat een door een lid getypte naam nooit @channel of @here kan worden. Slack antwoordt 200 ok op een bericht dat het heeft geaccepteerd. De workflow- en trigger-URL's van Slack zijn geen berichten: ze behouden de JSON-envelop, die een workflow met zijn eigen variabelen kan ontleden.
💡 Wat mensen ermee bouwen
- Plaats de activiteit van je team in een eigen Discord- of Slack-kanaal - een Discord- of Slack-webhook-URL werkt zoals hij is, of zet je eigen ontvanger ertussen om te filteren en te herformatteren.
- Spiegel het spoor naar je eigen logopslag en bewaar het langer dan het bewaarvenster van het paneel.
- Roep iemand op wanneer een moderatieactie buiten kantooruren binnenkomt.
- Start een build, een synchronisatie of een back-up wanneer een instelling van een functie verandert.
🔗 Zie ook
- Auditlogboek - wat het spoor vastlegt, en wat het bewust niet vastlegt
- Teamrollen en rechten - wie de pagina mag openen
- Serverlogs - het logboek aan de Discord-kant, een andere functie die een andere vraag beantwoordt