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

  1. Klik in het hoofdmenu links op Integraties, klik dan links op de categorie Ontwikkelaars en open de kaart Webhooks.
  2. Klik rechtsboven op Webhook toevoegen.
  3. Vul bij Naam iets in wat je later herkent, bijvoorbeeld CRM-Sync.
  4. Vul bij Adres het volledige https://-adres van je ontvanger in. Alleen https — onversleutelde adressen worden geweigerd. Adressen in je lokale netwerk, op localhost of in een intern netwerk weigert Univents eveneens.
  5. Vink de Gebeurtenissen aan die deze ontvanger moet krijgen.
  6. 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:

HeaderInhoud
X-Univents-Signaturet=<Zeitstempel>,v1=<Signatur>
X-Univents-Eventde triggersleutel, bijvoorbeeld inquiry_created
X-Univents-Delivery-Idde 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:

PogingTijd sinds de vorige
1direct
21 minuut
35 minuten
430 minuten
52 uur
66 uur
712 uur
824 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 header X-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

GebeurtenisVelden in data
inquiry_createdid, 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_acceptedde velden van inquiry_created en die van een offerte (zie Financiële documenten)

Evenementen

GebeurtenisVelden in data
event_confirmed, event_cancelledid, 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_minuszoals hierboven, aangevuld met days_before, weekday
event_t_pluszoals 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

GebeurtenisVelden in data
payment_receivedid, 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_signedid, 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_receivedcontact_id, received_at, has_attachments, has_contact_match — geen mailinhoud, geen onderwerp
schedule_monthlyday_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.

Univents-nieuwsbrief

Productnieuws en praktijktips voor event- en cateringbedrijven. Ongeveer één keer per maand, niet vaker.

De nieuwsbrief wordt in het Engels verstuurd.