# Pre-Go — API des applications connectées (v1)

> API d'un salon de barbier Pre-Go, pour les outils de son gérant : tableurs, Zapier/Make/n8n, scripts, assistants IA (MCP). Chaque jeton appartient à un seul salon et ne voit que lui. Chaque appel est tracé dans le journal d'audit du salon.

## Pour un assistant IA : l'essentiel

- Base : `https://pre-go.fr/api/v1`
- Authentification : en-tête `Authorization: Bearer pgo_…` (ou `X-Api-Key: pgo_…`). Jamais en query string.
- Commencez par `GET /api/v1/me` : il dit quel salon et quels droits (scopes) le jeton porte.
- Dates : `YYYY-MM-DD`. Heures : `HH:mm`, **dans le fuseau du salon** (`timezone` de `get_shop`, souvent `Europe/Paris`). Instants (`startsAt`, `createdAt`) : ISO 8601 UTC.
- Montants : euros TTC, nombres décimaux.
- Succès : `{ "data": … }`, plus `meta` (`count`, `limit`, `offset`, `hasMore`) pour les listes paginées.
- Erreur : `{ "error": { "code", "message", "details"? } }`. Lisez `message` : il dit quoi corriger.
- Avant de créer ou déplacer un rendez-vous : `get_availability` pour le créneau, `list_services` et `list_staff` pour les identifiants.
- Les écritures ont des effets réels : le client et le barbier peuvent recevoir un e-mail ou un SMS. Demandez confirmation à l'humain avant d'écrire.

## Obtenir un jeton

Dans le QG Pre-Go : **Paramètres → Applications connectées → Nouvelle application**. Choisissez les droits, copiez le jeton (affiché une seule fois). Seul le propriétaire du salon peut créer, régénérer ou révoquer un jeton. Un jeton cesse de fonctionner s'il est révoqué, s'il expire, ou si son créateur n'est plus propriétaire du salon.

## Utiliser Pre-Go depuis un assistant IA (MCP)

Serveur MCP en HTTP (« Streamable HTTP », sans état) : `https://pre-go.fr/api/v1/mcp`, même jeton en en-tête `Authorization`. Les outils MCP portent exactement les noms des opérations ci-dessous, filtrés par les droits du jeton.

Claude Code :

```bash
claude mcp add --transport http prego https://pre-go.fr/api/v1/mcp --header "Authorization: Bearer pgo_…"
```

Clients qui ne parlent que stdio (Claude Desktop, etc.) :

```json
{ "mcpServers": { "prego": { "command": "npx", "args": ["-y", "mcp-remote", "https://pre-go.fr/api/v1/mcp", "--header", "Authorization: Bearer pgo_…"] } } }
```

## Droits (scopes)

| Scope | Nom au QG | Donne accès à |
|---|---|---|
| `shop:read` | Salon | Nom, adresse, fuseau horaire, horaires d'ouverture. |
| `services:read` | Prestations | Catalogue : prix, durées, catégories. |
| `staff:read` | Équipe | Barbiers du salon : nom, titre, réservable ou non. |
| `availability:read` | Disponibilités | Créneaux libres d'une journée, par prestation et barbier. |
| `appointments:read` | Rendez-vous — lecture | Le planning, avec le nom et les coordonnées des clients. |
| `appointments:write` | Rendez-vous — écriture | Créer, déplacer, confirmer, annuler un rendez-vous. Mêmes notifications que depuis le QG. |
| `customers:read` | Clients — lecture | Fichier client : coordonnées, visites, chiffre d'affaires. |
| `customers:write` | Clients — écriture | Créer une fiche client, modifier ses coordonnées. |
| `sales:read` | Encaissements | Tickets de caisse et chiffre d'affaires sur une période. |
| `sms:read` | SMS | Consommation SMS : volumes par barbier, par type et par jour, pour refacturer. Ni numéro ni texte des messages. |
| `inventory:read` | Stocks — lecture | Produits, quantités en stock, seuils d'alerte, prix d'achat et historique des mouvements. |
| `inventory:write` | Stocks — écriture | Enregistrer une livraison, une perte ou un inventaire ; créer un produit, modifier ses prix et ses seuils. |

## Erreurs

