Intégration · Référence
Webhooks
Envoyez des événements signés à vos systèmes externes.
Utilisez les webhooks pour recevoir les événements des apprenants et des notifications au lieu d'interroger l'API.
Créer un webhook
- Ouvrez Administration > Webhooks et créez un webhook.
- Saisissez l'URL d'un endpoint public.
- Sélectionnez les événements ou laissez la liste vide pour tous les recevoir.
- Ajoutez les en-têtes personnalisés, activez le webhook et enregistrez-le.
- Copiez le secret de signature dans votre gestionnaire de secrets.
Chaque organisation peut avoir jusqu'à cinq webhooks. La désactivation suspend les nouvelles livraisons sans supprimer le webhook.
Événements
| Événement | Moment de l'envoi |
|---|---|
learner.started | Un apprenant commence une activité. |
learner.updated | La tentative enregistrée d'un apprenant change. |
learner.completed | Une tentative devient terminée, réussie ou échouée. |
course.invite | Un apprenant est invité à un cours. |
course.request | Un apprenant demande l'accès à un cours. |
course.request.accepted | Une demande d'accès à un cours est acceptée. |
course.request.rejected | Une demande d'accès à un cours est refusée. |
course.completion | Une notification d'achèvement de cours est créée. |
course.inactive | Une notification de cours inactif est créée. |
collection.invite | Un apprenant est invité à une collection. |
collection.request | Un apprenant demande l'accès à une collection. |
collection.request.accepted | Une demande d'accès à une collection est acceptée. |
collection.request.rejected | Une demande d'accès à une collection est refusée. |
collection.completion | Une notification d'achèvement de collection est créée. |
Kokobi envoie une requête HTTP POST contenant { "event": string, "data": object }. Les dates sont des chaînes ISO 8601. Une sélection vide reçoit aussi les nouveaux types d'événements. Ignorez les champs inutilisés par votre gestionnaire.
En-têtes et signatures
Chaque livraison comprend :
Content-Type: application/json
webhook-timestamp: 2026-07-24T12:00:00.000Z
webhook-signature: <signature-hexadécimale>Les en-têtes personnalisés sont aussi inclus. Content-Type, webhook-timestamp et webhook-signature sont réservés.
La signature est le condensat HMAC-SHA256 encodé en hexadécimal de :
timestamp + "." + raw_request_bodyVérifiez-la avec les octets exacts du corps avant d'analyser le JSON :
import { createHmac, timingSafeEqual } from "node:crypto";
const verify = (body: Buffer, timestamp: string, signature: string) => {
const expected = createHmac("sha256", process.env.KOKOBI_WEBHOOK_SECRET!)
.update(timestamp + ".")
.update(body)
.digest();
const provided = Buffer.from(signature, "hex");
return (
provided.length === expected.length && timingSafeEqual(provided, expected)
);
};Refusez aussi les anciens horodatages pour limiter les attaques par rejeu. N'exposez jamais le secret dans du code côté client.
Livraison
Une réponse 2xx marque la livraison comme réussie. Les erreurs réseau et les autres réponses font l'objet de nouvelles tentatives avec des délais croissants.
Vérifiez la signature, enregistrez ou mettez l'événement en file d'attente, puis répondez rapidement. Traitez les tâches lentes de façon asynchrone et dédupliquez les événements, car une nouvelle tentative peut livrer les mêmes données plusieurs fois. Les livraisons terminées deviennent admissibles au nettoyage après sept jours.
Dépannage
- Livraison en attente : confirmez que l'URL est publique, accepte les requêtes
POSTJSON et retourne2xx. - Signature invalide : utilisez le bon secret et le corps brut non modifié. N'ajoutez pas de saut de ligne.
- Événement absent : confirmez que le webhook est activé, que son filtre correspond et que l'action a eu lieu dans la même organisation.
Consultez le journal de livraison de Kokobi et les journaux de votre pare-feu, proxy et application avant de réessayer.
Types de données des événements
Les types suivants décrivent la propriété data de chaque événement. Les dates sont des chaînes ISO 8601. Des champs peuvent être ajoutés; ignorez les propriétés inutilisées par votre gestionnaire.
Événements apprenant
learner.started, learner.updated et learner.completed utilisent la même structure.
export type LearnerStartedData = LearnerEventData;
export type LearnerUpdatedData = LearnerEventData;
export type LearnerCompletedData = LearnerEventData;
export type LearnerEventData = {
user: {
id: string;
email: string;
name: string;
};
connection: {
userId: string;
organizationId: string;
externalId: string | null;
respondedAt: string | null;
connectType: "invite" | "request";
connectStatus:
| "pending"
| "accepted"
| "rejected"
| "disconnected"
| "removed";
createdAt: string;
updatedAt: string;
} & ({ courseId: string } | { collectionId: string });
attempt: {
id: string;
userId: string;
organizationId: string;
moduleId: string;
courseId: string;
completedAt: string | null;
data: Record<string, string>;
status: "completed" | "passed" | "failed" | "in-progress" | "not-started";
score?: { raw?: number; min?: number; max?: number };
module: {
id: string;
name: string | null;
courseId: string;
locale: "en" | "fr";
multilingual: boolean;
type: "1.2" | "2004";
versionNumber: number;
sizeBytes: number | null;
deletedAt: string | null;
createdAt: string;
updatedAt: string;
};
createdAt: string;
updatedAt: string;
} | null;
};Événements de cours
export type CourseInviteData = {
to: string[];
data: { id: string; reminderDays?: 14 | 30 | 90 };
};
export type CourseInactiveData = {
to: string[];
data: { id: string; reminderDays?: 30 | 90 };
};
export type CourseCompletionData = {
to: string[];
data: { id: string };
};
export type CourseRequestData = {
to: string[];
data: {
id: string;
userId: string;
userEmail: string;
userName?: string;
};
};
export type CourseRequestAcceptedData = {
to: string[];
data: { id: string };
};
export type CourseRequestRejectedData = {
to: string[];
data: { id: string };
};Événements de collection
export type CollectionInviteData = {
to: string[];
data: { id: string; reminderDays?: 14 | 30 | 90 };
};
export type CollectionCompletionData = {
to: string[];
data: { id: string };
};
export type CollectionRequestData = {
to: string[];
data: {
id: string;
userId: string;
userEmail: string;
userName?: string;
};
};
export type CollectionRequestAcceptedData = {
to: string[];
data: { id: string };
};
export type CollectionRequestRejectedData = {
to: string[];
data: { id: string };
};