Webhooks : signaler des événements à vos propres systèmes

Après cet article, vous aurez configuré un webhook qui appelle votre propre adresse pour les événements que vous avez choisis — avec clé de signature, livraison de test, journal et nouvelles tentatives automatiques en cas d’erreur.

Un webhook inverse le sens : ce n’est pas votre système qui interroge Univents, mais Univents qui contacte votre système dès que quelque chose se produit — une demande arrive, un devis est accepté ou une facture est émise. Vos propres programmes, scripts ou services d’automatisation se branchent ainsi sur votre workspace, sans que personne ait à interroger régulièrement.

La configuration se fait dans le workspace lui-même, sous Intégrations → Développeurs → Webhooks. La configuration, les clés et le journal ne sont visibles que pour le rôle Propriétaire — c’est là que figurent les adresses qu’Univents appelle.

Quand utiliser un webhook

Utilisez un webhook lorsque votre propre système doit réagir immédiatement : répercuter une demande dans votre CRM, déclencher l’expédition vers un système logistique, reporter une facture dans votre comptabilité, mettre à jour un écran d’affichage. Pour tout ce qui doit rester dans Univents, les Automatisations sont en revanche le bon endroit.

Ajouter un endpoint

  1. Dans le menu principal à gauche, cliquez sur Intégrations, puis à gauche sur la catégorie Développeurs, et ouvrez la carte Webhooks.
  2. Cliquez en haut à droite sur Ajouter un webhook.
  3. Sous Nom, saisissez un intitulé que vous reconnaîtrez plus tard, par exemple CRM-Sync.
  4. Sous Adresse, saisissez l’adresse https:// complète de votre destinataire. Uniquement https — les adresses non chiffrées sont refusées. Univents refuse également les adresses de votre réseau local, de localhost ou d’un réseau interne.
  5. Cochez les Événements que ce destinataire doit recevoir.
  6. Enregistrez. Univents vous affiche alors la clé de signature une seule fois.

La clé de signature

La clé commence par whsec_. Elle n’est affichée qu’une seule fois — lors de la création et lors de la régénération. Ensuite, vous n’en voyez plus que le début, afin que le workspace indique quelle clé est enregistrée sans que la valeur elle-même soit exposée. Copiez-la donc immédiatement dans votre système (variable d’environnement, secret store).

Avec Régénérer la clé de signature, vous obtenez une nouvelle clé à tout moment. L’ancienne devient alors immédiatement invalide — remplacez-la de votre côté au même moment, sinon votre destinataire rejettera les messages entrants.

Vérifier la signature

Univents envoie trois en-têtes avec chaque livraison :

En-têteContenu
X-Univents-Signaturet=<Zeitstempel>,v1=<Signatur>
X-Univents-Eventla clé du déclencheur, par exemple inquiry_created
X-Univents-Delivery-Idl’identifiant de cette livraison

La signature est un HMAC-SHA256 calculé sur le texte ${t}.${Body} — c’est-à-dire l’horodatage, un point, puis le corps brut, octet pour octet tel qu’il arrive. Important : vérifiez le corps brut, et non un corps resérialisé. Un JSON.parse suivi d’un JSON.stringify modifie l’ordre des champs et les échappements, et la vérification échoue alors, alors que le message est parfaitement valide.

Voici à quoi ressemble la vérification en 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);
});

Répondez avec 200 (ou toute autre réponse de la famille 2xx) dès que vous avez accepté le message. Toute autre réponse, ou une réponse qui met plus de dix secondes, est considérée comme un échec.

Livraisons : que se passe-t-il en cas d’erreur

La livraison a lieu au moins une fois — pas exactement une fois. Une coupure de connexion en cours de route peut faire qu’un même événement vous parvienne une seconde fois. Concevez donc votre destinataire de façon qu’un double appel ne casse rien : comparez le X-Univents-Delivery-Id aux derniers identifiants traités avant de créer quoi que ce soit.

Si votre destinataire ne répond pas ou répond par une erreur, Univents réessaie. Huit tentatives au total sont prévues, à intervalles croissants :

TentativeDélai depuis la précédente
1immédiate
21 minute
35 minutes
430 minutes
52 heures
66 heures
712 heures
824 heures

Ensuite, le message est considéré comme définitivement échoué et n’est plus renvoyé. Si votre système est en maintenance planifiée, vous disposez ainsi de près de deux jours pour redevenir joignable.

Livraison de test

Avec Test, vous envoyez un message à un endpoint sans attendre un véritable événement. Après la configuration, vous vérifiez ainsi que l’adresse, la vérification de la signature et le chemin de réponse sont corrects. La livraison de test apparaît ensuite dans le journal, afin que vous ne la confondiez pas avec un véritable message.