| Code | HTTP | Signification |
|---|---|---|
| `unauthorized` | 401 | Jeton absent, inconnu, expiré ou révoqué. |
| `forbidden_scope` | 403 | Le jeton n'a pas le droit (scope) requis par cette opération. |
| `not_found` | 404 | Ressource introuvable dans ce salon. |
| `method_not_allowed` | 405 | Méthode HTTP non prise en charge sur ce chemin. L'en-tête `Allow` liste les bonnes. |
| `invalid_request` | 400 | Paramètres invalides. `details` liste les champs en cause. |
| `conflict` | 409 | Le créneau n'est plus libre, ou l'état a changé entre-temps. |
| `unprocessable` | 422 | Requête valide, mais refusée par une règle métier (date passée, statut, doublon…). |
| `rate_limited` | 429 | Trop d'appels. Attendre `Retry-After` secondes. |
| `internal_error` | 500 | Erreur côté Pre-Go. Réessayer plus tard. |

Limite : 120 appels par minute et par adresse IP ; au-delà, 429 et en-tête `Retry-After`.

## Traçabilité

Chaque appel — lecture comprise — est inscrit au journal d'audit du salon (QG → Audit), au nom de l'application, avec l'opération, le statut et l'adresse IP. Les corps de réponse des lectures n'y sont pas copiés.

## Opérations

- `GET /me` — **whoami** — Qui suis-je ?
- `GET /shop` — **get_shop** — Informations du salon (`shop:read`)
- `GET /services` — **list_services** — Catalogue des prestations (`services:read`)
- `GET /staff` — **list_staff** — Équipe du salon (`staff:read`)
- `GET /availability` — **get_availability** — Créneaux libres d'une journée (`availability:read`)
- `GET /appointments` — **list_appointments** — Rendez-vous sur une période (`appointments:read`)
- `GET /appointments/{id}` — **get_appointment** — Détail d'un rendez-vous (`appointments:read`)
- `POST /appointments` — **create_appointment** — Créer un rendez-vous (`appointments:write`)
- `PATCH /appointments/{id}` — **update_appointment** — Modifier un rendez-vous (`appointments:write`)
- `POST /appointments/{id}/cancel` — **cancel_appointment** — Annuler un rendez-vous (`appointments:write`)
- `GET /customers` — **list_customers** — Rechercher dans le fichier client (`customers:read`)
- `GET /customers/{id}` — **get_customer** — Fiche d'un client (`customers:read`)
- `POST /customers` — **create_customer** — Créer une fiche client (`customers:write`)
- `PATCH /customers/{id}` — **update_customer** — Modifier une fiche client (`customers:write`)
- `GET /sales` — **list_sales** — Encaissements sur une période (`sales:read`)
- `GET /sms/usage` — **get_sms_usage** — Consommation SMS sur une période (`sms:read`)
- `GET /sms/messages` — **list_sms_messages** — SMS envoyés, ligne à ligne (`sms:read`)
- `GET /products` — **list_products** — Produits et niveaux de stock (`inventory:read`)
- `GET /products/{id}` — **get_product** — Fiche d'un produit (`inventory:read`)
- `GET /product-categories` — **list_product_categories** — Catégories de produits (`inventory:read`)
- `GET /stock-movements` — **list_stock_movements** — Historique des mouvements de stock (`inventory:read`)
- `POST /products/{id}/stock-movements` — **create_stock_movement** — Enregistrer une entrée, une perte ou un inventaire (`inventory:write`)
- `POST /products` — **create_product** — Créer un produit (`inventory:write`)
- `PATCH /products/{id}` — **update_product** — Modifier un produit (`inventory:write`)

### whoami — Qui suis-je ?

`GET /api/v1/me` · droit : aucun · lecture

Renvoie l'application connectée, son salon et ses droits (scopes). Aucun droit requis : à appeler en premier pour savoir ce que le jeton permet.

_Aucun paramètre._

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/me"
```

Réponse :

```json
{
  "data": {
    "app": {
      "id": "app_x",
      "name": "Mon tableur"
    },
    "shop": {
      "id": 4,
      "slug": "le-barbier",
      "timezone": "Europe/Paris"
    },
    "scopes": [
      "shop:read",
      "appointments:read"
    ]
  }
}
```

---

### get_shop — Informations du salon

`GET /api/v1/shop` · droit : `shop:read` · lecture

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.

_Aucun paramètre._

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/shop"
```

Réponse :

```json
{
  "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"
  }
}
```

---

### list_services — Catalogue des prestations

`GET /api/v1/services` · droit : `services:read` · lecture

