DocumentationOuvrir Moon AI
Parcourir la documentation

Webhooks de l’organisation

Recevoir les événements de l’organisation dans une requête signée, vérifier la signature, gérer les nouveaux essais et piloter les webhooks par l’API.

Sur cette page

Un webhook envoie une requête HTTPS signée à votre serveur dès qu’un événement se produit dans l’organisation : un membre arrive, part ou est suspendu, un lot de licences est plein, les règles changent, le contrat approche de sa fin. Une organisation a 10 webhooks au plus.

Créer un webhook dans l’administrationLien vers la section Créer un webhook dans l’administration

  1. Ouvrez l’administration › « API et clés », carte « Webhooks », puis cliquez sur « Nouveau webhook ».
  2. Saisissez l’« Adresse » : une adresse HTTPS publique de votre serveur.
  3. Cochez les « Événements » à recevoir (« Tout choisir » les coche tous) et, si vous voulez, une « Description ».
  4. Cliquez sur « Créer le webhook ».
  5. Copiez le « Secret de signature » (il commence par whsec_), puis cliquez sur « Je l’ai copié ». Il ne sera plus affiché.

Le menu de chaque webhook propose « Envoyer un test » (un événement ping, avec la réponse de votre serveur), « Voir les envois » (l’historique des 30 derniers jours, avec « Renvoyer »), « Modifier », « Créer un nouveau secret », « Désactiver » ou « Activer », et « Supprimer ».

ÉvénementsLien vers la section Événements

ÉvénementEnvoyé quand
member.joinedUn membre rejoint l’organisation : invitation acceptée, code d’accès, rattachement, annuaire SCIM.
member.removedUn membre est retiré, part de lui-même ou est retiré par l’annuaire.
member.suspendedUn membre est suspendu, dans l’administration ou par l’annuaire.
member.reactivatedUn membre suspendu est réactivé.
license.pool_fullUn lot de licences n’a plus de place libre.
policy.updatedLes règles de l’organisation, d’un rôle, d’un groupe ou d’un membre changent.
contract.renewal_dueLe contrat de l’organisation approche de sa fin sans renouvellement signé.
pingUn test envoyé depuis l’administration ou par l’API. On ne s’y abonne pas.

Un même événement produit dans les 5 secondes (même type, même membre, mêmes détails) n’est envoyé qu’une fois.

Ce que reçoit votre serveurLien vers la section Ce que reçoit votre serveur

Une requête POST avec ces en-têtes :

En-têteValeur
Content-Typeapplication/json
User-AgentMoonAI-Webhooks/1.0
Moon-EventLe type de l’événement, par exemple member.joined.
Moon-Webhook-IdL’identifiant de l’événement, le même à chaque nouvel essai.
Moon-Delivery-IdL’identifiant de cet envoi.
Moon-TimestampL’heure de l’envoi, en secondes Unix.
Moon-Signaturet=<heure>,v1=<signature>

Et ce corps :

JSON
{
  "id": "5c0e3f1a-6b0f-4f7e-9a51-2a8e3c9d7b10",
  "type": "member.joined",
  "created_at": 1793520000,
  "org_id": "8c1f2a4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b",
  "data": {
    "actor": "3f2b9c1d-7e4a-4b8c-9d0e-1f2a3b4c5d6e",
    "member": {
      "user_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "name": "Camille Martin",
      "email": "camille.martin@ecole.fr"
    }
  }
}
ChampDescription
idIdentifiant de l’événement (égal à Moon-Webhook-Id).
typeLe type de l’événement.
created_atHeure de l’événement, en secondes Unix.
org_idL’organisation.
data.actorQui a fait l’action : l’identifiant d’une personne, api_key:<id> pour une clé d’administration, ou null.

Selon l’événement, data contient aussi :

ÉvénementChamps de data
member.*member (user_id, name, email), et source, status ou reason quand ils sont connus. reason vaut left quand la personne est partie d’elle-même.
policy.updatedscope_type (org, role, group ou member) et scope_id.
license.pool_fullpool_id, plan_id, plan_name, seats, used et fallback_pool_id.
contract.renewal_duecontract_id, days_left et period_end.
pingmessage.

Un corps de plus de 64 Ko est envoyé avec "data": {"truncated": true} : relisez alors l’information par l’API d’administration.

Vérifier la signatureLien vers la section Vérifier la signature

v1 est le HMAC-SHA256 du texte <t>.<corps brut>, avec votre secret de signature comme clé, écrit en hexadécimal minuscule. Calculez-le sur les octets reçus, avant tout décodage du JSON, comparez en temps constant, et refusez une requête dont t s’écarte de plus de 5 minutes de votre horloge.

Python
import hashlib
import hmac
import time

def verify_moon_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    try:
        parts = dict(item.split("=", 1) for item in header.split(","))
        timestamp = int(parts["t"])
        expected = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > tolerance:
        return False
    message = f"{timestamp}.".encode("ascii") + raw_body
    computed = hmac.new(secret.encode("utf-8"), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, expected)
