{"openapi":"3.1.0","info":{"title":"Pre-Go — API des applications connectées","version":"1.0.0","description":"API d'un salon Pre-Go pour ses propres outils. Authentification par jeton porteur créé dans Paramètres → Applications connectées. Documentation pour LLM : /api/v1/docs.md"},"servers":[{"url":"https://pre-go.fr/api/v1"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Jeton `pgo_…` d'une application connectée."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["unauthorized","forbidden_scope","not_found","method_not_allowed","invalid_request","conflict","unprocessable","rate_limited","internal_error"]},"message":{"type":"string"},"details":{}}}}}}},"paths":{"/me":{"get":{"operationId":"whoami","summary":"Qui suis-je ?","description":"Renvoie l'application connectée, son salon et ses droits (scopes). Aucun droit requis : à appeler en premier pour savoir ce que le jeton permet.","tags":["meta"],"parameters":[],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":{"app":{"id":"app_x","name":"Mon tableur"},"shop":{"id":4,"slug":"le-barbier","timezone":"Europe/Paris"},"scopes":["shop:read","appointments:read"]}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/shop":{"get":{"operationId":"get_shop","summary":"Informations du salon","description":"Renvoie le salon lié au jeton : nom, adresse, contact, fuseau horaire (toutes les heures de l'API sont exprimées dans ce fuseau), horaires d'ouverture et lien de réservation en ligne.","tags":["shop"],"x-required-scope":"shop:read","parameters":[],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":{"id":4,"name":"Le Barbier","slug":"le-barbier","timezone":"Europe/Paris","businessHours":{"monday":{"open":"09:00","close":"19:00","closed":false}},"bookingUrl":"https://pre-go.fr/sites/le-barbier/booking"}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/services":{"get":{"operationId":"list_services","summary":"Catalogue des prestations","description":"Liste les prestations du salon avec prix (en euros TTC), durée (en minutes) et catégorie. Par défaut, seules les prestations actives.","tags":["services"],"x-required-scope":"services:read","parameters":[{"name":"includeInactive","in":"query","required":false,"schema":{"description":"true pour inclure les prestations archivées.","type":["boolean","string"]},"description":"true pour inclure les prestations archivées."}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":[{"id":12,"name":"Coupe homme","category":"Haircut","price":25,"discountPrice":null,"durationMinutes":30,"bookableOnline":true,"isActive":true}]}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/staff":{"get":{"operationId":"list_staff","summary":"Équipe du salon","description":"Liste les barbiers (et membres du back-office) du salon. `acceptsBookings` indique qui peut recevoir des rendez-vous ; utilisez leur `id` comme `staffId` ailleurs.","tags":["staff"],"x-required-scope":"staff:read","parameters":[{"name":"includeInactive","in":"query","required":false,"schema":{"description":"true pour inclure les membres désactivés.","type":["boolean","string"]},"description":"true pour inclure les membres désactivés."}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":[{"id":"mbr_x1","firstName":"Karim","lastName":"B.","title":"Barber","acceptsBookings":true,"isActive":true}]}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/availability":{"get":{"operationId":"get_availability","summary":"Créneaux libres d'une journée","description":"Renvoie les créneaux libres d'une journée pour une prestation, tous barbiers confondus ou pour un barbier donné. Mêmes règles que la réservation en ligne : horaires, congés, rendez-vous existants, créneaux passés exclus. Utilisez un créneau renvoyé ici pour `create_appointment`.","tags":["availability"],"x-required-scope":"availability:read","parameters":[{"name":"date","in":"query","required":true,"schema":{"type":"string","description":"Jour voulu, YYYY-MM-DD, dans le fuseau du salon."},"description":"Jour voulu, YYYY-MM-DD, dans le fuseau du salon."},{"name":"serviceId","in":"query","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"Prestation (voir list_services)."},"description":"Prestation (voir list_services)."},{"name":"staffId","in":"query","required":false,"schema":{"description":"Barbier précis (voir list_staff). Absent : tous les barbiers.","type":"string","minLength":1},"description":"Barbier précis (voir list_staff). Absent : tous les barbiers."}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":{"date":"2026-10-02","slots":[{"startTime":"10:00","endTime":"10:30","staffId":"mbr_x1"}]}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/appointments":{"get":{"operationId":"list_appointments","summary":"Rendez-vous sur une période","description":"Liste les rendez-vous entre deux dates incluses (92 jours au plus), triés par date puis heure. Filtres facultatifs : barbier, client, statut. Heures au format HH:mm dans le fuseau du salon ; `startsAt` est l'instant UTC exact.","tags":["appointments"],"x-required-scope":"appointments:read","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","description":"Premier jour, YYYY-MM-DD."},"description":"Premier jour, YYYY-MM-DD."},{"name":"to","in":"query","required":true,"schema":{"type":"string","description":"Dernier jour inclus, YYYY-MM-DD."},"description":"Dernier jour inclus, YYYY-MM-DD."},{"name":"staffId","in":"query","required":false,"schema":{"type":"string","minLength":1}},{"name":"customerId","in":"query","required":false,"schema":{"type":"string","minLength":1}},{"name":"status","in":"query","required":false,"schema":{"description":"Un ou plusieurs statuts séparés par des virgules : pending, scheduled, confirmed, in_progress, completed, cancelled, no_show.","type":"string"},"description":"Un ou plusieurs statuts séparés par des virgules : pending, scheduled, confirmed, in_progress, completed, cancelled, no_show."},{"name":"limit","in":"query","required":false,"schema":{"default":200,"type":"integer","minimum":1,"maximum":500}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":[{"id":"apt_9f2","date":"2026-10-02","startTime":"10:00","endTime":"10:30","status":"confirmed","staff":{"id":"mbr_x1","name":"Karim B."},"service":{"id":12,"name":"Coupe homme"},"customer":{"id":"c81e","name":"Jean Dupont","email":"jean@example.com","phone":"+33612345678"},"price":25}],"meta":{"count":1,"limit":200,"offset":0,"hasMore":false}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_appointment","summary":"Créer un rendez-vous","description":"Réserve un créneau pour un client. Mêmes garde-fous que la réservation en ligne : refus d'un créneau passé ou déjà pris (409 conflict). Sans `staffId`, le salon choisit le barbier selon sa règle d'attribution. Le rendez-vous est confirmé d'office. Par défaut le client reçoit sa confirmation (e-mail, et SMS si le salon l'a activé) et le barbier est prévenu, comme pour une réservation en ligne. Vérifiez d'abord le créneau avec `get_availability`.","tags":["appointments"],"x-required-scope":"appointments:write","parameters":[],"responses":{"201":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":{"bookingGroupId":"bkg_1","appointments":[{"id":"apt_9f2","date":"2026-10-02","startTime":"10:00","status":"confirmed"}]}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string","description":"Jour du rendez-vous, YYYY-MM-DD."},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Heure de début, HH:mm, fuseau du salon."},"serviceId":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"staffId":{"description":"Barbier voulu. Absent : n'importe quel barbier disponible.","type":"string","minLength":1},"customerId":{"description":"Client existant (voir list_customers). Sinon, renseignez `customer`.","type":"string","minLength":1},"customer":{"description":"Coordonnées du client s'il n'a pas de fiche. Rattaché à une fiche existante si l'e-mail ou le téléphone correspond sans ambiguïté.","type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","maxLength":254,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"phone":{"type":"string","maxLength":40}},"required":["name"]},"customerNotes":{"description":"Demande du client, visible par le barbier.","type":"string","maxLength":1000},"notifyCustomer":{"default":true,"description":"false pour ne rien envoyer au client ni au barbier.","type":"boolean"}},"required":["date","startTime","serviceId"]}}}}}},"/appointments/{id}":{"get":{"operationId":"get_appointment","summary":"Détail d'un rendez-vous","description":"Renvoie un rendez-vous, avec toutes ses lignes de prestation.","tags":["appointments"],"x-required-scope":"appointments:read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Identifiant du rendez-vous."},"description":"Identifiant du rendez-vous."}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"update_appointment","summary":"Modifier un rendez-vous","description":"Déplace un rendez-vous (date, heure, barbier), change son statut ou ses notes. Passe par la même logique que le planning du QG : le barbier est prévenu d'un déplacement ou d'une annulation, la liste d'attente est sollicitée sur un créneau libéré. Attention : comme dans le QG, un déplacement n'est pas refusé s'il chevauche un autre rendez-vous — vérifiez `get_availability` avant. L'encaissement (statut `completed`) se fait au QG, pas ici. Pour annuler, préférez `cancel_appointment`.","tags":["appointments"],"x-required-scope":"appointments:write","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"date":{"description":"Nouveau jour, YYYY-MM-DD.","type":"string"},"startTime":{"description":"Nouvelle heure de début, HH:mm.","type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$"},"staffId":{"description":"Nouveau barbier.","type":"string","minLength":1},"durationMinutes":{"type":"integer","minimum":5,"maximum":720},"status":{"type":"string","enum":["scheduled","confirmed","no_show","cancelled"]},"notes":{"description":"Note interne du salon.","anyOf":[{"type":"string","maxLength":2000},{"type":"null"}]}}}}}}}},"/appointments/{id}/cancel":{"post":{"operationId":"cancel_appointment","summary":"Annuler un rendez-vous","description":"Annule un rendez-vous au nom du salon. Le barbier est prévenu et la liste d'attente est sollicitée, comme depuis le planning du QG. Le client n'est pas prévenu automatiquement (même comportement que le QG). Idempotent : annuler un rendez-vous déjà annulé le renvoie tel quel.","tags":["appointments"],"x-required-scope":"appointments:write","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"description":"Motif, visible dans la fiche du rendez-vous.","type":"string","maxLength":500}}}}}}}},"/customers":{"get":{"operationId":"list_customers","summary":"Rechercher dans le fichier client","description":"Liste les clients du salon, triés par nom. `search` cherche dans le prénom, le nom, l'e-mail et le téléphone (le format du numéro n'importe pas : 0612345678 trouve « 06 12 34 56 78 »). Un numéro enregistré ou modifié désormais est renvoyé au format international E.164 (`+33612345678`) ; une fiche plus ancienne garde le format saisi à l'époque, et un numéro non reconnu (étranger hors format international, coquille) reste tel que saisi.","tags":["customers"],"x-required-scope":"customers:read","parameters":[{"name":"search","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"limit","in":"query","required":false,"schema":{"default":50,"type":"integer","minimum":1,"maximum":100}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":[{"id":"c81e","firstName":"Jean","lastName":"Dupont","email":"jean@example.com","phone":"+33612345678","totalVisits":7,"lastVisitDate":"2026-09-12T08:00:00.000Z"}],"meta":{"count":1,"limit":50,"offset":0,"hasMore":false}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_customer","summary":"Créer une fiche client","description":"Crée une fiche client. Si l'e-mail correspond déjà à une fiche, ou le téléphone avec un nom proche (un numéro peut être partagé en famille), renvoie 422 avec l'`id` existant dans `details.existingCustomerId` au lieu de créer un doublon ; passez `allowDuplicate: true` pour forcer.","tags":["customers"],"x-required-scope":"customers:write","parameters":[],"responses":{"201":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string","minLength":1,"maxLength":80},"lastName":{"default":"","type":"string","maxLength":80},"email":{"anyOf":[{"type":"string","maxLength":254,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"phone":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]},"emailOptIn":{"type":"boolean"},"smsOptIn":{"type":"boolean"},"allowDuplicate":{"default":false,"type":"boolean"}},"required":["firstName"]}}}}}},"/customers/{id}":{"get":{"operationId":"get_customer","summary":"Fiche d'un client","description":"Renvoie la fiche d'un client : coordonnées, consentements, statistiques de visite. Ses rendez-vous s'obtiennent avec `list_appointments` et `customerId`.","tags":["customers"],"x-required-scope":"customers:read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"update_customer","summary":"Modifier une fiche client","description":"Met à jour les coordonnées ou les consentements d'un client. Seuls les champs envoyés changent ; `null` efface l'e-mail ou le téléphone.","tags":["customers"],"x-required-scope":"customers:write","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string","minLength":1,"maxLength":80},"lastName":{"type":"string","maxLength":80},"email":{"anyOf":[{"type":"string","maxLength":254,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"phone":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]},"emailOptIn":{"type":"boolean"},"smsOptIn":{"type":"boolean"}}}}}}}},"/sales":{"get":{"operationId":"list_sales","summary":"Encaissements sur une période","description":"Liste les tickets de caisse entre deux dates incluses (jours du salon, 92 jours au plus), du plus récent au plus ancien, avec un résumé : nombre de tickets, total encaissé et pourboires (tickets remboursés exclus du résumé). Montants en euros TTC.","tags":["sales"],"x-required-scope":"sales:read","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string"}},{"name":"to","in":"query","required":true,"schema":{"type":"string"}},{"name":"staffId","in":"query","required":false,"schema":{"type":"string","minLength":1}},{"name":"limit","in":"query","required":false,"schema":{"default":200,"type":"integer","minimum":1,"maximum":500}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":[{"id":"sal_1","receiptNumber":"T-2026-0042","createdAt":"2026-10-02T09:12:00.000Z","total":30,"tipAmount":5,"paymentMethod":"card","paymentStatus":"completed","staffId":"mbr_x1","customerId":"c81e","appointmentId":"apt_9f2"}],"meta":{"count":1,"limit":200,"offset":0,"hasMore":false},"summary":{"salesCount":1,"total":30,"tips":5}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sms/usage":{"get":{"operationId":"get_sms_usage","summary":"Consommation SMS sur une période","description":"Volumes SMS entre deux jours inclus (jours du salon, 366 jours au plus ; par défaut le mois en cours) : total, par barbier, par type de message et par jour. Mêmes chiffres que l'écran « Consommation SMS » du QG. Seuls les SMS facturés comptent (statuts pending, sent, delivered) ; `unbilled` dit combien de tentatives ne l'ont pas été (numéro invalide, plafond atteint, erreur du fournisseur). `messages` = SMS envoyés (un par destinataire) ; `segments` = unités facturées (un message long en compte plusieurs) — c'est `segments` qu'on refacture. Le barbier d'un SMS est celui du rendez-vous qui l'a déclenché ; `staffId: null` = sans barbier (SMS de test…). Le détail ligne à ligne est dans list_sms_messages.","tags":["sms"],"x-required-scope":"sms:read","parameters":[{"name":"from","in":"query","required":false,"schema":{"description":"Premier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `to` : mois en cours.","type":"string"},"description":"Premier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `to` : mois en cours."},{"name":"to","in":"query","required":false,"schema":{"description":"Dernier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `from` : mois en cours.","type":"string"},"description":"Dernier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `from` : mois en cours."},{"name":"staffId","in":"query","required":false,"schema":{"description":"Barbier (voir list_staff) : seuls les SMS de ses rendez-vous. Un identifiant d'un autre salon ne renvoie rien.","type":"string","minLength":1,"maxLength":64},"description":"Barbier (voir list_staff) : seuls les SMS de ses rendez-vous. Un identifiant d'un autre salon ne renvoie rien."}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":{"period":{"from":"2026-09-01","to":"2026-09-30","timeZone":"Europe/Paris"},"totals":{"messages":412,"segments":431},"byStaff":[{"staffId":"mbr_k4r1m","name":"Karim Benali","messages":240,"segments":251},{"staffId":"mbr_s4m1","name":"Sami Haddad","messages":170,"segments":178},{"staffId":null,"name":"Sans barbier","messages":2,"segments":2}],"byType":[{"type":"reminder_24h","label":"Rappel 24h","messages":300,"segments":300},{"type":"booking_confirmation","label":"Confirmation de réservation","messages":110,"segments":129},{"type":"sms_test","label":"SMS de test","messages":2,"segments":2}],"byDay":[{"date":"2026-09-01","messages":14,"segments":15}],"unbilled":3}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sms/messages":{"get":{"operationId":"list_sms_messages","summary":"SMS envoyés, ligne à ligne","description":"Le détail qui justifie une refacturation : un SMS par ligne, du plus récent au plus ancien, entre deux jours inclus (jours du salon, 366 jours au plus ; par défaut le mois en cours). Par défaut seuls les SMS facturés (statuts pending, sent, delivered) ; `status=unbilled` pour les tentatives non facturées, `status=all` pour tout. Chaque ligne donne le barbier du rendez-vous, le type de message, le nombre de segments facturés, et les identifiants du rendez-vous et du client. Ni numéro ni texte du message. `day` est le jour du salon qui sert aux totaux de get_sms_usage.","tags":["sms"],"x-required-scope":"sms:read","parameters":[{"name":"from","in":"query","required":false,"schema":{"description":"Premier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `to` : mois en cours.","type":"string"},"description":"Premier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `to` : mois en cours."},{"name":"to","in":"query","required":false,"schema":{"description":"Dernier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `from` : mois en cours.","type":"string"},"description":"Dernier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `from` : mois en cours."},{"name":"staffId","in":"query","required":false,"schema":{"description":"Barbier (voir list_staff) : seuls les SMS de ses rendez-vous. Un identifiant d'un autre salon ne renvoie rien.","type":"string","minLength":1,"maxLength":64},"description":"Barbier (voir list_staff) : seuls les SMS de ses rendez-vous. Un identifiant d'un autre salon ne renvoie rien."},{"name":"status","in":"query","required":false,"schema":{"default":"billed","description":"`billed` (défaut) : SMS facturés ; `unbilled` : tentatives non facturées ; `all` : tout.","type":"string","enum":["billed","unbilled","all"]},"description":"`billed` (défaut) : SMS facturés ; `unbilled` : tentatives non facturées ; `all` : tout."},{"name":"limit","in":"query","required":false,"schema":{"default":200,"type":"integer","minimum":1,"maximum":500}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":[{"id":98123,"sentAt":"2026-09-30T21:30:00.000Z","day":"2026-09-30","staff":{"id":"mbr_k4r1m","name":"Karim Benali"},"type":"reminder_24h","typeLabel":"Rappel 24h","segments":1,"status":"delivered","billed":true,"appointmentId":"apt_9f2","customerId":"c81e"}],"meta":{"count":1,"limit":200,"offset":0,"hasMore":false},"period":{"from":"2026-09-01","to":"2026-09-30","timeZone":"Europe/Paris"}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/products":{"get":{"operationId":"list_products","summary":"Produits et niveaux de stock","description":"Liste les produits du salon (vente au comptoir, consommables, linge réutilisable) avec leur stock. `stock.status` vaut `out` (épuisé), `low` (au seuil minimum ou dessous, comme l'alerte du QG) ou `ok`. Filtre `stock=low` pour la liste à recommander (épuisés compris). `summary` porte sur tous les produits actifs, pas seulement la page : nombre, produits bas, épuisés, valeur du stock au prix d'achat (€ HT).","tags":["inventory"],"x-required-scope":"inventory:read","parameters":[{"name":"search","in":"query","required":false,"schema":{"description":"Cherche dans le nom, la référence (SKU), le code-barres et la marque.","type":"string","minLength":1,"maxLength":100},"description":"Cherche dans le nom, la référence (SKU), le code-barres et la marque."},{"name":"categoryId","in":"query","required":false,"schema":{"description":"Catégorie (voir list_product_categories).","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"description":"Catégorie (voir list_product_categories)."},{"name":"kind","in":"query","required":false,"schema":{"description":"retail : vente au comptoir ; consumable : produit entamé pendant les prestations ; reusable : linge, serviettes.","type":"string","enum":["consumable","reusable","retail"]},"description":"retail : vente au comptoir ; consumable : produit entamé pendant les prestations ; reusable : linge, serviettes."},{"name":"stock","in":"query","required":false,"schema":{"description":"low : au seuil minimum ou dessous (épuisés compris) ; out : épuisés seulement.","type":"string","enum":["low","out"]},"description":"low : au seuil minimum ou dessous (épuisés compris) ; out : épuisés seulement."},{"name":"includeInactive","in":"query","required":false,"schema":{"description":"true pour inclure les produits archivés.","type":["boolean","string"]},"description":"true pour inclure les produits archivés."},{"name":"limit","in":"query","required":false,"schema":{"default":100,"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":[{"id":31,"name":"Cire mate 100 ml","sku":"CIRE-MAT-100","barcode":"3760000000017","brand":"Reuzel","category":{"id":2,"name":"Coiffants"},"kind":"retail","sellingPrice":19.9,"costPrice":9.5,"taxRate":20,"stock":{"current":2,"status":"low","minimum":3,"maximum":12,"reorderPoint":null,"reorderQuantity":6,"tracked":true,"allowNegative":false},"openUnit":null,"reusable":null,"isActive":true}],"meta":{"count":1,"limit":100,"offset":0,"hasMore":false},"summary":{"products":24,"lowStock":3,"outOfStock":1,"stockValueAtCost":812.4}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_product","summary":"Créer un produit","description":"Ajoute un produit au catalogue du salon. Prix en euros : `sellingPrice` TTC, `costPrice` HT. Si un produit porte déjà ce `sku` (casse ignorée), renvoie 422 avec son `id` dans `details.existingProductId` au lieu de créer un doublon ; passez `allowDuplicateSku: true` pour forcer. `initialStock` crée le stock de départ avec un mouvement « Stock initial » dans l'historique.","tags":["inventory"],"x-required-scope":"inventory:write","parameters":[],"responses":{"201":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"sku":{"type":"string","minLength":1,"maxLength":64},"costPrice":{"type":"number","minimum":0,"maximum":100000},"sellingPrice":{"type":"number","minimum":0,"maximum":100000},"kind":{"default":"retail","description":"retail : vente au comptoir ; consumable : entamé pendant les prestations ; reusable : linge.","type":"string","enum":["consumable","reusable","retail"]},"barcode":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}]},"brand":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]},"description":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}]},"categoryId":{"anyOf":[{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},{"type":"null"}]},"taxRate":{"default":20,"description":"TVA en %, 20 par défaut.","type":"number","minimum":0,"maximum":100},"minStockLevel":{"default":0,"description":"Seuil d'alerte : à ce stock ou dessous, le produit passe en « bas ».","type":"integer","minimum":0,"maximum":1000000},"maxStockLevel":{"anyOf":[{"type":"integer","minimum":0,"maximum":1000000},{"type":"null"}]},"reorderPoint":{"anyOf":[{"type":"integer","minimum":0,"maximum":1000000},{"type":"null"}]},"reorderQuantity":{"anyOf":[{"type":"integer","minimum":0,"maximum":1000000},{"type":"null"}]},"capacityPerUnit":{"description":"Consommable : contenance d'une unité (ml, g, doses).","type":"number","exclusiveMinimum":0,"maximum":1000000},"supplierSku":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}]},"leadTimeDays":{"anyOf":[{"type":"integer","minimum":0,"maximum":365},{"type":"null"}]},"initialStock":{"default":0,"type":"integer","minimum":0,"maximum":1000000},"allowDuplicateSku":{"default":false,"type":"boolean"}},"required":["name","sku","costPrice","sellingPrice"]}}}}}},"/products/{id}":{"get":{"operationId":"get_product","summary":"Fiche d'un produit","description":"Renvoie un produit, son stock et ses dix derniers mouvements (le plus récent d'abord). L'historique complet s'obtient avec `list_stock_movements` et `productId`.","tags":["inventory"],"x-required-scope":"inventory:read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"update_product","summary":"Modifier un produit","description":"Met à jour la fiche d'un produit : prix, seuils de stock, référence, catégorie, archivage (`isActive: false`). Seuls les champs envoyés changent ; `null` efface un champ facultatif. Le stock lui-même ne se modifie pas ici : utilisez create_stock_movement, qui garde l'historique.","tags":["inventory"],"x-required-scope":"inventory:write","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"sku":{"type":"string","minLength":1,"maxLength":64},"barcode":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}]},"brand":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]},"description":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}]},"categoryId":{"anyOf":[{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},{"type":"null"}]},"costPrice":{"type":"number","minimum":0,"maximum":100000},"sellingPrice":{"type":"number","minimum":0,"maximum":100000},"taxRate":{"type":"number","minimum":0,"maximum":100},"minStockLevel":{"type":"integer","minimum":0,"maximum":1000000},"maxStockLevel":{"anyOf":[{"type":"integer","minimum":0,"maximum":1000000},{"type":"null"}]},"reorderPoint":{"anyOf":[{"type":"integer","minimum":0,"maximum":1000000},{"type":"null"}]},"reorderQuantity":{"anyOf":[{"type":"integer","minimum":0,"maximum":1000000},{"type":"null"}]},"supplierSku":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}]},"leadTimeDays":{"anyOf":[{"type":"integer","minimum":0,"maximum":365},{"type":"null"}]},"isActive":{"description":"false pour archiver le produit, true pour le réactiver.","type":"boolean"}}}}}}}},"/product-categories":{"get":{"operationId":"list_product_categories","summary":"Catégories de produits","description":"Liste les catégories de produits du salon, dans l'ordre d'affichage du QG. Utilisez leur `id` comme `categoryId` dans list_products, create_product et update_product.","tags":["inventory"],"x-required-scope":"inventory:read","parameters":[],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":[{"id":2,"name":"Coiffants","description":null,"parentId":null,"color":"#3B82F6","isActive":true}]}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/stock-movements":{"get":{"operationId":"list_stock_movements","summary":"Historique des mouvements de stock","description":"Liste les entrées et sorties de stock, de la plus récente à la plus ancienne. `type` : purchase (réception), sale (vente en caisse), service_use (consommé pendant une prestation), waste (perte, casse, périmé), adjustment (correction ou inventaire), transfer. `quantity` est signée : positive pour une entrée, négative pour une sortie. Période facultative en jours du salon (92 jours au plus si `from` et `to` sont donnés).","tags":["inventory"],"x-required-scope":"inventory:read","parameters":[{"name":"productId","in":"query","required":false,"schema":{"description":"Un seul produit (voir list_products).","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"description":"Un seul produit (voir list_products)."},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["purchase","sale","adjustment","waste","service_use","transfer"]}},{"name":"from","in":"query","required":false,"schema":{"description":"Premier jour inclus, YYYY-MM-DD, fuseau du salon.","type":"string"},"description":"Premier jour inclus, YYYY-MM-DD, fuseau du salon."},{"name":"to","in":"query","required":false,"schema":{"description":"Dernier jour inclus, YYYY-MM-DD, fuseau du salon.","type":"string"},"description":"Dernier jour inclus, YYYY-MM-DD, fuseau du salon."},{"name":"limit","in":"query","required":false,"schema":{"default":100,"type":"integer","minimum":1,"maximum":500}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":[{"id":912,"product":{"id":31,"name":"Cire mate 100 ml"},"type":"purchase","quantity":6,"stockBefore":2,"stockAfter":8,"unitCost":9.5,"totalCost":57,"reason":"Réception","reference":"BL-2026-118","recordedBy":"Karim B.","connectedApp":{"id":"app_x","name":"Mon tableur"},"createdAt":"2026-10-02T08:30:00.000Z"}],"meta":{"count":1,"limit":100,"offset":0,"hasMore":false}}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/products/{id}/stock-movements":{"post":{"operationId":"create_stock_movement","summary":"Enregistrer une entrée, une perte ou un inventaire","description":"Modifie le stock d'un produit en laissant une trace dans son historique. Quatre cas :\n- `type: \"purchase\"` + `quantity` > 0 : livraison reçue, le stock monte de `quantity`.\n- `type: \"waste\"` + `quantity` > 0 : perte, casse ou périmé, le stock baisse de `quantity`.\n- `type: \"adjustment\"` + `quantity` signée (≠ 0) : correction ponctuelle.\n- `type: \"adjustment\"` + `countedStock` : inventaire, le stock devient exactement `countedStock`.\nLes quantités sont des unités entières (flacons fermés pour un consommable). Une sortie qui ferait passer le stock sous zéro est refusée (422) sauf si le produit l'autorise. Pour un réutilisable (linge), une réception ou une perte change aussi le nombre possédé.\nIdempotence : avec une `reference` (n° de bon de livraison…), un second appel identique sur le même produit renvoie le mouvement déjà enregistré avec `replayed: true` au lieu de compter deux fois.\nUne vente se fait en caisse au QG, pas ici.","tags":["inventory"],"x-required-scope":"inventory:write","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"201":{"description":"Succès","content":{"application/json":{"schema":{"type":"object"},"example":{"data":{"movement":{"id":912,"type":"purchase","quantity":6,"stockBefore":2,"stockAfter":8,"reference":"BL-2026-118"},"product":{"id":31,"name":"Cire mate 100 ml","stock":{"current":8,"status":"ok"}}},"replayed":false}}}},"default":{"description":"Erreur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["purchase","waste","adjustment"]},"quantity":{"description":"Unités. > 0 pour purchase et waste ; signée pour adjustment.","type":"integer","minimum":-1000000,"maximum":1000000},"countedStock":{"description":"Inventaire : le stock compté. Seulement avec type adjustment, à la place de quantity.","type":"integer","minimum":0,"maximum":1000000},"reason":{"description":"Motif lisible au QG. Par défaut : Réception, Perte, Correction ou Inventaire.","type":"string","minLength":1,"maxLength":200},"reference":{"description":"N° de bon de livraison, de facture… Rend l'appel idempotent.","type":"string","minLength":1,"maxLength":100},"unitCost":{"description":"Prix d'achat unitaire HT de cette livraison (€).","type":"number","minimum":0,"maximum":100000}},"required":["type"]}}}}}}}}