Liste les prestations du salon avec prix (en euros TTC), durée (en minutes) et catégorie. Par défaut, seules les prestations actives.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `includeInactive` | query | boolean,string | non | true pour inclure les prestations archivées. |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/services"
```

Réponse :

```json
{
  "data": [
    {
      "id": 12,
      "name": "Coupe homme",
      "category": "Haircut",
      "price": 25,
      "discountPrice": null,
      "durationMinutes": 30,
      "bookableOnline": true,
      "isActive": true
    }
  ]
}
```

---

### list_staff — Équipe du salon

`GET /api/v1/staff` · droit : `staff:read` · lecture

Liste les barbiers (et membres du back-office) du salon. `acceptsBookings` indique qui peut recevoir des rendez-vous ; utilisez leur `id` comme `staffId` ailleurs.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `includeInactive` | query | boolean,string | non | true pour inclure les membres désactivés. |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/staff"
```

Réponse :

```json
{
  "data": [
    {
      "id": "mbr_x1",
      "firstName": "Karim",
      "lastName": "B.",
      "title": "Barber",
      "acceptsBookings": true,
      "isActive": true
    }
  ]
}
```

---

### get_availability — Créneaux libres d'une journée

`GET /api/v1/availability` · droit : `availability:read` · lecture

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`.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `date` | query | string | oui | Jour voulu, YYYY-MM-DD, dans le fuseau du salon. |
| `serviceId` | query | integer | oui | Prestation (voir list_services). |
| `staffId` | query | string | non | Barbier précis (voir list_staff). Absent : tous les barbiers. |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/availability"
```

Réponse :

```json
{
  "data": {
    "date": "2026-10-02",
    "slots": [
      {
        "startTime": "10:00",
        "endTime": "10:30",
        "staffId": "mbr_x1"
      }
    ]
  }
}
```

---

### list_appointments — Rendez-vous sur une période

`GET /api/v1/appointments` · droit : `appointments:read` · lecture

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `from` | query | string | oui | Premier jour, YYYY-MM-DD. |
| `to` | query | string | oui | Dernier jour inclus, YYYY-MM-DD. |
| `staffId` | query | string | non |  |
| `customerId` | query | string | non |  |
| `status` | query | string | non | Un ou plusieurs statuts séparés par des virgules : pending, scheduled, confirmed, in_progress, completed, cancelled, no_show. |
| `limit` | query | integer | non |  (défaut : `200`) |
| `offset` | query | integer | non |  (défaut : `0`) |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/appointments"
```

Réponse :

```json
{
  "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
  }
}
```

---

### get_appointment — Détail d'un rendez-vous

`GET /api/v1/appointments/{id}` · droit : `appointments:read` · lecture

Renvoie un rendez-vous, avec toutes ses lignes de prestation.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `id` | chemin | string | oui | Identifiant du rendez-vous. |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/appointments/<id>"
```

---

### create_appointment — Créer un rendez-vous

`POST /api/v1/appointments` · droit : `appointments:write` · **écriture**

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`.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `date` | corps JSON | string | oui | Jour du rendez-vous, YYYY-MM-DD. |
| `startTime` | corps JSON | string | oui | Heure de début, HH:mm, fuseau du salon. |
| `serviceId` | corps JSON | integer | oui |  |
| `staffId` | corps JSON | string | non | Barbier voulu. Absent : n'importe quel barbier disponible. |
| `customerId` | corps JSON | string | non | Client existant (voir list_customers). Sinon, renseignez `customer`. |
| `customer` | corps JSON | objet { name, email, phone } | non | 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é. |
| `customerNotes` | corps JSON | string | non | Demande du client, visible par le barbier. |
| `notifyCustomer` | corps JSON | boolean | non | false pour ne rien envoyer au client ni au barbier. (défaut : `true`) |

```bash
curl -X POST -H "Authorization: Bearer $PREGO_TOKEN" -H "Content-Type: application/json" \
  -d '{…}' "https://pre-go.fr/api/v1/appointments"