JavaScript
import crypto from 'node:crypto';

export function verifyMoonSignature(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(header.split(',').map((item) => item.split('=', 2)));
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false;
  const computed = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(computed);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Répondre, nouveaux essais et désactivationLien vers la section Répondre, nouveaux essais et désactivation

  • Répondez par un code 2xx en moins de 10 secondes. Faites le travail long après avoir répondu.
  • Sinon, l’envoi est réessayé après 1 minute, 5 minutes, 30 minutes, 2 heures, 6 heures, 12 heures puis 24 heures : 8 essais au plus, puis l’envoi passe en échec.
  • Une réponse 410 arrête l’envoi et désactive le webhook tout de suite (disabled_reason : gone).
  • Après 100 essais en échec d’affilée, le webhook est désactivé (disabled_reason : too_many_failures). Le réactiver remet ce compte à zéro.
  • Un webhook désactivé ne reçoit plus rien, sauf les tests.
  • Un ping n’est jamais réessayé.
  • Le même événement peut arriver plus d’une fois : dédoublonnez avec Moon-Webhook-Id.

L’adresse doit être publique sur Internet. Une adresse privée ou locale est refusée à l’enregistrement, et un envoi vers une adresse devenue privée échoue sans nouvel essai. Les redirections ne sont pas suivies.

Gérer les webhooks par l’APILien vers la section Gérer les webhooks par l’API

Toutes les routes sont sous https://chat.realmoon.ai/api/v1/orgs/{org_id} et demandent une clé d’administration avec la portée webhooks:read (lectures) ou webhooks:write (écritures et lectures). Voir l’authentification.

L’objet webhookLien vers la section L’objet webhook

JSON
{
  "id": "0b7e6d5c-4a3b-4c2d-9e1f-a0b1c2d3e4f5",
  "url": "https://rh.example.fr/moonai/webhook",
  "description": "Logiciel RH",
  "events": ["member.joined", "member.removed"],
  "secret_prefix": "whsec_Ab3dE9",
  "active": true,
  "disabled_reason": null,
  "failure_count": 0,
  "last_success_at": 1793520012,
  "last_failure_at": null,
  "created_at": 1793433600,
  "updated_at": 1793433600
}
ChampTypeDescription
idchaîneIdentifiant du webhook.
urlchaîneAdresse HTTPS de votre serveur.
descriptionchaîne ou nullDescription libre.
eventslisteÉvénements reçus, triés.
secret_prefixchaîneDébut du secret (whsec_ et six caractères), pour le reconnaître.
activebooléenLe webhook reçoit les événements.
disabled_reasonchaîne ou nullmanual, gone ou too_many_failures quand il est désactivé.
failure_countentierEssais en échec d’affilée.
last_success_at, last_failure_atentier ou nullDernier essai réussi, dernier essai en échec.
created_at, updated_atentierCréation, dernière modification.

Lister les webhooksLien vers la section Lister les webhooks

GET /webhooks · webhooks:read

Paramètres de requête : page et limit (pagination). Les webhooks les plus récents d’abord. La réponse ajoute events, la liste des événements auxquels on peut s’abonner.

JSON
{
  "items": [{ "id": "0b7e6d5c-4a3b-4c2d-9e1f-a0b1c2d3e4f5", "url": "https://rh.example.fr/moonai/webhook", "active": true }],
  "total": 1,
  "page": 1,
  "limit": 50,
  "events": ["member.joined", "member.removed", "member.suspended", "member.reactivated", "license.pool_full", "policy.updated", "contract.renewal_due"]
}

Créer un webhookLien vers la section Créer un webhook

POST /webhooks · webhooks:write

ChampTypeObligatoireDescription
urlchaîneouiAdresse https:// publique, 8 à 2 048 caractères.
eventsliste de chaînesoui1 à 20 événements de la liste ci-dessus. Les doublons sont retirés.
descriptionchaînenon200 caractères au plus.

La réponse est l’objet webhook avec, en plus, secret : le secret de signature complet, rendu cette seule fois.

cURL
curl https://chat.realmoon.ai/api/v1/orgs/$ORG_ID/webhooks \
  -H "Authorization: Bearer $MOONAI_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: webhook-rh-0001" \
  -d '{
    "url": "https://rh.example.fr/moonai/webhook",
    "events": ["member.joined", "member.removed"],
    "description": "Logiciel RH"
  }'
Python
import os
import uuid
import requests

r = requests.post(
    f"https://chat.realmoon.ai/api/v1/orgs/{os.environ['ORG_ID']}/webhooks",
    headers={
        "Authorization": f"Bearer {os.environ['MOONAI_ADMIN_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "url": "https://rh.example.fr/moonai/webhook",
        "events": ["member.joined", "member.removed"],
        "description": "Logiciel RH",
    },
    timeout=30,
)
r.raise_for_status()
hook = r.json()
print("Secret à garder :", hook["secret"])
JavaScript
import crypto from 'node:crypto';