Un endpoint en pause ne reçoit pas de livraison de test — activez-le d’abord. Et comme la livraison de test est le seul moyen de faire appeler par notre serveur une adresse de votre choix, une limite de dix livraisons de test par workspace et par heure s’applique. Au-delà, l’interface vous indique de réessayer plus tard.

Journal et nouvelle livraison

Sous chaque endpoint figurent les 50 dernières livraisons. Pour chaque ligne, vous voyez :

  • l’état — en attente, en cours, livrée ou définitivement échouée,
  • l’événement,
  • le nombre de tentatives (par exemple 3/8),
  • le statut de réponse de votre destinataire.

Avec Afficher le contenu, vous voyez le message envoyé ; avec Détails, la dernière erreur et l’heure de la prochaine tentative. Univents conserve la réponse de votre destinataire sous forme abrégée, comme matériel de diagnostic.

Renvoyer renvoie un message livré ou définitivement échoué — utile lorsque votre système n’a été indisponible que brièvement. Tant qu’une livraison est en cours ou encore en attente, le bouton est verrouillé afin que deux appels ne se produisent pas en parallèle.

Mettre en pause et supprimer

L’interrupteur de l’endpoint le met en pause : Univents ne livre alors plus rien immédiatement, mais la configuration est conservée — idéal pour un système en cours de refonte. Univents relit l’état à chaque livraison ; une mise en pause prend donc effet tout de suite, et pas seulement au prochain événement.

Supprimer retire l’endpoint avec son journal. Univents demande confirmation au préalable.

Événements disponibles

Vous avez le choix entre les mêmes événements que ceux des Automatisations. La liste de la boîte de dialogue s’étoffe avec le temps : dès qu’Univents signale un événement à un nouvel endroit, il devient aussi disponible pour les webhooks. Un événement non coché n’est pas livré.

Contenu du message

Chaque message a la même enveloppe :

{
  "id": "48213",
  "type": "inquiry_created",
  "created_at": "2026-09-28T10:12:33.000Z",
  "data": { "…": "…" }
}
  • id — l’identifiant de cette livraison, identique à l’en-tête X-Univents-Delivery-Id. Utilisez-le comme clé d’idempotence.
  • type — la clé du déclencheur (inquiry_created, event_confirmed, …).
  • created_at — le moment où l’enveloppe a été construite, et non celui où l’événement s’est produit.
  • data — le contenu métier, défini pour chaque événement selon une liste fixe.

data contient uniquement ce qui figure dans cette liste. Les champs internes et tout ce qui pourrait contenir des identifiants d’accès ou du texte libre sont exclus — notamment legacy_*, stripe_* (identifiants de paiement), *token*, *secret*, *_hash*, pdf_url, les champs d’e-mail et de téléphone, les notes/descriptions ainsi que les indicateurs internes (is_*, created_by, deleted_at). Un champ sans valeur est omis, et non envoyé comme null. Les nouvelles colonnes de nos tables n’apparaissent pas ici d’elles-mêmes ; nous les ajoutons volontairement, une par une.

Chaque version de data contient entity_type et entity_id — ce qui vous permet d’associer le message même lorsque l’événement n’a pas d’enregistrement propre.

Demandes

ÉvénementChamps dans 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_acceptedles champs de inquiry_created et ceux d’un devis (voir Documents financiers)

Événements

ÉvénementChamps dans 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_minuscomme ci-dessus, avec en plus days_before, weekday
event_t_pluscomme ci-dessus, avec en plus days_after

Documents financiers

Valable pour invoice_overdue, invoice_paid, quote_sent, quote_expired et quote_rejected ; pour quote_rejected, avec en 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

Autres événements

ÉvénementChamps dans 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 — aucun contenu d’e-mail, aucun objet
schedule_monthlyday_of_month, target_date, period_start, period_end, period_label, account_id

La livraison de test utilise la même enveloppe avec type: "webhook_test" et data: { test, endpoint_id, sent_at }.

Un événement pour lequel aucune liste n’est indiquée ici arrive avec un data vide : mieux vaut une enveloppe vide bien visible qu’un enregistrement brut.

Limites

  • Uniquement https, sans redirections : si votre serveur répond par une redirection, cela compte comme un échec. Saisissez l’adresse définitive.
  • Dix secondes par tentative. Tout traitement plus long doit venir après la réponse : acceptez le message, répondez par 200 et traitez-le ensuite.
  • Le Contenu du message décrit l’enregistrement qui a déclenché l’événement — traitez-le comme les données clients de votre workspace et ne le journalisez pas plus loin sans contrôle.

Newsletter Univents

Actualités produit et conseils pratiques pour les professionnels de l'événementiel et de la restauration. Environ une fois par mois, pas plus.

La newsletter est envoyée en anglais.