```

Réponse :

```json
{
  "data": {
    "bookingGroupId": "bkg_1",
    "appointments": [
      {
        "id": "apt_9f2",
        "date": "2026-10-02",
        "startTime": "10:00",
        "status": "confirmed"
      }
    ]
  }
}
```

---

### update_appointment — Modifier un rendez-vous

`PATCH /api/v1/appointments/{id}` · droit : `appointments:write` · **écriture**

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`.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `id` | chemin | string | oui |  |
| `date` | corps JSON | string | non | Nouveau jour, YYYY-MM-DD. |
| `startTime` | corps JSON | string | non | Nouvelle heure de début, HH:mm. |
| `staffId` | corps JSON | string | non | Nouveau barbier. |
| `durationMinutes` | corps JSON | integer | non |  |
| `status` | corps JSON | `scheduled` \| `confirmed` \| `no_show` \| `cancelled` | non |  |
| `notes` | corps JSON | string \| null | non | Note interne du salon. |

```bash
curl -X PATCH -H "Authorization: Bearer $PREGO_TOKEN" -H "Content-Type: application/json" \
  -d '{…}' "https://pre-go.fr/api/v1/appointments/<id>"
```

---

### cancel_appointment — Annuler un rendez-vous

`POST /api/v1/appointments/{id}/cancel` · droit : `appointments:write` · **écriture**

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `id` | chemin | string | oui |  |
| `reason` | corps JSON | string | non | Motif, visible dans la fiche du rendez-vous. |

```bash
curl -X POST -H "Authorization: Bearer $PREGO_TOKEN" -H "Content-Type: application/json" \
  -d '{…}' "https://pre-go.fr/api/v1/appointments/<id>/cancel"
```

---

### list_customers — Rechercher dans le fichier client

`GET /api/v1/customers` · droit : `customers:read` · lecture

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `search` | query | string | non |  |
| `limit` | query | integer | non |  (défaut : `50`) |
| `offset` | query | integer | non |  (défaut : `0`) |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/customers"
```

Réponse :

```json
{
  "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
  }
}
```

---

### get_customer — Fiche d'un client

`GET /api/v1/customers/{id}` · droit : `customers:read` · lecture

Renvoie la fiche d'un client : coordonnées, consentements, statistiques de visite. Ses rendez-vous s'obtiennent avec `list_appointments` et `customerId`.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `id` | chemin | string | oui |  |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/customers/<id>"
```

---

### create_customer — Créer une fiche client

`POST /api/v1/customers` · droit : `customers:write` · **écriture**

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `firstName` | corps JSON | string | oui |  |
| `lastName` | corps JSON | string | non |  (défaut : `""`) |
| `email` | corps JSON | string \| null | non |  |
| `phone` | corps JSON | string \| null | non |  |
| `emailOptIn` | corps JSON | boolean | non |  |
| `smsOptIn` | corps JSON | boolean | non |  |
| `allowDuplicate` | corps JSON | boolean | non |  (défaut : `false`) |

```bash
curl -X POST -H "Authorization: Bearer $PREGO_TOKEN" -H "Content-Type: application/json" \
  -d '{…}' "https://pre-go.fr/api/v1/customers"
```

---

### update_customer — Modifier une fiche client

`PATCH /api/v1/customers/{id}` · droit : `customers:write` · **écriture**

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `id` | chemin | string | oui |  |
| `firstName` | corps JSON | string | non |  |
| `lastName` | corps JSON | string | non |  |
| `email` | corps JSON | string \| null | non |  |
| `phone` | corps JSON | string \| null | non |  |
| `emailOptIn` | corps JSON | boolean | non |  |
| `smsOptIn` | corps JSON | boolean | non |  |

```bash
curl -X PATCH -H "Authorization: Bearer $PREGO_TOKEN" -H "Content-Type: application/json" \
  -d '{…}' "https://pre-go.fr/api/v1/customers/<id>"
```

---

### list_sales — Encaissements sur une période

`GET /api/v1/sales` · droit : `sales:read` · lecture

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `from` | query | string | oui |  |
| `to` | query | string | oui |  |
| `staffId` | query | string | non |  |
| `limit` | query | integer | non |  (défaut : `200`) |
| `offset` | query | integer | non |  (défaut : `0`) |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/sales"
```

Réponse :

```json
{
  "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
  }
}
```

---

### get_sms_usage — Consommation SMS sur une période

`GET /api/v1/sms/usage` · droit : `sms:read` · lecture

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `from` | query | string | non | Premier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `to` : mois en cours. |
| `to` | query | string | non | Dernier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `from` : mois en cours. |
| `staffId` | query | string | non | Barbier (voir list_staff) : seuls les SMS de ses rendez-vous. Un identifiant d'un autre salon ne renvoie rien. |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/sms/usage"
```

