T ToutadminDocumentação

Esta página ainda não está traduzida no seu idioma: é apresentada em francês.

Webhooks

Un webhook prévient un autre outil quand quelque chose arrive, sans qu'il ait à demander. Toutadmin en émet 8, signés, réessayés, et jamais vers votre réseau interne par inadvertance. Cette page couvre l'enregistrement, la signature, la file, les réessais, la désactivation automatique, et la charge utile exacte de chaque événement.

Enregistrer une destination#

Intégrations → Webhooks, réservé à l'administration : ouvrir une porte de sortie sur les données de l'entreprise ne se délègue pas. Quatre champs :

ChampCe qu'il fait
LibelléPour vous, dans la liste.
AdresseOù poster. https exigé — voir ci-dessous.
SecretSert à signer. Chiffré en base, il ne ressort jamais : l'écran n'en montre que l'existence.
ÉvénementsCeux que cette destination écoute. Une destination qui écoute tout reçoit tout.

Ce que l'adresse doit être#

L'adresse est validée avant d'être acceptée, et deux refus sont volontaires :

  • http en clair est refusé. La charge utile et sa signature circuleraient lisibles : quiconque écoute le réseau lit les données et apprend à forger une requête signée.
  • Une adresse du réseau interne est refusée. localhost, 127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, 169.254.0.0/16, 0.0.0.0, ::1, et les suffixes .localhost, .internal, .local.
Pourquoi bloquer le réseau interne

Sans ce contrôle, un webhook devient un moyen de faire émettre au serveur des requêtes vers des machines qu'il est seul à joindre — service de métadonnées d'un hébergeur, console d'administration, base interne. C'est la falsification de requête côté serveur. Une case à cocher lève le refus quand votre destination est vraiment sur le réseau interne : le choix est alors explicite et consigné.

Les 8 événements#

CléQuandCharge utile
facture.creee À la création d'une facture, client ou fournisseur. id, reference, libelle, sens, montant_ht, devise, echeance
facture.payee Au passage au statut « Payée », et à ce passage seulement. id, reference, libelle, sens, montant_ttc, devise
absence.approuvee À l'approbation d'une absence. id, type, du, au, jours
membre.arrive Quand l'administration ajoute un membre. id, prenom, nom, email, poste, contrat
membre.parti À la désactivation d'un compte, à la main ou par fin de contrat. id, prenom, nom, email, motif
document.signe Quand le dernier signataire a signé, pas au premier. id, titre, nature, empreinte, signataires
ticket.ouvert À l'ouverture d'un ticket de support. id, reference, sujet, priorite, origine
sauvegarde.echec Quand l'externalisation d'une sauvegarde échoue. destination, fichier, motif

Ce que la charge utile ne contient jamais#

  • Le motif d'une absence : il reste dans l'entreprise.
  • Le mot de passe temporaire d'un membre qui arrive.
  • Le contenu d'un document signé — seulement son empreinte.
  • Tout secret et toute empreinte de mot de passe.