const r = await fetch(`https://chat.realmoon.ai/api/v1/orgs/${process.env.ORG_ID}/webhooks`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MOONAI_ADMIN_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    url: 'https://rh.example.fr/moonai/webhook',
    events: ['member.joined', 'member.removed'],
    description: 'Logiciel RH',
  }),
});
if (!r.ok) throw new Error(await r.text());
const hook = await r.json();
console.log('Secret à garder :', hook.secret);
StatutcodeCause
400invalid_requestfields vise url (adresse invalide, pas en https://, pas publique, ou qui ne répond pas pour l’instant) ou events (événement inconnu, liste vide).
409webhook_limitL’organisation a déjà 10 webhooks.

Lire un webhookLien vers la section Lire un webhook

GET /webhooks/{webhook_id} · webhooks:read

Rend l’objet webhook, sans le secret. 404 not_found si l’identifiant est inconnu.

Modifier un webhookLien vers la section Modifier un webhook

PATCH /webhooks/{webhook_id} · webhooks:write

Envoyez seulement les champs à changer, au moins un.

ChampTypeObligatoireDescription
urlchaînenonNouvelle adresse, mêmes règles qu’à la création.
eventsliste de chaînesnonRemplace la liste, 1 à 20 événements.
descriptionchaînenon200 caractères au plus ; une chaîne vide l’efface.
activebooléennonfalse désactive le webhook (disabled_reason : manual) ; true le réactive et remet failure_count à zéro.

Rend l’objet webhook. Un corps vide répond 400 invalid_request (« Rien à modifier. »).

Supprimer un webhookLien vers la section Supprimer un webhook

DELETE /webhooks/{webhook_id} · webhooks:write

Supprime le webhook et l’historique de ses envois. Réponse : {"id": "…", "deleted": true}.

Créer un nouveau secretLien vers la section Créer un nouveau secret

POST /webhooks/{webhook_id}/rotate-secret · webhooks:write

Remplace le secret de signature. L’ancien cesse de fonctionner tout de suite : donnez le nouveau à votre serveur juste après. La réponse est l’objet webhook avec secret, rendu cette seule fois.

Envoyer un testLien vers la section Envoyer un test

POST /webhooks/{webhook_id}/test · webhooks:write

Envoie tout de suite un événement ping à ce webhook, même désactivé, et rend l’envoi (voir ci-dessous) après la réponse de votre serveur. Les tests et les renvois comptent dans la limite de 20 tests en 10 minutes par personne (pour une clé, son créateur) : au-delà, 429 rate_limited.

Lister les envoisLien vers la section Lister les envois

GET /webhooks/{webhook_id}/deliveries · webhooks:read

ParamètreTypeDescription
statuschaîneFiltre : pending, sending, delivered ou failed. Toute autre valeur répond 400.
page, limitentierPagination.

Les envois sont gardés 30 jours. Chaque envoi :

JSON
{
  "id": "e4d3c2b1-a0f9-4e8d-7c6b-5a4f3e2d1c0b",
  "event": "member.joined",
  "event_id": "5c0e3f1a-6b0f-4f7e-9a51-2a8e3c9d7b10",
  "status": "pending",
  "attempts": 2,
  "next_attempt_at": 1793520300,
  "last_status_code": 503,
  "last_error": "http_error",
  "last_response": "Service Unavailable",
  "duration_ms": 182,
  "created_at": 1793520000,
  "delivered_at": null
}
ChampDescription
statuspending (en attente d’un essai), sending (en cours), delivered (remis) ou failed (en échec).
attemptsEssais déjà faits.
next_attempt_atHeure du prochain essai, ou null.
last_status_codeCode HTTP de la dernière réponse, ou null sans réponse.
last_errorhttp_error (réponse non 2xx), timeout (pas de réponse en 10 secondes), blocked_address (adresse non publique), webhook_disabled, ou le type d’erreur réseau.
last_responseLes 500 premiers caractères de la réponse de votre serveur.
duration_msDurée du dernier essai, en millisecondes.

Renvoyer un envoiLien vers la section Renvoyer un envoi

POST /webhooks/{webhook_id}/deliveries/{delivery_id}/retry · webhooks:write

Renvoie tout de suite un envoi remis, en attente ou en échec, et rend l’envoi après l’essai. Ses essais repartent de zéro : s’il échoue, le calendrier des nouveaux essais reprend. Un webhook désactivé ne le reçoit pas : l’envoi passe en échec avec webhook_disabled. 409 delivery_in_progress si un essai de cet envoi est en cours. Compte dans la limite des tests.

Voir aussiLien vers la section Voir aussi

Moon AI

Moon AI en action

0:00

À découvrir

Captures de chat.realmoon.ai du 29 octobre 2026, avec des données fictives.

Moon AI

Vos préférences

Aucun suivi publicitaire.

Ce site ne dépose aucun cookie publicitaire ni cookie de mesure d’audience : notre serveur compte les visites sans cookie. Votre thème et la fermeture de l’information sur les cookies sont mémorisés sur cet appareil.

L’application Moon AI possède ses propres réglages de confidentialité.