Webhooks

Un webhook est une adresse HTTPS à vous que Hela appelle quand un événement se produit dans l'entreprise. C'est la façon de réagir en temps réel sans interroger l'API en boucle.

Créer un webhook

Dans Administration → Développeurs → Nouveau webhook, ou par l'API :

POST /companies/{companyId}/webhooks
{
  "url": "https://example.com/hooks/hela",
  "events": ["invoice.paid", "payment.received", "stock.low"],
  "description": "ERP interne"
}
→ { "id": "…", "secret": "whsec_…", … }

Le secret n'est renvoyé qu'à la création et à la rotation. Conservez-le : il sert à vérifier que les envois viennent bien de Hela.

Les événements s'écrivent aggregat.action. Trois formes sont acceptées :

  • un type exact : invoice.paid ;
  • une famille : invoice.* ;
  • tout : *.

Le catalogue des événements liste tout ce qui existe.

Ce que vous recevez

Une requête POST avec un corps JSON :

{
  "id": "evt_48213",
  "type": "invoice.paid",
  "createdAt": "2026-09-28T14:03:12.000Z",
  "companyId": "4c1d…",
  "object": "sale",
  "objectId": "9f3a…",
  "data": {
    "id": "9f3a…",
    "number": "ERNS-SALE-260928-0012",
    "total": { "amount": 12500, "currency": "USD" },
    "paid": { "amount": 12500, "currency": "USD" },
    "partner": { "id": "…", "name": "Marie Kabila" },
    "paidAt": "2026-09-28T14:03:11.000Z"
  }
}

data est une vue stable du document, pas la ligne de base de données : un champ peut s'ajouter, jamais disparaître. Si vous avez besoin du document complet, appelez l'API avec objectId.

Et les en-têtes :

En-tête Contenu
Content-Type application/json
Hela-Event-Type Le type (invoice.paid)
Hela-Event-Id L'identifiant numérique de l'événement, le même à chaque tentative
Hela-Delivery-Attempt Le numéro de la tentative, 1 pour la première
Hela-Signature t=<secondes unix>,v1=<hmac hex>
User-Agent Hela-Webhooks/1.0

Vérifier la signature

La signature est un HMAC-SHA256, avec le secret du webhook, de la chaîne "<t>.<corps brut>". Vérifiez-la sur le corps brut tel que reçu, avant tout parsing ; refusez si l'horodatage a plus de cinq minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, "hex"));
}

Après une rotation du secret, les envois suivants sont signés avec le nouveau : mettez votre serveur à jour avant de faire tourner, ou acceptez les deux secrets le temps de la bascule. Le SDK fournit verifyWebhook().

Répondre

Répondez 2xx en moins de 15 secondes. Tout le reste — un 4xx, un 5xx, un délai, une erreur de connexion — est un échec et sera réessayé. Faites le travail après avoir répondu, pas avant : mettez l'événement dans votre propre file et répondez 200 tout de suite.

Réessais

Après un échec, Hela réessaie à 1 minute, 5 minutes, 30 minutes, 2 heures, 12 heures, puis 24 heures. Au septième échec consécutif l'envoi est marqué exhausted et le webhook est désactivé avec le motif ; le propriétaire de l'entreprise reçoit une notification integration.failed. Réactiver se fait d'un bouton, et les envois manqués se rejouent un par un depuis la page.

Chaque tentative porte le même id d'événement et un Hela-Delivery-Attempt qui s'incrémente. Un consommateur idempotent se contente de mémoriser les id déjà traités.

Ordre et doublons

Les événements d'un même document sont émis dans l'ordre, mais les envois peuvent arriver dans le désordre quand l'un d'eux est réessayé. Lisez createdAt, ou rappelez l'API pour l'état courant, plutôt que de supposer que le dernier reçu est le dernier arrivé.

Un événement peut exceptionnellement être livré deux fois (une réponse 200 qui n'est jamais revenue jusqu'à nous). Dédoublonnez sur id.

Sécurité

  • HTTPS obligatoire. Les adresses http://, les adresses IP privées, localhost et les noms qui résolvent vers un réseau privé sont refusés à la création (webhook-url:private-address) et re-vérifiés à chaque envoi.
  • Les redirections ne sont pas suivies : une 301 est un échec.
  • Le secret n'est jamais renvoyé par GET. Perdu, il se fait tourner (POST /webhooks/{id}/rotate).

Tester en local

Hela ne peut pas atteindre votre machine. Exposez-la avec un tunnel (cloudflared tunnel --url http://localhost:3000, ngrok http 3000) et utilisez l'adresse publique obtenue. Le bouton Tester envoie un événement ping immédiat :

{ "id": "evt_test", "type": "ping", "companyId": "…", "data": { "at": "2026-09-28T14:00:00.000Z" } }

Voir ce qui s'est passé

La page Développeurs liste, par webhook, les cinquante derniers envois : date, événement, statut HTTP, durée, tentative, erreur. Chaque ligne a un bouton Renvoyer. Les envois sont conservés trente jours ; les événements eux-mêmes, quatre-vingt-dix.

Un passage est faux ou manquant ? Écrivez-nous.