membre.parti porte un motif qui vaut desactivation (geste d'un administrateur) ou fin_de_contrat (balayage automatique). Réactiver un compte n'émet rien : une réactivation n'est pas une arrivée.

La forme de la requête#

POST /votre-adresse HTTP/1.1
content-type: application/json
x-toutadmin-event: facture.payee
x-toutadmin-signature: sha256=4f1c…
user-agent: Salarie-Member-Webhook/1

{
  "event": "facture.payee",
  "at": "2026-03-14T09:21:06+00:00",
  "data": {
    "id": 418,
    "reference": "FA-2026-0117",
    "libelle": "Prestation mars",
    "sens": "client",
    "montant_ttc": 4200,
    "devise": "EUR"
  }
}

L'enveloppe est toujours la même : event, at (UTC), et data propre à l'événement. Écrivez votre lecteur sur l'enveloppe, pas sur la position des champs.

Vérifier la signature#

x-toutadmin-signature vaut sha256= suivi du HMAC-SHA256 du corps brut, clé = votre secret. Signez les octets reçus, pas le résultat d'une relecture JSON : un espace de différence change la signature.

// Node
const crypto = require('crypto');

function verifie(corpsBrut, entete, secret) {
  const attendu = 'sha256=' + crypto.createHmac('sha256', secret).update(corpsBrut).digest('hex');
  const a = Buffer.from(attendu);
  const b = Buffer.from(String(entete || ''));
  // Longueurs différentes : timingSafeEqual lèverait. On refuse d'abord.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// PHP
function verifie(string $corpsBrut, string $entete, string $secret): bool
{
    $attendu = 'sha256=' . hash_hmac('sha256', $corpsBrut, $secret);
    return hash_equals($attendu, $entete);   // jamais ===
}
# Python
import hmac, hashlib

def verifie(corps: bytes, entete: str, secret: str) -> bool:
    attendu = 'sha256=' + hmac.new(secret.encode(), corps, hashlib.sha256).hexdigest()
    return hmac.compare_digest(attendu, entete)
Comparez à temps constant

Une comparaison ordinaire s'arrête au premier caractère qui diffère. Le temps de réponse dit alors combien de caractères étaient justes, et la signature se devine octet par octet. timingSafeEqual, hash_equals, compare_digest : selon le langage, mais jamais ==.

La file et les réessais#

Un événement n'est pas envoyé pendant la requête qui l'a produit : il est mis en file. Payer une facture ne doit pas attendre que votre serveur réponde — ni échouer parce qu'il est en panne.

RègleValeur
Délai d'attente d'une livraison8 secondes
Tentatives avant abandon5
Attente entre deux tentatives2, 4, 8, 16 minutes, plafonnée à 60
Échecs consécutifs avant désactivation de la destination20

Une réponse 2xx vaut réussite. Tout le reste — 4xx, 5xx, expiration, connexion refusée, certificat invalide — est un échec qui sera réessayé, puis abandonné après cinq tentatives. La livraison garde alors le statut « Abandonné » et la dernière erreur, lisible dans l'écran.

Après vingt échecs consécutifs, la destination est désactivée. Sans cela, une adresse morte ferait vivre indéfiniment une file qui grossit. La réactiver se fait d'un clic, une fois votre service réparé.

Quand la file est vidée#

ÉditionDéclencheur
NodeLe balayage interne du serveur, en permanence.
PHPtools/cron.php, au rythme de votre cron.

Sur un hébergement limité à un passage tous les quarts d'heure, les webhooks partent avec ce délai. C'est la seule différence de comportement observable entre les deux éditions.

Essayer sans attendre un vrai événement#

Chaque destination a un bouton d'essai : il envoie une charge utile de test sur le premier événement écouté, avec {"essai": true} dans les données. La livraison passe par la file normale — vous vérifiez donc en une fois l'adresse, le certificat, la signature et votre lecteur.

Écrire un lecteur qui tient#

  1. Répondez 2xx tout de suite, puis travaillez. Un traitement long fait expirer la livraison, qui sera réessayée — et vous traiterez deux fois.
  2. Attendez-vous aux doublons. Un réessai après une réponse perdue renvoie la même charge utile. Déduisez une clé de event + data.id et ignorez ce que vous avez déjà vu.
  3. Vérifiez la signature avant de lire le corps, pas après.
  4. Ignorez les champs inconnus. Une charge utile peut gagner des champs ; elle n'en perd pas.
  5. N'ordonnez rien sur l'arrivée. Deux événements réessayés différemment n'arrivent pas dans l'ordre où ils se sont produits. at dit l'ordre réel.

Quand rien n'arrive#

SymptômeCause la plus fréquente
Aucune livraison, pas même en attenteLa destination n'écoute pas cet événement, ou elle est désactivée.
Tout reste « En attente »Édition PHP : le cron n'est pas installé ou n'est pas exécuté.
« secret illisible »Le secret d'instance a changé : le secret chiffré n'est plus déchiffrable. Ressaisissez-le.
Signature qui ne correspond jamaisVous signez le JSON relu, pas le corps brut reçu.
Abandons en série sur une adresse qui marcheCertificat TLS invalide ou chaîne incomplète côté destination.

L'écran Intégrations montre, pour chaque destination, le dernier statut, la dernière tentative et le nombre d'échecs consécutifs. C'est là qu'il faut regarder d'abord — avant les journaux du serveur.

Documentação do Toutadmin — construída em 2026-09-13. Site autónomo, independente do software.