Réponse :

```json
{
  "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
  }
}
```

---

### list_sms_messages — SMS envoyés, ligne à ligne

`GET /api/v1/sms/messages` · droit : `sms:read` · lecture

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `from` | query | string | non | Premier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `to` : mois en cours. |
| `to` | query | string | non | Dernier jour inclus (YYYY-MM-DD, jour du salon). Absent avec `from` : mois en cours. |
| `staffId` | query | string | non | Barbier (voir list_staff) : seuls les SMS de ses rendez-vous. Un identifiant d'un autre salon ne renvoie rien. |
| `status` | query | `billed` \| `unbilled` \| `all` | non | `billed` (défaut) : SMS facturés ; `unbilled` : tentatives non facturées ; `all` : tout. (défaut : `"billed"`) |
| `limit` | query | integer | non |  (défaut : `200`) |
| `offset` | query | integer | non |  (défaut : `0`) |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/sms/messages"
```

Réponse :

```json
{
  "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"
  }
}
```

---

### list_products — Produits et niveaux de stock

`GET /api/v1/products` · droit : `inventory:read` · lecture

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).

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `search` | query | string | non | Cherche dans le nom, la référence (SKU), le code-barres et la marque. |
| `categoryId` | query | integer | non | Catégorie (voir list_product_categories). |
| `kind` | query | `consumable` \| `reusable` \| `retail` | non | retail : vente au comptoir ; consumable : produit entamé pendant les prestations ; reusable : linge, serviettes. |
| `stock` | query | `low` \| `out` | non | low : au seuil minimum ou dessous (épuisés compris) ; out : épuisés seulement. |
| `includeInactive` | query | boolean,string | non | true pour inclure les produits archivés. |
| `limit` | query | integer | non |  (défaut : `100`) |
| `offset` | query | integer | non |  (défaut : `0`) |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/products"
```

Réponse :

```json
{
  "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
  }
}
```

---

### get_product — Fiche d'un produit

`GET /api/v1/products/{id}` · droit : `inventory:read` · lecture

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`.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `id` | chemin | integer | oui |  |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/products/<id>"
```

---

### list_product_categories — Catégories de produits

`GET /api/v1/product-categories` · droit : `inventory:read` · lecture

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.

_Aucun paramètre._

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/product-categories"
```

Réponse :

```json
{
  "data": [
    {
      "id": 2,
      "name": "Coiffants",
      "description": null,
      "parentId": null,
      "color": "#3B82F6",
      "isActive": true
    }
  ]
}
```

---

### list_stock_movements — Historique des mouvements de stock

`GET /api/v1/stock-movements` · droit : `inventory:read` · lecture

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).

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `productId` | query | integer | non | Un seul produit (voir list_products). |
| `type` | query | `purchase` \| `sale` \| `adjustment` \| `waste` \| `service_use` \| `transfer` | non |  |
| `from` | query | string | non | Premier jour inclus, YYYY-MM-DD, fuseau du salon. |
| `to` | query | string | non | Dernier jour inclus, YYYY-MM-DD, fuseau du salon. |
| `limit` | query | integer | non |  (défaut : `100`) |
| `offset` | query | integer | non |  (défaut : `0`) |

```bash
curl -H "Authorization: Bearer $PREGO_TOKEN" "https://pre-go.fr/api/v1/stock-movements"
```

Réponse :

```json
{
  "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
  }
}
```

---

### create_stock_movement — Enregistrer une entrée, une perte ou un inventaire

`POST /api/v1/products/{id}/stock-movements` · droit : `inventory:write` · **écriture**

Modifie le stock d'un produit en laissant une trace dans son historique. Quatre cas :
- `type: "purchase"` + `quantity` > 0 : livraison reçue, le stock monte de `quantity`.
- `type: "waste"` + `quantity` > 0 : perte, casse ou périmé, le stock baisse de `quantity`.
- `type: "adjustment"` + `quantity` signée (≠ 0) : correction ponctuelle.
- `type: "adjustment"` + `countedStock` : inventaire, le stock devient exactement `countedStock`.
Les 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é.
Idempotence : 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.
Une vente se fait en caisse au QG, pas ici.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `id` | chemin | integer | oui |  |
| `type` | corps JSON | `purchase` \| `waste` \| `adjustment` | oui |  |
| `quantity` | corps JSON | integer | non | Unités. > 0 pour purchase et waste ; signée pour adjustment. |
| `countedStock` | corps JSON | integer | non | Inventaire : le stock compté. Seulement avec type adjustment, à la place de quantity. |
| `reason` | corps JSON | string | non | Motif lisible au QG. Par défaut : Réception, Perte, Correction ou Inventaire. |
| `reference` | corps JSON | string | non | N° de bon de livraison, de facture… Rend l'appel idempotent. |
| `unitCost` | corps JSON | number | non | Prix d'achat unitaire HT de cette livraison (€). |

```bash
curl -X POST -H "Authorization: Bearer $PREGO_TOKEN" -H "Content-Type: application/json" \
  -d '{…}' "https://pre-go.fr/api/v1/products/<id>/stock-movements"
