Webhooks: gebeurtenissen melden aan je eigen systemen
Na dit artikel heb je een webhook ingesteld die bij de gebeurtenissen die jij kiest je eigen adres aanroept — met ondertekeningssleutel, testbezorging, logboek en automatische herhaling bij fouten.
Een webhook draait de richting om: je systeem vraagt niet bij Univents na, maar Univents meldt zich bij jouw systeem zodra er iets gebeurt — er komt een aanvraag binnen, een offerte wordt geaccepteerd of er wordt een factuur aangemaakt. Zo koppelen eigen programma’s, scripts of automatiseringsdiensten zich aan je workspace, zonder dat iemand steeds hoeft na te vragen.
Dit stel je in de workspace zelf in, onder Integraties → Ontwikkelaars → Webhooks. Instellingen, sleutel en logboek zijn alleen zichtbaar voor de rol Eigenaar — daar staat immers welke adressen Univents aanroept.
Wanneer is een webhook de moeite waard
Gebruik een webhook als je eigen systeem direct moet reageren: een aanvraag spiegelen naar je eigen CRM, de verzending naar een magazijnsysteem starten, een factuur doorzetten naar je boekhouding, een informatiescherm bijwerken. Voor alles wat binnen Univents moet blijven, zijn de Automatiseringen juist de aangewezen plek.
Eindpunt toevoegen
- Klik in het hoofdmenu links op Integraties, klik dan links op de categorie Ontwikkelaars en open de kaart Webhooks.
- Klik rechtsboven op Webhook toevoegen.
- Vul bij Naam iets in wat je later herkent, bijvoorbeeld
CRM-Sync. - Vul bij Adres het volledige
https://-adres van je ontvanger in. Alleenhttps— onversleutelde adressen worden geweigerd. Adressen in je lokale netwerk, oplocalhostof in een intern netwerk weigert Univents eveneens. - Vink de Gebeurtenissen aan die deze ontvanger moet krijgen.
- Sla op. Univents toont je nu eenmalig de ondertekeningssleutel.
De ondertekeningssleutel
De sleutel begint met whsec_. Hij wordt precies één keer getoond — bij het toevoegen en bij het opnieuw genereren. Daarna zie je alleen nog het begin, zodat in de workspace duidelijk blijft welke sleutel is opgeslagen, zonder dat de waarde zelf rondslingert. Kopieer hem dus meteen naar je systeem (omgevingsvariabele, secret store).
Met Ondertekeningssleutel opnieuw genereren krijg je op elk moment een nieuwe. De oude is daarna direct ongeldig — vervang hem aan jouw kant op hetzelfde moment, anders weigert je ontvanger de binnenkomende meldingen.
De handtekening controleren
Univents stuurt bij elke bezorging drie headers mee:
| Header | Inhoud |
|---|---|
X-Univents-Signature | t=<Zeitstempel>,v1=<Signatur> |
X-Univents-Event | de triggersleutel, bijvoorbeeld inquiry_created |
X-Univents-Delivery-Id | de identificatie van deze bezorging |
De handtekening is een HMAC-SHA256 over de tekst ${t}.${Body} — dus tijdstempel, een punt en de ruwe body, byte voor byte zoals hij binnenkomt. Belangrijk: controleer de ruwe body, niet een opnieuw geserialiseerde. Een JSON.parse gevolgd door JSON.stringify wijzigt de volgorde van de velden en de escapes, en de controle mislukt dan terwijl de melding in orde is.
Zo ziet de controle eruit in Node:
const crypto = require('node:crypto');
const express = require('express');
const app = express();
// Roher Body — NICHT express.json() davor.
app.post('/webhooks/dein-system', express.raw({ type: '*/*' }), (req, res) => {
const header = req.get('X-Univents-Signature') ?? '';
const parts = Object.fromEntries(
header.split(',').map((piece) => piece.trim().split('=')),
);
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${parts.t}.${req.body.toString('utf8')}`)
.digest('hex');
const received = parts.v1 ?? '';
// Länge zuerst prüfen: timingSafeEqual wirft bei ungleicher Länge.
const ok =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return res.status(400).send('signature mismatch');
// Der Zeitstempel liegt unter der Signatur — so kann niemand eine alte
// Meldung erneut einspielen.
const ageSeconds = Math.abs(Date.now() / 1000 - Number(parts.t));
if (ageSeconds > 300) return res.status(400).send('stale');
const event = JSON.parse(req.body.toString('utf8'));
console.log(req.get('X-Univents-Event'), event);
res.sendStatus(200);
});
Antwoord met 200 (of een andere reactie in het 2xx-bereik) zodra je de melding hebt aangenomen. Elke andere reactie, of een reactie die langer dan tien seconden duurt, geldt als mislukte poging.
Bezorgingen: wat er bij fouten gebeurt
Er wordt ten minste één keer bezorgd — niet precies één keer. Een verbroken verbinding onderweg kan ertoe leiden dat dezelfde gebeurtenis een tweede keer bij je aankomt. Bouw je ontvanger daarom zo dat een dubbele aanroep niets stukmaakt: controleer de X-Univents-Delivery-Id tegen de laatst verwerkte identificaties voordat je iets aanmaakt.
Antwoordt je ontvanger niet of met een fout, dan probeert Univents het opnieuw. In totaal zijn er acht pogingen gepland, met steeds grotere tussenpozen:
| Poging | Tijd sinds de vorige |
|---|---|
| 1 | direct |
| 2 | 1 minuut |
| 3 | 5 minuten |
| 4 | 30 minuten |
| 5 | 2 uur |
| 6 | 6 uur |
| 7 | 12 uur |
| 8 | 24 uur |
Daarna geldt de melding als definitief mislukt en wordt hij niet meer geprobeerd. Heeft je systeem gepland onderhoud, dan heb je zo bijna twee dagen om weer bereikbaar te zijn.
Testbezorging
Met Test versturen stuur je een melding naar een eindpunt, zonder op een echte gebeurtenis te wachten. Zo controleer je na het instellen of adres, handtekeningcontrole en antwoordroute kloppen. De testbezorging staat daarna in het logboek, zodat je haar niet verwisselt met een echte melding.
Een gepauzeerd eindpunt krijgt geen testbezorging — activeer het eerst. En omdat de testbezorging de enige manier is waarop je onze server een door jou bepaald adres laat aanroepen, geldt er een limiet van tien testbezorgingen per workspace per uur. Daarna meldt de interface dat je het later nog eens moet proberen.
Logboek en opnieuw bezorgen
Onder elk eindpunt staan de laatste 50 bezorgingen. Per regel zie je:
- de status — in wachtrij, bezig, bezorgd of definitief mislukt,
- de gebeurtenis,
- het aantal pogingen (bijvoorbeeld
3/8), - de antwoordstatus van je ontvanger.
Met Inhoud tonen zie je het meegestuurde bericht, met Details de laatste fout en het tijdstip van de volgende poging. Het antwoord van je ontvanger bewaart Univents ingekort als diagnosemateriaal.
Opnieuw bezorgen verstuurt een bezorgde of definitief mislukte melding nog een keer — handig als je systeem maar even weg was. Zolang een bezorging loopt of nog wacht, is de knop geblokkeerd, zodat er niet twee aanroepen tegelijk ontstaan.
Pauzeren en verwijderen
Met de schakelaar bij het eindpunt pauzeer je het: Univents bezorgt dan direct niets meer, maar de instellingen blijven bewaard — ideaal voor een systeem dat wordt verbouwd. Univents leest de status bij elke bezorging opnieuw uit, dus pauzeren werkt niet pas bij de volgende gebeurtenis.
Verwijderen haalt het eindpunt weg, samen met het bijbehorende logboek. Univents vraagt vooraf om bevestiging.
Beschikbare gebeurtenissen
Je kunt kiezen uit dezelfde gebeurtenissen die ook de automatiseringen kennen. De lijst in het dialoogvenster groeit mee: zodra Univents op een nieuwe plek een gebeurtenis meldt, is die ook beschikbaar voor webhooks. Een gebeurtenis zonder vinkje wordt niet bezorgd.
Wat er in de melding staat
Elk bericht heeft dezelfde envelop:
{
"id": "48213",
"type": "inquiry_created",
"created_at": "2026-09-28T10:12:33.000Z",
"data": { "…": "…" }
}
id— de identificatie van deze bezorging, identiek aan de headerX-Univents-Delivery-Id. Gebruik hem als idempotentiesleutel.type— de triggersleutel (inquiry_created,event_confirmed, …).created_at— wanneer de envelop is samengesteld, niet wanneer de gebeurtenis plaatsvond.data— de inhoudelijke gegevens, per gebeurtenis volgens een vaste lijst.
In data staat alleen wat in die lijst staat. Interne velden en alles wat toegangsgegevens of vrije tekst kan bevatten, vallen weg — onder andere legacy_*, stripe_* (betalingsidentificaties), *token*, *secret*, *_hash*, pdf_url, e-mail- en telefoonvelden, notities/beschrijvingen en interne schakelaars (is_*, created_by, deleted_at). Een veld zonder waarde wordt weggelaten en niet als null verstuurd. Nieuwe kolommen in onze tabellen verschijnen hier niet vanzelf; we nemen ze bewust één voor één op.
Elke data-variant bevat entity_type en entity_id — zo kun je de melding ook toewijzen als de gebeurtenis geen eigen record heeft.
Aanvragen
| Gebeurtenis | Velden in data |
|---|---|
inquiry_created | id, event_id, contact_id, location_id, booking_page_id, booking_request_state, event_name, event_date, event_end_date, guest_count, estimated_value, account_id, created_at, updated_at |
quote_accepted | de velden van inquiry_created en die van een offerte (zie Financiële documenten) |
Evenementen
| Gebeurtenis | Velden in data |
|---|---|
event_confirmed, event_cancelled | id, name, guest_count, budget, start_date, end_date, date_is_confirmed, language, event_state, location_id, room_id, contact_id, account_id, created_at, updated_at |
event_t_minus | zoals hierboven, aangevuld met days_before, weekday |
event_t_plus | zoals hierboven, aangevuld met days_after |
Financiële documenten
Geldt voor invoice_overdue, invoice_paid, quote_sent, quote_expired en quote_rejected; bij quote_rejected komt daar source bij.
id, event_id, contact_id, doc_type, document_number, title, version, quote_state, invoice_state, issue_date, due_date, sent_date, accepted_date, rejected_date, expires_at, net_total, gross_total, tax_total, discount_total, deposit_amount, currency, linked_order_id, prior_version_id, account_id, created_at, updated_at
Overige gebeurtenissen
| Gebeurtenis | Velden in data |
|---|---|
payment_received | id, booking_request_id, event_id, contact_id, booking_order_state, total_net, total_gross, total_tax, discount_total, currency, payment_method, paid_at, account_id, created_at, updated_at |
contract_signed | id, event_id, contact_id, title, contract_doc_type, contract_state, version, sent_at, expires_at, has_signed_pdf, account_id, created_at, updated_at |
email_received | contact_id, received_at, has_attachments, has_contact_match — geen mailinhoud, geen onderwerp |
schedule_monthly | day_of_month, target_date, period_start, period_end, period_label, account_id |
De testbezorging gebruikt dezelfde envelop met type: "webhook_test" en data: { test, endpoint_id, sent_at }.
Een gebeurtenis waarvoor hier geen lijst staat, komt aan met lege data: liever een zichtbare, lege envelop dan een ruw record.
Beperkingen
- Alleen https, geen doorverwijzingen: antwoordt je server met een doorverwijzing, dan geldt dat als mislukte poging. Vul het definitieve adres in.
- Tien seconden per poging. Meer werk dan dat hoort achter het antwoord: neem de melding aan, antwoord met 200 en verwerk haar daarna.
- De inhoud van de melding beschrijft het record dat de gebeurtenis heeft veroorzaakt — behandel die als klantgegevens van je workspace en log ze niet ongecontroleerd verder.