{"openapi":"3.0.3","info":{"title":"Créno API","version":"1.0.0","description":"API REST de la plateforme de prise de rendez-vous Créno. Authentification par clé API Bearer."},"servers":[{"url":"/api/v1","description":"API v1"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"Clé API Créno (format: creno_<suffix>.<secret>)"},"CronSecret":{"type":"apiKey","in":"header","name":"x-cron-secret","description":"Secret partagé de l'ordonnanceur (valeur de CRON_SECRET). Accepté aussi via en-tête Authorization: Bearer <secret>. Réservé aux tâches planifiées (resynchronisation Outlook, purge RGPD)."}},"schemas":{"Agency":{"type":"object","properties":{"id":{"type":"integer"},"orgId":{"type":"integer"},"name":{"type":"string"},"slug":{"type":"string"},"addressLine":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"timezone":{"type":"string"},"phone":{"type":"string","nullable":true},"channels":{"type":"string","description":"JSON array de canaux"},"active":{"type":"integer","enum":[0,1]}}},"Skill":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"color":{"type":"string"},"active":{"type":"integer"}}},"AppointmentType":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"durationMin":{"type":"integer"},"bufferMin":{"type":"integer"},"channels":{"type":"string"},"color":{"type":"string"},"active":{"type":"integer"}}},"User":{"type":"object","properties":{"id":{"type":"integer"},"orgId":{"type":"integer"},"email":{"type":"string","format":"email"},"firstName":{"type":"string"},"lastName":{"type":"string"},"role":{"type":"string","enum":["admin","manager","collaborateur"]},"title":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"agencyIds":{"type":"array","items":{"type":"integer"},"description":"Agences de rattachement"},"skillIds":{"type":"array","items":{"type":"integer"},"description":"Compétences"},"scopeAgencyIds":{"type":"array","items":{"type":"integer"},"description":"Périmètre managé (managers)"},"outlookUpn":{"type":"string","nullable":true,"description":"Adresse Microsoft 365 (UPN) liée à l'agenda Outlook. Défaut = e-mail."},"outlookConnected":{"type":"boolean"},"distributionPriority":{"type":"integer","description":"Priorité de répartition (plus petit = prioritaire)"},"active":{"type":"boolean","description":"false = compte désactivé (non réservable, connexion bloquée)"},"createdAt":{"type":"string","format":"date-time"}}},"UserCreate":{"type":"object","required":["email","firstName","lastName","role"],"properties":{"email":{"type":"string","format":"email"},"firstName":{"type":"string"},"lastName":{"type":"string"},"role":{"type":"string","enum":["admin","manager","collaborateur"]},"agencyIds":{"type":"array","items":{"type":"integer"}},"skillIds":{"type":"array","items":{"type":"integer"}},"scopeAgencyIds":{"type":"array","items":{"type":"integer"}},"title":{"type":"string"},"phone":{"type":"string"},"outlookUpn":{"type":"string","description":"Optionnel. Si absent, l'e-mail est utilisé."},"password":{"type":"string","description":"Optionnel. Si absent, un mot de passe provisoire est généré."},"distributionPriority":{"type":"integer"}}},"UserUpdate":{"type":"object","description":"Mise à jour partielle : seuls les champs fournis sont modifiés. active:true réactive un compte désactivé.","properties":{"email":{"type":"string","format":"email"},"firstName":{"type":"string"},"lastName":{"type":"string"},"role":{"type":"string","enum":["admin","manager","collaborateur"]},"agencyIds":{"type":"array","items":{"type":"integer"}},"skillIds":{"type":"array","items":{"type":"integer"}},"scopeAgencyIds":{"type":"array","items":{"type":"integer"}},"title":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"outlookUpn":{"type":"string","nullable":true},"password":{"type":"string"},"distributionPriority":{"type":"integer"},"active":{"type":"boolean"}}},"Slot":{"type":"object","properties":{"startIso":{"type":"string","format":"date-time"},"endIso":{"type":"string","format":"date-time"},"candidateUserIds":{"type":"array","items":{"type":"integer"}}}},"DaySlots":{"type":"object","properties":{"date":{"type":"string","format":"date","example":"2026-06-15"},"slots":{"type":"array","items":{"$ref":"#/components/schemas/Slot"}}}},"AppointmentSerialized":{"type":"object","properties":{"id":{"type":"integer"},"ref":{"type":"string","example":"CRN-7F3A2K"},"status":{"type":"string","enum":["booked","cancelled","completed","transferred","no_show"]},"startAt":{"type":"string","format":"date-time"},"endAt":{"type":"string","format":"date-time"},"channel":{"type":"string","enum":["agence","visio","telephone"]},"agencyId":{"type":"integer"},"typeId":{"type":"integer"},"userId":{"type":"integer","nullable":true},"client":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string","nullable":true}}},"manageUrl":{"type":"string","format":"uri"},"deepLink":{"type":"string","format":"uri"},"history":{"type":"array","description":"Historique complet du RDV (modifications, changements de conseiller, de créneau, annulation, clôture). Présent uniquement sur GET /appointments/{id}.","items":{"$ref":"#/components/schemas/AppointmentHistoryEntry"}}}},"AppointmentHistoryEntry":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"action":{"type":"string","enum":["created","rescheduled","reassigned","transferred","info_updated","cancelled","completed","no_show"]},"actor":{"type":"object","properties":{"type":{"type":"string","enum":["client","user","system"]},"id":{"type":"integer","nullable":true},"label":{"type":"string","nullable":true}}},"changes":{"type":"object","nullable":true,"description":"Valeurs avant/après par champ modifié, ex { startAt: { from, to } } ou { userId: { from, to } }","additionalProperties":{"type":"object","properties":{"from":{},"to":{}}}}}},"Reassignment":{"type":"object","description":"Détail d'une réaffectation de RDV (présent dans le payload de appointment.reassigned).","properties":{"fromUserId":{"type":"integer","nullable":true,"description":"Ancien conseiller (null si le RDV n'était pas encore affecté)."},"toUserId":{"type":"integer","description":"Nouveau conseiller."},"mode":{"type":"string","enum":["reassigned","transferred"],"description":"reassigned = simple changement de conseiller ; transferred = transfert (le RDV redevient booked, une trace « transferred » est conservée en historique)."}}},"WebhookEventEnvelope":{"type":"object","description":"Corps JSON POST vers l'URL abonnée. Signé (en-tête X-Creno-Signature: sha256=<hmac>) avec le secret du webhook. Le champ data porte le RDV sérialisé ; appointment.reassigned ajoute en plus l'objet reassignment.","properties":{"event":{"type":"string","enum":["appointment.created","appointment.updated","appointment.cancelled","appointment.reassigned","appointment.no_show","appointment.completed"]},"data":{"$ref":"#/components/schemas/AppointmentSerialized"},"reassignment":{"allOf":[{"$ref":"#/components/schemas/Reassignment"}],"description":"Présent uniquement pour appointment.reassigned."}}},"OutlookResyncResult":{"type":"object","properties":{"ok":{"type":"boolean"},"result":{"type":"object","properties":{"processed":{"type":"integer","description":"Entrées de file traitées lors de cet appel."},"succeeded":{"type":"integer","description":"Opérations Graph rejouées avec succès."},"requeued":{"type":"integer","description":"Opérations remises en file (échec transitoire, backoff)."},"abandoned":{"type":"integer","description":"Opérations abandonnées après trop de tentatives."}}}}},"PurgeResult":{"type":"object","properties":{"ok":{"type":"boolean"},"purged":{"type":"object","description":"Volumes supprimés par la purge à durée contractuelle (RGPD). Idempotent.","properties":{"ranAt":{"type":"string","format":"date-time"},"attachments":{"type":"object","properties":{"filesDeleted":{"type":"integer","description":"Pièces jointes effacées du disque."},"filesMissing":{"type":"integer"},"rowsMarked":{"type":"integer","description":"Lignes marquées supprimées (soft-delete)."}}},"accessLogs":{"type":"object","properties":{"deleted":{"type":"integer","description":"Journaux d'accès de plus de 12 mois purgés."},"cutoff":{"type":"string","format":"date-time"}}},"webhookDeliveries":{"type":"object","properties":{"deleted":{"type":"integer","description":"Livraisons de webhooks de plus de 12 mois purgées."}}}}}}},"LogExportPage":{"type":"object","description":"Page de journaux exportés. Chaque item est un objet plat clé → valeur string, avec les mêmes colonnes que l'export du back-office (access : id, createdAt, actorLabel, userId, action, resource, ip, meta ; audit : id, createdAt, actorLabel, userId, entity, entityId, action, before, after ; email : id, createdAt, toEmail, subject, template, status, provider, appointmentId, error ; sms : id, createdAt, toNumber, fromName, message, status, provider, appointmentId, error).","properties":{"items":{"type":"array","items":{"type":"object","additionalProperties":{"type":"string"}},"description":"Lignes du journal, de la plus récente à la plus ancienne."},"total":{"type":"integer","description":"Nombre total de lignes correspondant aux filtres (avant limit/offset)."},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"Error":{"type":"object","properties":{"error":{"type":"string"}}}}},"security":[{"BearerAuth":[]}],"paths":{"/agencies":{"get":{"summary":"Lister les agences","operationId":"listAgencies","tags":["Référentiels"],"responses":{"200":{"description":"Liste des agences","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Agency"}}}}}}},"401":{"description":"Non autorisé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/skills":{"get":{"summary":"Lister les compétences","operationId":"listSkills","tags":["Référentiels"],"responses":{"200":{"description":"Liste des compétences","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Skill"}}}}}}},"401":{"description":"Non autorisé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/appointment-types":{"get":{"summary":"Lister les thèmes de RDV","operationId":"listAppointmentTypes","tags":["Référentiels"],"responses":{"200":{"description":"Liste des thèmes","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AppointmentType"}}}}}}},"401":{"description":"Non autorisé"}}}},"/users":{"get":{"summary":"Lister les collaborateurs","operationId":"listUsers","tags":["Utilisateurs"],"responses":{"200":{"description":"Liste des utilisateurs","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}},"401":{"description":"Non autorisé"}}},"post":{"summary":"Créer un collaborateur","description":"Provisionne un compte. outlookUpn par défaut = e-mail. Mot de passe provisoire si non fourni.","operationId":"createUser","tags":["Utilisateurs"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreate"}}}},"responses":{"201":{"description":"Utilisateur créé","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/User"}}}}}},"400":{"description":"Corps invalide (champ requis, e-mail, rôle, agence/compétence inconnue)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Non autorisé"},"403":{"description":"Scope write requis"},"409":{"description":"E-mail déjà utilisé"}}}},"/users/{id}":{"get":{"summary":"Détail d'un collaborateur","operationId":"getUser","tags":["Utilisateurs"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Utilisateur","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/User"}}}}}},"401":{"description":"Non autorisé"},"404":{"description":"Utilisateur introuvable"}}},"patch":{"summary":"Mettre à jour un collaborateur","description":"Mise à jour partielle. Envoyer active:true pour réactiver un compte désactivé.","operationId":"updateUser","tags":["Utilisateurs"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}}},"responses":{"200":{"description":"Utilisateur mis à jour","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/User"}}}}}},"400":{"description":"Corps invalide"},"401":{"description":"Non autorisé"},"403":{"description":"Scope write requis"},"404":{"description":"Utilisateur introuvable"},"409":{"description":"E-mail déjà utilisé par un autre compte"}}},"delete":{"summary":"Désactiver un collaborateur","description":"Désactivation LOGIQUE : le compte n'est plus réservable et ne peut plus se connecter. Les rendez-vous historiques restent intacts. Réactivation possible via PATCH active:true.","operationId":"deactivateUser","tags":["Utilisateurs"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Compte désactivé","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/User"}}}}}},"401":{"description":"Non autorisé"},"403":{"description":"Scope write requis"},"404":{"description":"Utilisateur introuvable"}}}},"/availabilities":{"get":{"summary":"Obtenir les créneaux disponibles","operationId":"getAvailabilities","tags":["Disponibilités"],"parameters":[{"name":"agencyId","in":"query","required":true,"schema":{"type":"integer"}},{"name":"typeId","in":"query","required":true,"schema":{"type":"integer"}},{"name":"channel","in":"query","required":false,"schema":{"type":"string","default":"agence"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":21}}],"responses":{"200":{"description":"Créneaux par jour","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DaySlots"}}}}}}},"400":{"description":"Paramètres manquants"},"401":{"description":"Non autorisé"}}}},"/appointments":{"get":{"summary":"Lister les rendez-vous","operationId":"listAppointments","tags":["Rendez-vous"],"parameters":[{"name":"status","in":"query","schema":{"type":"string"}},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"agencyId","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Liste des RDV","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AppointmentSerialized"}}}}}}},"401":{"description":"Non autorisé"}}},"post":{"summary":"Créer un rendez-vous","operationId":"createAppointment","tags":["Rendez-vous"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["agencyId","typeId","channel","startIso","client"],"properties":{"agencyId":{"type":"integer"},"typeId":{"type":"integer"},"channel":{"type":"string","enum":["agence","visio","telephone"]},"startIso":{"type":"string","format":"date-time"},"candidateUserIds":{"type":"array","items":{"type":"integer"}},"client":{"type":"object","required":["firstName","lastName","email"],"properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string","format":"email"},"phone":{"type":"string"}}}}}}}},"responses":{"201":{"description":"RDV créé","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AppointmentSerialized"}}}}}},"400":{"description":"Corps invalide"},"401":{"description":"Non autorisé"},"403":{"description":"Scope write requis"},"409":{"description":"Créneau indisponible"}}}},"/appointments/{id}":{"get":{"summary":"Détail d'un rendez-vous","operationId":"getAppointment","tags":["Rendez-vous"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"RDV","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AppointmentSerialized"}}}}}},"404":{"description":"RDV introuvable"}}},"patch":{"summary":"Reprogrammer ou réaffecter un RDV","operationId":"updateAppointment","tags":["Rendez-vous"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"startIso":{"type":"string","format":"date-time","description":"Nouvelle date/heure"},"userId":{"type":"integer","description":"Nouvel ID collaborateur"}}}}}},"responses":{"200":{"description":"RDV mis à jour"},"403":{"description":"Scope write requis"},"409":{"description":"Conflit"}}},"delete":{"summary":"Annuler un rendez-vous","operationId":"cancelAppointment","tags":["Rendez-vous"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"RDV annulé"},"403":{"description":"Scope write requis"},"404":{"description":"RDV introuvable"}}}},"/logs":{"get":{"summary":"Exporter les journaux (SIEM)","description":"Journaux de l'organisation, exportables par API pour collecte programmatique (SIEM) : accès (connexions, exports, appels API), audit du paramétrage (avant/après), e-mails et SMS envoyés. Filtrables par période. format=json renvoie une page { items, total, limit, offset } ; format=csv renvoie le fichier CSV Excel-FR (séparateur « ; », BOM UTF-8, fins de ligne CRLF), mêmes colonnes que l'export du back-office. Collecte incrémentale : appeler périodiquement avec from = date du dernier relevé. Chaque appel est lui-même journalisé dans le journal d'accès (action « export »).","operationId":"exportLogs","tags":["Journaux"],"parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["access","audit","email","sms"],"default":"access"},"description":"Journal à exporter : access = journal d'accès, audit = modifications du paramétrage, email = e-mails envoyés, sms = SMS envoyés."},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Borne basse (incluse) sur la date de création, ISO 8601."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Borne haute (incluse) sur la date de création, ISO 8601."},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","csv"],"default":"json"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":1000,"minimum":1,"maximum":10000},"description":"Nombre max de lignes renvoyées."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"minimum":0},"description":"Décalage de pagination."}],"responses":{"200":{"description":"Journaux exportés","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogExportPage"}},"text/csv":{"schema":{"type":"string"},"example":"ID;Date;Acteur;Utilisateur ID;Action;Ressource;IP;Métadonnées\r\n42;2026-07-18T09:00:00Z;API;;export;logs/access;203.0.113.7;"}}},"400":{"description":"Paramètre invalide (type, format, limit ou offset)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Non autorisé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/{votre-endpoint-webhook}":{"post":{"summary":"Réception d'un événement webhook Créno","description":"Payload POST envoyé par Créno vers l'URL d'un webhook abonné. Signé HMAC-SHA256 (en-tête X-Creno-Signature: sha256=<hmac> calculé sur le corps brut avec le secret du webhook). Événements : appointment.created, appointment.updated, appointment.cancelled, appointment.reassigned (ajoute reassignment), appointment.no_show, appointment.completed. Répondre 2xx pour accuser réception ; sinon Créno retente avec backoff.","operationId":"receiveWebhookEvent","tags":["Webhooks"],"security":[],"parameters":[{"name":"X-Creno-Signature","in":"header","required":true,"schema":{"type":"string","example":"sha256=1f8ac..."},"description":"Signature HMAC-SHA256 du corps brut avec le secret du webhook."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEventEnvelope"}}}},"responses":{"200":{"description":"Événement accusé (toute réponse 2xx convient)."}}}},"/cron/outlook-resync":{"servers":[{"url":"/api","description":"Racine des tâches planifiées (hors /api/v1)"}],"post":{"summary":"Rejouer la file de resynchronisation Outlook","description":"Rejeu des opérations Microsoft Graph mises en file après une indisponibilité (la réservation aboutit toujours, l'événement Outlook est synchronisé dès rétablissement). À appeler périodiquement (ex : toutes les 5 min) par un ordonnanceur externe.","operationId":"cronOutlookResync","tags":["Tâches planifiées"],"security":[{"CronSecret":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer","default":50,"maximum":500,"description":"Nombre max d'entrées traitées sur cet appel."}}}}}},"responses":{"200":{"description":"File traitée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutlookResyncResult"}}}},"401":{"description":"Secret manquant ou invalide (ou CRON_SECRET non configuré côté serveur).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/cron/purge":{"servers":[{"url":"/api","description":"Racine des tâches planifiées (hors /api/v1)"}],"post":{"summary":"Purge à durée contractuelle (RGPD)","description":"Applique les purges de rétention : pièces jointes des RDV passés effacées du disque, journaux d'accès et livraisons de webhooks de plus de 12 mois supprimés. Idempotente. Chaque exécution est journalisée (volumes supprimés).","operationId":"cronPurge","tags":["Tâches planifiées"],"security":[{"CronSecret":[]}],"responses":{"200":{"description":"Purge exécutée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurgeResult"}}}},"401":{"description":"Secret manquant ou invalide.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"CRON_SECRET non configuré côté serveur (purge refusée par sécurité).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}