```

Réponse :

```json
{
  "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
}
```

---

### create_product — Créer un produit

`POST /api/v1/products` · droit : `inventory:write` · **écriture**

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `name` | corps JSON | string | oui |  |
| `sku` | corps JSON | string | oui |  |
| `costPrice` | corps JSON | number | oui |  |
| `sellingPrice` | corps JSON | number | oui |  |
| `kind` | corps JSON | `consumable` \| `reusable` \| `retail` | non | retail : vente au comptoir ; consumable : entamé pendant les prestations ; reusable : linge. (défaut : `"retail"`) |
| `barcode` | corps JSON | string \| null | non |  |
| `brand` | corps JSON | string \| null | non |  |
| `description` | corps JSON | string \| null | non |  |
| `categoryId` | corps JSON | integer \| null | non |  |
| `taxRate` | corps JSON | number | non | TVA en %, 20 par défaut. (défaut : `20`) |
| `minStockLevel` | corps JSON | integer | non | Seuil d'alerte : à ce stock ou dessous, le produit passe en « bas ». (défaut : `0`) |
| `maxStockLevel` | corps JSON | integer \| null | non |  |
| `reorderPoint` | corps JSON | integer \| null | non |  |
| `reorderQuantity` | corps JSON | integer \| null | non |  |
| `capacityPerUnit` | corps JSON | number | non | Consommable : contenance d'une unité (ml, g, doses). |
| `supplierSku` | corps JSON | string \| null | non |  |
| `leadTimeDays` | corps JSON | integer \| null | non |  |
| `initialStock` | corps JSON | integer | non |  (défaut : `0`) |
| `allowDuplicateSku` | corps JSON | boolean | non |  (défaut : `false`) |

```bash
curl -X POST -H "Authorization: Bearer $PREGO_TOKEN" -H "Content-Type: application/json" \
  -d '{…}' "https://pre-go.fr/api/v1/products"
```

---

### update_product — Modifier un produit

`PATCH /api/v1/products/{id}` · droit : `inventory:write` · **écriture**

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.

| Paramètre | Où | Type | Requis | Description |
|---|---|---|---|---|
| `id` | chemin | integer | oui |  |
| `name` | corps JSON | string | non |  |
| `sku` | corps JSON | string | non |  |
| `barcode` | corps JSON | string \| null | non |  |
| `brand` | corps JSON | string \| null | non |  |
| `description` | corps JSON | string \| null | non |  |
| `categoryId` | corps JSON | integer \| null | non |  |
| `costPrice` | corps JSON | number | non |  |
| `sellingPrice` | corps JSON | number | non |  |
| `taxRate` | corps JSON | number | non |  |
| `minStockLevel` | corps JSON | integer | non |  |
| `maxStockLevel` | corps JSON | integer \| null | non |  |
| `reorderPoint` | corps JSON | integer \| null | non |  |
| `reorderQuantity` | corps JSON | integer \| null | non |  |
| `supplierSku` | corps JSON | string \| null | non |  |
| `leadTimeDays` | corps JSON | integer \| null | non |  |
| `isActive` | corps JSON | boolean | non | false pour archiver le produit, true pour le réactiver. |

```bash
curl -X PATCH -H "Authorization: Bearer $PREGO_TOKEN" -H "Content-Type: application/json" \
  -d '{…}' "https://pre-go.fr/api/v1/products/<id>"
```

## Autres formats

- Page lisible par un humain : `https://pre-go.fr/api-mcp`
- OpenAPI 3.1 : `https://pre-go.fr/api/v1/openapi.json`
- Cette page : `https://pre-go.fr/api/v1/docs.md`
