Webhooks: reporting events to your own systems

After reading this article you have a webhook that calls your own address when the events you picked happen — with a signing key, a test delivery, a delivery log and automatic retries when a call fails.

A webhook turns the direction around: instead of your system asking Univents for something, Univents calls your system as soon as something happens — an enquiry arrives, a quote is accepted, an invoice is issued. That way your own programs, scripts or automation services hook into your workspace instead of polling for changes.

You set it up inside the workspace, under Integrations → Developer → Webhooks. Setting it up, the key and the log are visible to owners only — this is where the addresses Univents calls are listed. Your own system does not need an account with us for this.

When a webhook is the right tool

Use a webhook when your own system should react immediately: mirror an enquiry into your CRM, trigger a production system, push an invoice into your accounting, refresh a display board. If the reaction should stay inside Univents, the automations are the right place instead.

Adding an endpoint

  1. Click Integrations in the left main menu, then Developer on the left, and open the Webhooks card.
  2. Click Add webhook in the top right.
  3. Enter something you will recognise under Name, for example CRM sync.
  4. Enter your receiver's full https:// address under Address. Only https — unencrypted addresses are rejected. Univents also rejects addresses on your local network, on localhost or inside a private network.
  5. Tick the events this receiver should get.
  6. Save. Univents now shows you the signing key once.

The signing key

The key starts with whsec_. It is shown exactly once — when you add the endpoint and when you regenerate it. Afterwards you only see the beginning, so that the workspace can tell which key is stored without the value lying around. Copy it into your system straight away (an environment variable or a secret store).

Regenerate signing key gives you a new one at any time. The old one stops working immediately — swap it on your side in the same moment, or your endpoint will reject the messages coming in.

Verifying the signature

Univents sends three headers with every delivery:

HeaderContent
X-Univents-Signaturet=<timestamp>,v1=<signature>
X-Univents-Eventthe trigger key, for example inquiry_created
X-Univents-Delivery-Idthe identifier of this delivery

The signature is an HMAC-SHA256 over the string ${t}.${body} — the timestamp, a dot, and the raw body, byte for byte as it arrives. Important: verify the raw body, not a re-serialised one. A JSON.parse followed by JSON.stringify changes the key order and the escapes, and your check then fails even though the message is perfectly fine.

Here is what the check looks like in Node:

const crypto = require('node:crypto');
const express = require('express');

const app = express();

// Raw body — do NOT put express.json() in front of this.
app.post('/webhooks/your-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 ?? '';

  // Compare lengths first: timingSafeEqual throws on different lengths.
  const ok =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

  if (!ok) return res.status(400).send('signature mismatch');

  // The timestamp sits under the signature — so nobody can replay an old
  // message.
  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);
});

Answer with 200 (or any other 2xx) as soon as you have accepted the message. Any other answer, or an answer that takes longer than ten seconds, counts as a failed attempt.

Deliveries: what happens when a call fails

Delivery is at least once — not exactly once. A connection that drops on the way can mean the same occurrence reaches you a second time. Build your endpoint so that a repeated call breaks nothing: check the X-Univents-Delivery-Id against the identifiers you have already processed before you create anything.

If your endpoint does not answer or answers with an error, Univents tries again. Eight attempts are planned in total, with growing gaps:

AttemptGap since the previous one
1immediately
21 minute
35 minutes
430 minutes
52 hours
66 hours
712 hours
824 hours

After that the message counts as finally failed and is not tried again. If your system has scheduled maintenance, this gives you almost two days to come back.

Test delivery

Send test sends a message to an endpoint without waiting for a real event. That is how you check after setting it up that the address, the signature check and the answer path all work. The test delivery shows up in the log afterwards, so you cannot confuse it with a real message.

A paused endpoint gets no test delivery — activate it first. And because the test delivery is the only way for you to make our server call an address of your choosing, there is a cap of ten test deliveries per workspace per hour. Beyond that the interface tells you to try again later.

The log and delivering again

Under each endpoint you see the last 50 deliveries. Per row you get:

  • the state — waiting, running, delivered or finally failed,
  • the event,
  • the number of attempts (for example 3/8),
  • your endpoint's response status.

Show content reveals the message that was sent, Details the last error and when the next attempt is due. Univents keeps your endpoint's answer in a shortened form as diagnostic material.

Deliver again sends a delivered or finally failed message once more — useful when your system was only briefly away. While a delivery is running or still waiting, the button is locked, so that two calls cannot end up side by side.

Pausing and deleting

The switch on the endpoint pauses it: Univents then stops delivering to it immediately, but the setup stays — ideal for a system being rebuilt. Univents reads the state fresh on every delivery, so pausing takes effect at once rather than at the next event.

Delete removes the endpoint together with its log. Univents asks before it acts.

Which events you can pick

The list holds the same events the automations know. The list in the dialog grows with us: as soon as Univents reports an event somewhere new, it becomes available for webhooks too. An event without a tick is not delivered.

What is in the message

Every message has the same envelope:

{
  "id": "48213",
  "type": "inquiry_created",
  "created_at": "2026-09-28T10:12:33.000Z",
  "data": { "…": "…" }
}
  • id — the identity of this delivery, the same value as the X-Univents-Delivery-Id header. Use it as your idempotency key.
  • type — the trigger key (inquiry_created, event_confirmed, …).
  • created_at — when the envelope was built, not when the event happened.
  • data — the business content, per event following a fixed list.

data carries only what is on that list. Internal columns and anything that could hold credentials or free text are dropped — among them legacy_*, stripe_* (payment identifiers), *token*, *secret*, *_hash*, pdf_url, e-mail and phone fields, notes/descriptions, and internal switches (is_*, created_by, deleted_at). A field without a value is left out, not sent as null. New columns on our tables do not show up here by themselves; we add them deliberately, one by one.

Every data shape carries entity_type and entity_id — so you can match a message even when the event has no record of its own.

Requests

EventFields 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_acceptedthe fields of inquiry_created plus those of a quote (see Finance documents)

Events

EventFields 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_minusas above, plus days_before, weekday
event_t_plusas above, plus days_after

Finance documents

Applies to invoice_overdue, invoice_paid, quote_sent, quote_expired and quote_rejected; for quote_rejected plus source.

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

Further events

EventFields 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 — no mail body, no subject
schedule_monthlyday_of_month, target_date, period_start, period_end, period_label, account_id

The test delivery uses the same envelope with type: "webhook_test" and data: { test, endpoint_id, sent_at }.

An event without a list here arrives with an empty data: a visible, empty envelope beats a raw record.

Limits

  • https only, no redirects: if your server answers with a redirect, that counts as a failed attempt. Enter the final address.
  • Ten seconds per attempt. Anything slower belongs behind the answer: accept the message, answer 200, and process it afterwards.
  • The content of a message describes the record that triggered the event — treat it like the customer data of your workspace and do not log it onwards unchecked.

Univents newsletter

Product news and practical tips for event and catering businesses. Roughly once a month, no more.