{
  "openapi": "3.0.3",
  "info": {
    "title": "API CRM Mailpro™ v3",
    "version": "3",
    "description": "L'API CRM Mailpro™ v3 vous permet de gérer par programmation les contacts, les listes, les tags,\nles champs personnalisés, les modèles d'e-mail et les campagnes — et d'envoyer aussi bien des\ne-mails marketing en masse que des e-mails transactionnels individuels, ainsi que des SMS.\n\n## Authentification\n\nTous les points de terminaison (sauf `POST /token` et les points de terminaison OAuth) acceptent **trois\nméthodes d'authentification**. Choisissez celle qui correspond à votre stack.\n\n- **OAuth 2.0** (authorization_code + PKCE, ou client_credentials) — le\n  choix moderne pour les applications tierces et les plateformes d'intégration\n  (Zapier, Make, n8n) qui agissent pour le compte d'un utilisateur Mailpro. Émet des\n  jetons d'accès cantonnés à des scopes et révocables. **La seule méthode qui porte des scopes\n  granulaires et que vous pouvez confier en toute sécurité à un tiers.**\n- **Bearer JWT** (héritée) — un jeton HMAC-HS256 issu de `POST /token`. Ressemble\n  à OAuth, mais lié à un unique identifiant de compte et non cantonné à des scopes.\n- **IdClient + ApiKey** (héritée) — votre identifiant client + clé API dans les en-têtes HTTP.\n\n> Les méthodes héritées Bearer JWT et IdClient+ApiKey ne sont **pas filtrées par scope** — elles conservent\n> un accès complet à chaque point de terminaison qu'elles pouvaient déjà atteindre. Seuls les jetons d'accès\n> OAuth 2.0 sont soumis aux vérifications de scope par point de terminaison indiquées sur chaque\n> opération ci-dessous (un jeton dépourvu du scope reçoit **403 `insufficient_scope`**).\n\n### Méthode 1 — OAuth 2.0 (recommandée pour les intégrations)\n\nMailpro exploite **un seul** serveur d'autorisation OAuth 2.0 pour toutes ses API,\nservi sur une surface unique indépendante de la version : **`https://api.mailpro.com/oauth/`**.\nUn seul serveur, un seul jeton — le **même** jeton d'accès fonctionne sur CRM v3,\nEmail v2 et SMS. Les points de terminaison qu'un jeton peut appeler sont régis par les\n**scopes** du jeton, et non par l'URL de base de l'API que vous appelez. Il n'existe pas\nde point de terminaison OAuth par version (pas de `/v3/oauth`, `/v2/oauth` ni `/sms/oauth`).\n\n| Point de terminaison | URL |\n|----------|-----|\n| Authorize | `GET https://api.mailpro.com/oauth/authorize` |\n| Token / Refresh | `POST https://api.mailpro.com/oauth/token` |\n| JWKS (vérifier les signatures) | `GET https://api.mailpro.com/oauth/.well-known/jwks` |\n| Discovery | `GET https://api.mailpro.com/oauth/.well-known/openid-configuration` |\n\nÉmetteur (le claim `iss` sur chaque jeton) : `https://api.mailpro.com/oauth/`.\n\n**Grants pris en charge**\n\n- `authorization_code` **+ PKCE** (`code_challenge_method=S256` est\n  **obligatoire**) — le flux de consentement navigateur « Connect Mailpro ». Retourne un\n  jeton d'accès **et** un refresh token rotatif.\n- `client_credentials` — machine à machine, sans utilisateur. Retourne uniquement un\n  jeton d'accès.\n- `refresh_token` — échangez un refresh token contre une nouvelle paire access/refresh.\n  **Les refresh tokens sont rotatifs** : chaque utilisation invalide l'ancien.\n\n**Durées de vie des jetons**\n\n| Jeton | TTL |\n|-------|-----|\n| Jeton d'accès (JWT RS256) | **20 min** |\n| Refresh token (glissant, à usage unique) | **30 jours** |\n| Code d'autorisation | **60 secondes** |\n\n**Authorization Code + PKCE — étape par étape**\n\n1. Générez `code_verifier` (43 à 128 caractères aléatoires) et\n   `code_challenge = base64url(sha256(code_verifier))`.\n2. Redirigez l'utilisateur vers :\n   ```\n   GET https://api.mailpro.com/oauth/authorize\n     ?response_type=code\n     &client_id=YOUR_CLIENT_ID\n     &redirect_uri=https://yourapp.example.com/callback\n     &scope=contacts:read contacts:write\n     &state=RANDOM_CSRF_TOKEN\n     &code_challenge=CODE_CHALLENGE\n     &code_challenge_method=S256\n   ```\n3. L'utilisateur approuve sur l'écran de consentement Mailpro ; Mailpro le redirige\n   avec `?code=…&state=…`. Vérifiez `state` (protection CSRF).\n4. Échangez le code contre des jetons :\n   ```\n   POST https://api.mailpro.com/oauth/token\n   Content-Type: application/x-www-form-urlencoded\n\n   grant_type=authorization_code&code=AUTH_CODE\n   &redirect_uri=https://yourapp.example.com/callback\n   &client_id=YOUR_CLIENT_ID&code_verifier=CODE_VERIFIER\n   ```\n   ```json\n   { \"access_token\": \"eyJ…\", \"token_type\": \"Bearer\",\n     \"expires_in\": 1200, \"refresh_token\": \"…\" }\n   ```\n5. Appelez n'importe quelle API Mailpro avec `Authorization: Bearer ACCESS_TOKEN`.\n6. À son expiration, rafraîchissez avec `grant_type=refresh_token` et stockez le\n   **nouveau** refresh token (l'ancien est désormais inutilisable).\n\n**client_credentials**\n\n```\nPOST https://api.mailpro.com/oauth/token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=client_credentials&client_id=YOUR_CLIENT_ID\n&client_secret=YOUR_CLIENT_SECRET&scope=contacts:read email:send\n```\n\nLe même jeton d'accès OAuth est aussi accepté par les API Email v2\n(`api.mailpro.com/v2`) et SMS (`api.mailpro.com/sms`) — authentifiez-vous\nune seule fois, appelez n'importe quelle API. Catalogue complet des scopes : voir **Scopes OAuth 2.0** ci-dessous.\n\n### Tester OAuth 2.0 en quelques minutes — démarrage rapide\n\nLe moyen le plus rapide d'essayer OAuth est le grant **client_credentials**\n(serveur à serveur, sans navigateur, sans utilisateur). Vous avez besoin d'une paire `client_id` /\n`client_secret` (demandez-la à votre contact Mailpro, ou créez-en une dans **Paramètres →\nAccès API**). Remplacez les valeurs d'exemple ci-dessous — **ne validez jamais un véritable\nsecret dans le code source**.\n\n**1. Obtenir un jeton d'accès**\n\n```bash\ncurl -X POST https://api.mailpro.com/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=client_credentials\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\" \\\n  -d \"scope=contacts:read\"\n```\n\n```json\n{ \"access_token\": \"eyJhbGciOiJSUzI1Ni…\", \"token_type\": \"Bearer\",\n  \"expires_in\": 1200 }\n```\n\n**2. Appeler un point de terminaison de ressource avec le jeton**\n\n```bash\ncurl https://api.mailpro.com/v3/List \\\n  -H \"Authorization: Bearer eyJhbGciOiJSUzI1Ni…\"\n```\n\nLe **même** jeton fonctionne aussi sur Email v2 (`…/v2/…`) et SMS (`…/sms/…`) —\ntant qu'il porte le scope requis par chaque point de terminaison. Un jeton dépourvu du\nscope requis est rejeté avec **403 `insufficient_scope`**.\n\n**« Connect Mailpro » interactif (authorization_code + PKCE)** — lorsque vous agissez\npour le compte d'un *utilisateur* (Zapier/Make/n8n, applications partenaires) : redirigez l'utilisateur vers\n`GET https://api.mailpro.com/oauth/authorize` avec `response_type=code`,\n`client_id`, `redirect_uri`, `scope`, `state`, `code_challenge` et\n`code_challenge_method=S256` ; l'utilisateur consent ; vous échangez le `code` retourné\nsur `POST https://api.mailpro.com/oauth/token` avec le `code_verifier` correspondant.\nDétail complet ci-dessus ; scopes dans **Scopes OAuth 2.0** ci-dessous.\n\n> Les jetons sont des **JWT RS256** — les jetons d'accès vivent **20 min**, les refresh\n> tokens **30 jours** (glissants, à usage unique : chaque rafraîchissement fait tourner le jeton).\n> Vérifiez les signatures contre le JWKS situé à\n> `https://api.mailpro.com/oauth/.well-known/jwks` et validez le scope de\n> chaque requête contre la matrice de scopes ci-dessous.\n\n### Méthode 2 — Bearer JWT (héritée)\n\n```\nAuthorization: Bearer <access_token>\n```\n\nObtenez un jeton via `POST /token` (voir ci-dessous). Valide **30 jours** par défaut ;\nutilisez `grant_type=refresh_token` pour le renouveler sans vous réauthentifier. Plus simple\nà mettre en place, mais lié à un unique identifiant de compte et **impossible** à cantonner\nni à confier à un tiers — privilégiez OAuth 2.0 pour toute intégration installable.\n\n### Méthode 3 — IdClient + ApiKey (plus simple)\n\nEnvoyez votre identifiant client et votre clé API en tant qu'**en-têtes HTTP** (recommandé) ou en tant que\n**paramètres de requête** :\n\n```\nIdClient: 12345\nApiKey:   a1b2c3d4-e5f6-7890-abcd-ef1234567890\n```\n\nMêmes identifiants que les API v2 / SMS — copiez-collez depuis\n**Paramètres → Accès API**. Aucune étape d'échange de jeton. La forme en en-tête est recommandée\ncar les URL sont journalisées par IIS / Cloudflare / les proxys.\n\n### Point de terminaison de jeton (grant password)\n```\nPOST /token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=password&username=YOUR_USERNAME&password=YOUR_API_KEY\n```\n- `username` — votre nom d'utilisateur Mailpro (e-mail)\n- `password` — votre clé API (GUID), disponible dans **Paramètres → Accès API**\n\n### Point de terminaison de jeton (grant client_credentials)\n```\nPOST /token\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=client_credentials&client_id=YOUR_CLIENT_ID\n```\n\n> Le jeton émis par `POST /v3/token` est également valide sur l'API Email v2\n> (`api.mailpro.com/v2`) et sur l'API SMS (`api.mailpro.com/sms`). Authentifiez-vous\n> une seule fois, appelez n'importe quelle API.\n\n## Scopes OAuth 2.0\n\nLorsque votre application demande un jeton d'accès OAuth, elle déclare un ensemble de **scopes** —\ndes permissions qui définissent exactement ce que le jeton peut faire. L'utilisateur voit une\ndescription en langage clair de chaque scope demandé sur l'écran de consentement.\nLes scopes sont séparés par des espaces dans le paramètre `scope`\n(`scope=contacts:read email:send`). Ne demandez que ce dont vous avez besoin.\n\nLes scopes suivent le format `<resource>:<action>` et sont **hiérarchiquement indépendants** —\n`contacts:write` n'implique **pas** `contacts:read` ; demandez les deux si nécessaire.\n\n| Scope | Autorise |\n|-------|--------|\n| `contacts:read` | Consulter vos contacts, listes, tags, segments et champs personnalisés. |\n| `contacts:write` | Créer, modifier et supprimer vos contacts, listes, tags et champs personnalisés. |\n| `email:send` | Envoyer des e-mails transactionnels et de campagne en votre nom. |\n| `email:read` | Consulter les statistiques de délivrabilité, ouvertures, clics et historique des rebonds. |\n| `email:templates:read` | Consulter vos modèles d'e-mail (objet, HTML, corps en texte brut). |\n| `email:templates:write` | Créer, modifier et supprimer vos modèles d'e-mail. |\n| `sms:send` | Envoyer des messages SMS transactionnels et de campagne en votre nom. |\n| `sms:read` | Consulter le solde de crédits SMS et l'historique des messages. |\n| `sms:contacts:read` | Consulter votre répertoire SMS. |\n| `sms:contacts:write` | Créer, modifier et supprimer des entrées dans votre répertoire SMS. |\n| `webhooks:manage` | S'abonner à, modifier et supprimer les points de terminaison webhook de votre compte. |\n| `account:read` | Consulter les crédits, les limites et le statut d'abonnement de votre compte. |\n\n### Application des scopes par point de terminaison\n\nLes vérifications de scope sont **actives** et appliquées par point de terminaison pour les jetons émis par OAuth\n(l'authentification héritée en est exemptée). Chaque opération de cette référence indique son scope requis\nsous **Authorization**. Récapitulatif pour la surface CRM v3 :\n\n| Points de terminaison | Scope requis |\n|-----------|----------------|\n| `GET /Contact/{id}`, `GET /List`, `GET /List/{id}`, `GET /List/{id}/Contact`, `GET /List/{listId}/Unsubscribers`, `GET /Tag`, `GET /Tag/{id}`, `GET /Tag/{id}/Contact`, `GET /Segment`, vérification d'appartenance à un segment | `contacts:read` |\n| `POST /Contact`, `POST /Contact/Multiple`, `PUT`/`DELETE /Contact/{id}`, ajout/retrait d'un tag ou d'une liste sur un contact, `POST`/`PUT`/`DELETE /List`, `POST`/`PUT`/`DELETE /Tag` | `contacts:write` |\n\n> Les points de terminaison de cette spécification qui ne sont **pas** listés ci-dessus (par exemple `GET /Field`,\n> les campagnes, l'envoi transactionnel, l'envoi SMS, les modèles de message, les informations de compte)\n> sont accessibles avec tout jeton valide aujourd'hui — ils ne sont pas encore filtrés par scope sur\n> la surface CRM v3. Les API Email v2 et SMS filtrent en plus leurs propres points de terminaison\n> d'envoi / modèles / contacts / compte (voir ces spécifications).\n\nUn rejet de scope retourne **403** avec\n`{ \"error\": \"insufficient_scope\", \"required_scope\": \"…\" }` (RFC 6750 §3.1).\n\n## Format des données\n\n- Tous les corps de requête/réponse utilisent du **JSON**\n- Les noms de propriété sont en **snake_case**\n- Les ID de contact sont des **UUID**\n- Les ID de liste, de tag et de champ sont des **entiers (int64)**\n\n## Pagination\n\n**Même règle sur les trois API Mailpro** (Email v2, CRM v3, SMS) : **5 000 éléments par page,\npar défaut comme au maximum.**\n- `numberOfRecords` — éléments par page. Absent, `0` ou au-delà de 5 000 = 5 000.\n- `pageNumber` — numéro de page à partir de 1 (par défaut 1).\n- Les deux sont acceptés en paramètre de query **ou** en en-tête HTTP.\n- Une page pleine signifie qu'il peut y en avoir d'autres : demandez la page suivante\n  jusqu'à recevoir une page incomplète.\n- Exception : les messages et l'historique avec leur HTML (`includeBodyHTML=true`) renvoient\n  10 éléments par page par défaut.\n- Une page vide, ou une recherche sans résultat, renvoie `200` avec une liste vide `[]`.\n  `404` signifie que la ressource elle-même (liste, tag, segment, campagne…) n'existe pas.\n\n**Paramètres inconnus** : ils sont ignorés, jamais refusés. Quand une requête porte un\nparamètre que l'API ne reconnaît pas, la réponse contient `X-Ignored-Parameters` (les noms)\net `X-Documentation` (un lien vers cette page), pour qu'une faute de frappe ne passe pas inaperçue.\n\n## Codes d'erreur métier\n\nLes violations de règles métier retournent un HTTP **406** avec un corps JSON :\n```json\n{ \"Message\": \"Contact cannot be created. Email cannot be empty.\" }\n```\n\nMotifs 406 courants :\n- `Email cannot be empty` — l'e-mail est obligatoire\n- `Email has an invalid format` — adresse e-mail invalide\n- `A contact already exist with the same Email` — violation d'unicité de l'e-mail\n- `The email address was unsubscribed permanently` — adresse mise en liste noire\n- `The email address has identified has a hard bounce` — adresse en rebond dur\n- `The email address has identified has a spam trap` — spam trap détecté\n- `At least one List is necessary` — les contacts doivent appartenir à au moins une liste\n- `One or more lists have reached the contact limit` — 1 M de contacts max par liste\n- `Invalid Phone Number` — erreur de format de numéro de téléphone\n- `Invalid country code for Phone Number` — préfixe pays non pris en charge\n- `Length exceeded for Phone Number (12)` — numéro de téléphone trop long\n\n### Codes de statut retournés\n\n| Code | Signification |\n|------|---------------|\n| `400` | Requête mal formée (JSON invalide, message dépassant la limite de parts SMS) |\n| `401` | Identifiants manquants, invalides ou expirés |\n| `402` | Crédits insuffisants pour effectuer l'envoi |\n| `403` | Autorisé par vos identifiants, mais refusé — compte bloqué pour l'envoi de SMS |\n| `404` | L'objet référencé n'existe pas (contact, liste, tag, champ, campagne) |\n| `406` | Violation d'une règle métier de contact (voir la liste ci-dessus) |\n| `422` | Entrée rejetée par la validation, ou limite de compte atteinte |\n\n> **Changement :** certains de ces cas renvoyaient auparavant `404`. Les crédits\n> insuffisants renvoient désormais **402**, les erreurs de validation et les\n> limites de compte renvoient **422**, et un compte bloqué pour le SMS renvoie\n> **403**. `404` n'est plus utilisé que pour « cet objet n'existe pas », de\n> manière cohérente avec l'API SMS. Si votre intégration se base sur `404` pour\n> ces cas, mettez-la à jour avec les codes ci-dessus.\n\n## Format des numéros de téléphone\n\nLes numéros de téléphone sont normalisés automatiquement. Vous pouvez les envoyer dans de nombreux formats —\nles espaces, tirets, points, barres obliques, parenthèses, tirets bas et espaces insécables sont\nsupprimés avant validation. Tout autre caractère (lettres, apostrophes,\n`=`, `;`, etc.) rend le numéro invalide.\n\n| Valide                                          | Invalide                                            |\n|------------------------------------------------|----------------------------------------------------|\n| `+41794849980` — E.164 international           | `0612345678` — format national (`0` initial)       |\n| `0041 79 484 99 80` — préfixe 00 + espaces      | `41'794'849'80` — apostrophes (non supprimées)     |\n| `+33 6-12-34-56-78` — + avec tirets             | `tel=0041794849980` — caractères supplémentaires    |\n| `41794849980` — chiffres bruts                  | `1234567` — trop court (< 8 chiffres)              |\n\n**Règles :**\n- Préfixes acceptés : `+`, `00`, ou aucun préfixe\n- Longueur après normalisation : **8 à 15 chiffres** (maximum E.164)\n- Le premier chiffre après suppression du préfixe doit être **1–9** (pas `0`)\n- Séparateurs supprimés : espace, tabulation, tiret `-`, point `.`, barre oblique `/`, parenthèses `( )`, tiret bas `_`, NBSP\n- Tout le reste (lettres, apostrophes, `=`, `;`, `+` à l'intérieur du numéro, etc.) → rejeté\n- Forme stockée dans notre base de données : chiffres bruts, sans préfixe (par ex. `41794849980`)\n\n**Spécificités du Contact v3 :** le champ `phone_number` d'un Contact est actuellement\nlimité à **12 chiffres** après normalisation (contre 15 pour les points de terminaison SMS\nautonomes). Les numéros issus de pays comme l'Allemagne (13), le Brésil (13) ou l'Italie\nmobile (12–13) peuvent être rejetés sur `/v3/Contact` avec\n`Length exceeded for Phone Number (12)`. Cette contrainte héritée pourra être\nassouplie dans une future version.\n\n## Limites\n\n| Ressource | Limite |\n|----------|-------|\n| Tags par compte | 1 000 |\n| Listes par compte | 1 000 |\n| Champs personnalisés par compte | 50 |\n| Requêtes API mensuelles par compte | 50 000 (5× les crédits e-mail de l'abonnement) |\n\n## Suivi des envois (e-mails transactionnels)\n\nChaque e-mail **unitaire** envoyé (`POST /Send/Mail`, l'API Email v2, le relais SMTP ou une automation) reçoit un identifiant. Il peut être suivi jusqu'à ce que le serveur du destinataire l'accepte ou le refuse. Les campagnes ne sont pas concernées : utilisez les statistiques.\n\n1. **Envoyer** : la réponse de `POST /Send/Mail` contient `id_single_send`.\n2. **Recevoir le résultat** (recommandé) : les événements webhook `email.delivered` et `email.bounced` envoient le statut final à votre serveur dès qu'il est connu, sans aucune interrogation.\n3. **Ou interroger** : `GET /Send/{id}/Status` rend le statut du moment (tableau ci-dessous).\n\n| `status` | Signification | Final ? |\n|---|---|---|\n| `queued` | Accepté, en attente d'expédition. | non |\n| `scheduled` | Planifié pour plus tard (`date_planned`). | non |\n| `sent` | Remis par nos serveurs, pas encore de réponse du serveur du destinataire. | non |\n| `delivered` | Accepté par le serveur du destinataire (`diag_code` = sa réponse SMTP). | **oui** |\n| `bounced` | Refusé. `bounce_type` : `hard` = adresse invalide ou refus définitif ; `soft` = refus après nouvelles tentatives (boîte pleine, serveur injoignable…). | **oui** |\n| `not_found` | Identifiant inconnu ou qui n'appartient pas à votre compte. | **oui** |\n\n**Interroger sans boucler**\n\n- Arrêtez d'interroger dès que le statut est **final** : il ne changera plus.\n- Première interrogation **5 minutes** après l'envoi au plus tôt : `delivered` apparaît environ 5 minutes après la remise.\n- Ensuite, **une interrogation toutes les 15 minutes au plus**.\n- Si vous interrogez un envoi non final avant 15 minutes, vous recevez la même réponse, avec l'en-tête `Retry-After` (le nombre de secondes à attendre), et l'appel ne compte pas dans votre quota.\n- **Au plus tard 48 heures** après l'envoi, chaque e-mail a un statut final : au-delà, n'interrogez plus.\n- `scheduled` : inutile d'interroger avant `date_planned`.\n- **Chaque interrogation compte dans votre quota de requêtes API.** En suivant ces règles, un envoi coûte au plus 192 requêtes (4 par heure pendant 48 heures), et bien moins en pratique, puisque l'on s'arrête au statut final. Le même suivi par webhook coûte 1 seul événement.\n\nLes statuts sont conservés **365 jours**.\n\n## Webhooks\n\nMailpro™ peut notifier votre serveur en temps réel lorsque certains événements surviennent sur votre\ncompte (création d'un contact, e-mail délivré, fin d'une campagne, etc.).\nÀ la manière de Stripe — votre URL reçoit un POST signé en HMAC-SHA256 dès que\nl'événement se déclenche.\n\nChaque événement envoyé à votre webhook compte pour 1 requête dans votre quota de requêtes API.\n\n### Fonctionnement\n\n1. **Abonnez-vous** à un ou plusieurs types d'événements via `POST /Webhook`. La réponse\n   contient un `secret` affiché **une seule fois** (à la manière de Stripe / GitHub) — conservez-le.\n2. **Mailpro envoie un POST** signé avec une charge utile JSON à votre URL dès qu'un événement correspond.\n3. **Vérifiez la signature** côté serveur avec votre `secret` (extraits de code ci-dessous).\n4. **Répondez avec un code 200–299** dans les 10 secondes. Sinon, nous réessayons avec un\n   backoff exponentiel (10 s → 1 min → 5 min → 30 min → 2 h → 6 h → 24 h, puis abandon).\n\n> **⏱ Délai d'activation** — après avoir créé, mis à jour ou supprimé un webhook, le\n> changement met jusqu'à **10 min** à se propager sur l'ensemble des serveurs de l'API\n> Mailpro. Pendant cette fenêtre, des événements peuvent ne pas encore correspondre à votre nouvel abonnement\n> (ou continuer à correspondre à un abonnement tout juste supprimé). C'est le temps nécessaire\n> pour rafraîchir le cache d'abonnements en mémoire sur chaque instance d'API.\n\n### Enveloppe de la charge utile\n\nTous les événements partagent une enveloppe commune :\n\n```json\n{\n  \"id\":         \"evt_8f3a9b2c4d1e5f6789abcdef01234567\",\n  \"type\":       \"contact.created\",\n  \"created\":    1748194800,\n  \"apiVersion\": \"v3\",\n  \"account\":    { \"id\": 12345, \"name\": \"ACME Corp\" },\n  \"data\":       { \"id\": \"...\", \"email\": \"alice@example.com\", \"...\": \"...\" }\n}\n```\n\n| Champ | Format | Description |\n|-------|--------|-------------|\n| `id` | `evt_` + 32 hex | ID d'événement unique — utilisez-le comme clé d'idempotence de votre côté. |\n| `type` | `<domain>.<verb>` | Type d'événement canonique (par ex. `email.delivered`, `contact.created`). |\n| `created` | Secondes Unix UTC | Inclus dans la signature HMAC. |\n| `apiVersion` | `\"v3\"` | API Mailpro qui a publié l'événement. |\n| `account.id` | int | Votre ID client Mailpro. |\n| `account.name` | string | Le nom de votre compte (pratique pour le débogage côté client). |\n| `data` | object | Charge utile de l'événement — la forme dépend du `type` (voir catalogue ci-dessous). |\n\n### Catalogue des événements\n\n| Événement | Déclencheur | Forme de `data` |\n|-------|---------|--------------|\n| `ping` | Test via `POST /Webhook/{id}/test` | `{ message }` |\n| `contact.created` | `POST /Contact` | `{ id, email, firstName, lastName, createdAt }` |\n| `contact.updated` | `PUT /Contact/{id}` | `{ id, email, firstName, lastName, updatedAt, changes }` |\n| `contact.deleted` | `DELETE /Contact/{id}` | `{ id, email, deletedAt }` |\n| `list.contact_added` | Contact ajouté à une liste | `{ listId, listName, contactId, contactEmail, addedAt }` |\n| `list.contact_removed` | Contact retiré d'une liste | `{ listId, listName, contactId, contactEmail, removedAt }` |\n| `tag.applied` | Tag appliqué à un contact | `{ tag, contactId, contactEmail, appliedAt }` |\n| `tag.removed` | Tag retiré | `{ tag, contactId, contactEmail, removedAt }` |\n| `segment.matched` | Un contact entre dans un segment | `{ segmentId, segmentName, contactId, contactEmail, matchedAt }` |\n| `email.processed` | Envoi accepté par le pipeline | `{ messageId, email, campaignId, processedAt }` |\n| `email.delivered` | Un email **unitaire** a été accepté par le serveur destinataire — API v2/v3, SMTP ou Automation (**pas** les campagnes). Émis depuis le flux de livraison PowerMTA. | `{ messageId, email, idSending, sendingType, diagCode, deliveredAt }` |\n| `email.opened` | Pixel de tracking déclenché | `{ messageId, email, campaignId, openedAt, ip, userAgent }` |\n| `email.clicked` | Clic sur un lien tracké | `{ messageId, email, campaignId, clickedAt, url, ip, userAgent }` |\n| `email.bounced` | Rebond dur ou doux | `{ messageId, email, campaignId, bouncedAt, bounceType, reason }` |\n| `email.spam_report` | Plainte FBL | `{ messageId, email, campaignId, reportedAt }` |\n| `email.unsubscribed` | Clic sur le lien de désinscription | `{ messageId, email, campaignId, unsubscribedAt }` |\n| `email.dropped` | Refusé avant envoi | `{ messageId, email, campaignId, droppedAt, reason }` |\n| `campaign.started` | Démarrage de la diffusion | `{ campaignId, name, totalRecipients, startedAt }` |\n| `campaign.finished` | Tous les destinataires traités | `{ campaignId, name, totalSent, totalFailed, finishedAt }` |\n| `import.started` | Démarrage d'une tâche d'import | `{ importId, fileName, totalRows, startedAt }` |\n| `import.progress` | À 25/50/75 % | `{ importId, fileName, processedRows, totalRows, progressPct }` |\n| `import.completed` | Import OK | `{ importId, fileName, totalRows, addedCount, updatedCount, errorCount, completedAt }` |\n| `import.failed` | Erreur fatale pendant l'import | `{ importId, fileName, errorMessage, failedAt }` |\n\nAbonnez-vous à tous les événements de cette API en passant `[\"*\"]` dans le tableau `events`.\n\n### Signature HMAC\n\nChaque POST porte un en-tête `X-Mailpro-Signature` :\n\n```\nX-Mailpro-Signature: t=1748194800,v1=7f9ac2b6e4d1c8a3...\nX-Mailpro-Event-Id:  evt_8f3a9b2c4d1e5f6789abcdef01234567\nX-Mailpro-Event-Type: contact.created\nX-Mailpro-Attempt:   1\n```\n\nPour vérifier :\n1. Extrayez `t` et `v1` de l'en-tête.\n2. **Rejetez** si `|now - t| > 300` (fenêtre de protection anti-rejeu de 5 min).\n3. Calculez `expected = hex(HMAC_SHA256(secret, t + \".\" + raw_body))`.\n4. **Comparez en temps constant** `v1` avec `expected` (`crypto.timingSafeEqual`,\n   `hash_equals`, `CryptographicOperations.FixedTimeEquals`) — jamais `==`.\n\n#### Node.js\n\n```js\nconst crypto = require('crypto');\n\nfunction verifyMailproSignature(rawBody, header, secret) {\n  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));\n  if (Math.abs(Date.now() / 1000 - parseInt(parts.t)) > 300) return false;\n  const expected = crypto\n    .createHmac('sha256', secret)\n    .update(parts.t + '.' + rawBody)\n    .digest('hex');\n  return crypto.timingSafeEqual(\n    Buffer.from(parts.v1, 'hex'),\n    Buffer.from(expected, 'hex'));\n}\n```\n\n#### PHP\n\n```php\nfunction verifyMailproSignature(string $rawBody, string $header, string $secret): bool {\n    parse_str(str_replace(',', '&', $header), $p);\n    if (abs(time() - (int)$p['t']) > 300) return false;\n    $expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);\n    return hash_equals($expected, $p['v1']);\n}\n```\n\n#### Python\n\n```python\nimport hmac, hashlib, time\n\ndef verify_mailpro_signature(raw_body: bytes, header: str, secret: str) -> bool:\n    parts = dict(p.split('=') for p in header.split(','))\n    t = int(parts['t'])\n    if abs(time.time() - t) > 300:\n        return False\n    expected = hmac.new(\n        secret.encode(),\n        f\"{t}.\".encode() + raw_body,\n        hashlib.sha256\n    ).hexdigest()\n    return hmac.compare_digest(expected, parts['v1'])\n```\n\n#### C#\n\n```csharp\nusing System.Security.Cryptography;\nusing System.Text;\n\nstatic bool VerifyMailproSignature(string rawBody, string header, string secret) {\n    var parts = header.Split(',')\n        .Select(p => p.Split('='))\n        .ToDictionary(a => a[0], a => a[1]);\n    var t = long.Parse(parts[\"t\"]);\n    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false;\n    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));\n    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes($\"{t}.{rawBody}\"));\n    var expected = Convert.ToHexString(hash).ToLowerInvariant();\n    return CryptographicOperations.FixedTimeEquals(\n        Encoding.UTF8.GetBytes(expected),\n        Encoding.UTF8.GetBytes(parts[\"v1\"]));\n}\n```\n\n### Réessais et désactivation automatique\n\n| Tentative | Délai avant la suivante |\n|---------|----------------------|\n| 1 → 2 | 10 secondes |\n| 2 → 3 | 1 min |\n| 3 → 4 | 5 min |\n| 4 → 5 | 30 min |\n| 5 → 6 | 2 h |\n| 6 → 7 | 6 h |\n| 7 | abandonné (livraison marquée `failed`) |\n\nAprès **10 échecs consécutifs** sur le même webhook, il est **automatiquement\ndésactivé** (`isActive: false`) et vous recevez un e-mail de notification. Réactivez-le\navec `PUT /Webhook/{id}` une fois votre endpoint corrigé.\n\n### Succès = HTTP 2xx\n\nNous considérons votre webhook comme réussi uniquement si vous retournez un statut compris dans `200–299`.\nTout 3xx/4xx/5xx ou un timeout (> 10 s) compte comme un échec et déclenche des réessais.\nSi votre endpoint est définitivement hors service, retournez explicitement `410 Gone` — les réessais\nont toujours lieu, mais le motif d'échec est plus clair dans votre journal de livraison.\n\n### Limites\n\n| Limite | Valeur |\n|-------|-------|\n| Webhooks par compte | 20 |\n| Événements souscrits par webhook | 50 |\n| Taille maximale de la charge utile | 256 Ko |\n| Timeout HTTP par tentative | 10 secondes |\n| Conservation de l'historique de livraison | 30 jours |\n"
  },
  "servers": [
    {
      "url": "https://api.mailpro.com/v3",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "IdClientHeader": [],
      "ApiKeyHeader": []
    },
    {
      "IdClientQuery": [],
      "ApiKeyQuery": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Jeton Bearer JWT obtenu via `POST /token`.\nTransmettez-le ainsi : `Authorization: Bearer <token>`\n"
      },
      "OAuth2": {
        "type": "oauth2",
        "description": "Mailpro OAuth 2.0 — la méthode moderne permettant aux applications tierces et\naux plateformes d’intégration (Zapier, Make, n8n) d’agir au nom d’un utilisateur\nMailpro grâce à des jetons d’accès limités et révocables.\n\n- **authorizationCode + PKCE** (S256, obligatoire pour les clients publics) — le\n  flux de consentement « Connect Mailpro ». Délivre un jeton d’accès ainsi qu’un\n  refresh token rotatif.\n- **clientCredentials** — communication serveur à serveur, sans utilisateur.\n\nUn serveur d’autorisation unique pour toutes les API, à la surface neutre en version\n`https://api.mailpro.com/oauth/` (émetteur / claim `iss`\n`https://api.mailpro.com/oauth/`). Les jetons d’accès sont des JWT RS256\n(à vérifier auprès du JWKS sur `https://api.mailpro.com/oauth/.well-known/jwks`) ;\nTTL **20 minutes**. Les refresh tokens sont valables **30 jours** (glissants) et\n**à usage unique** (rotation à chaque rafraîchissement). Les codes d’autorisation\nont une durée de vie de **60 secondes**.\n\nLe même jeton d’accès OAuth est accepté par les trois API Mailpro\n(CRM v3, Email v2, SMS) — authentifiez-vous une seule fois, appelez n’importe\nquelle API ; l’accès est régi par les scopes du jeton, et non par l’URL de base\nde l’API. L’authentification historique (Bearer JWT / IdClient+ApiKey) continue\nde fonctionner sans changement et n’est PAS soumise au contrôle des scopes.\n",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://api.mailpro.com/oauth/authorize",
            "tokenUrl": "https://api.mailpro.com/oauth/token",
            "refreshUrl": "https://api.mailpro.com/oauth/token",
            "scopes": {
              "contacts:read": "View your contacts, lists, tags, segments and custom fields.",
              "contacts:write": "Create, edit and delete your contacts, lists, tags and custom fields.",
              "email:send": "Send transactional and campaign emails on your behalf.",
              "email:read": "View delivery stats, opens, clicks and bounce history.",
              "email:templates:read": "View your email templates (subject, HTML, plain-text body).",
              "email:templates:write": "Create, edit and delete your email templates.",
              "sms:send": "Send transactional and campaign SMS messages on your behalf.",
              "sms:read": "View SMS credit balance and message history.",
              "sms:contacts:read": "View your SMS phone book.",
              "sms:contacts:write": "Create, edit and delete entries in your SMS phone book.",
              "webhooks:manage": "Subscribe to, edit and remove webhook endpoints for your account.",
              "account:read": "View your account credits, limits and subscription status."
            }
          },
          "clientCredentials": {
            "tokenUrl": "https://api.mailpro.com/oauth/token",
            "scopes": {
              "contacts:read": "View your contacts, lists, tags, segments and custom fields.",
              "contacts:write": "Create, edit and delete your contacts, lists, tags and custom fields.",
              "email:send": "Send transactional and campaign emails on your behalf.",
              "email:read": "View delivery stats, opens, clicks and bounce history.",
              "email:templates:read": "View your email templates (subject, HTML, plain-text body).",
              "email:templates:write": "Create, edit and delete your email templates.",
              "sms:send": "Send transactional and campaign SMS messages on your behalf.",
              "sms:read": "View SMS credit balance and message history.",
              "sms:contacts:read": "View your SMS phone book.",
              "sms:contacts:write": "Create, edit and delete entries in your SMS phone book.",
              "webhooks:manage": "Subscribe to, edit and remove webhook endpoints for your account.",
              "account:read": "View your account credits, limits and subscription status."
            }
          }
        }
      },
      "IdClientHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "IdClient",
        "description": "Votre identifiant client Mailpro numérique — transport recommandé (en-tête HTTP).\nLes URL sont journalisées par les serveurs web, Cloudflare et les proxys, si bien\nque les identifiants passés en chaîne de requête peuvent fuiter dans les journaux\nd’accès. Les en-têtes, eux, ne sont pas journalisés.\n"
      },
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "ApiKey",
        "description": "Votre clé d’API (GUID) — transport recommandé (en-tête HTTP)."
      },
      "IdClientQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "IdClient",
        "description": "Votre identifiant client Mailpro numérique — méthode historique, conservée pour\ncompatibilité ascendante. Passez à l’en-tête `IdClient` dès que possible.\n"
      },
      "ApiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "ApiKey",
        "description": "Votre clé d’API (GUID) — méthode historique, conservée pour compatibilité ascendante."
      }
    },
    "schemas": {
      "TokenResponse": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Jeton d’accès JWT — à utiliser dans toutes les requêtes suivantes sous la forme `Authorization: Bearer <token>`"
          },
          "token_type": {
            "type": "string",
            "example": "bearer"
          },
          "expires_in": {
            "type": "integer",
            "description": "Durée de vie du jeton, en secondes",
            "example": 31535999
          },
          "userName": {
            "type": "string",
            "description": "Identifiant client de l’utilisateur authentifié",
            "example": "12345"
          },
          "refresh_token": {
            "type": "string",
            "description": "Jeton permettant d’obtenir un nouveau jeton d’accès sans se ré-authentifier"
          },
          "application": {
            "type": "string",
            "description": "Nom de l’intégration (uniquement pour les délégations tierces)",
            "nullable": true
          }
        }
      },
      "Contact": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant unique du contact (UUID)",
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          "email": {
            "type": "string",
            "description": "Adresse e-mail du contact",
            "example": "alice@example.com"
          },
          "phone_number": {
            "type": "string",
            "description": "Numéro de téléphone — accepte les formats `+41…`, `0041…` ou `41…` avec ou sans séparateurs. Voir **Format des numéros de téléphone** dans la description de l’API.",
            "example": "+41791234567"
          },
          "tags": {
            "type": "array",
            "description": "Tags assignés à ce contact (inclus lorsque `includeTags=true`)",
            "items": {
              "$ref": "#/components/schemas/TagRef"
            }
          },
          "lists": {
            "type": "array",
            "description": "Listes auxquelles ce contact appartient (incluses lorsque `includeLists=true`)",
            "items": {
              "$ref": "#/components/schemas/ListRef"
            }
          },
          "custom_fields": {
            "type": "array",
            "description": "Valeurs des champs personnalisés pour ce contact",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          }
        }
      },
      "ContactEntry": {
        "type": "object",
        "required": [
          "email",
          "lists"
        ],
        "properties": {
          "email": {
            "type": "string",
            "description": "Adresse e-mail du contact (obligatoire, doit être unique)",
            "example": "alice@example.com"
          },
          "phone_number": {
            "type": "string",
            "description": "Numéro de téléphone — accepte les formats `+41…`, `0041…` ou `41…` avec ou sans séparateurs.\nDoit commencer par un indicatif pays valide. 12 chiffres maximum sur Contact (voir\n**Format des numéros de téléphone** dans la description de l’API pour les règles complètes).\n",
            "example": "+41791234567"
          },
          "lists": {
            "type": "array",
            "description": "Listes auxquelles affecter ce contact. **Au moins une liste est obligatoire.**",
            "items": {
              "$ref": "#/components/schemas/ListRef"
            },
            "example": [
              {
                "id": 101
              }
            ]
          },
          "tags": {
            "type": "array",
            "description": "Tags à affecter à ce contact (facultatif)",
            "items": {
              "$ref": "#/components/schemas/TagRef"
            },
            "example": [
              {
                "id": 5
              }
            ]
          },
          "custom_fields": {
            "type": "array",
            "description": "Valeurs des champs personnalisés pour ce contact (facultatif)",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            },
            "example": [
              {
                "id": 1,
                "value": "Alice"
              },
              {
                "id": 2,
                "value": "Dupont"
              }
            ]
          }
        }
      },
      "TagRef": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Identifiant du tag (issu de `GET /Tag`)",
            "example": 5
          }
        }
      },
      "ListRef": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Identifiant de la liste (issu de `GET /List`)",
            "example": 101
          }
        }
      },
      "FieldValue": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Identifiant du champ personnalisé (issu de `GET /Field`)",
            "example": 1
          },
          "value": {
            "type": "string",
            "description": "Valeur à enregistrer pour ce champ. Le type doit correspondre à celui du champ.",
            "example": "Alice"
          }
        }
      },
      "List": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Identifiant unique de la liste",
            "example": 101
          },
          "name": {
            "type": "string",
            "description": "Nom d’affichage de la liste",
            "example": "Newsletter subscribers"
          },
          "total_contacts": {
            "type": "integer",
            "description": "Nombre total de contacts dans la liste (inclus lorsque `includeTotalContacts=true`)",
            "example": 4521
          },
          "date_creation": {
            "type": "string",
            "format": "date-time",
            "example": "2025-01-15T10:30:00"
          }
        }
      },
      "ListEntry": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Nom d’affichage de la liste",
            "example": "Newsletter subscribers — France"
          }
        }
      },
      "Tag": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Identifiant unique du tag",
            "example": 5
          },
          "name": {
            "type": "string",
            "description": "Nom d’affichage du tag",
            "example": "VIP customers"
          }
        }
      },
      "TagEntry": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Nom d’affichage du tag",
            "example": "VIP customers"
          }
        }
      },
      "Field": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Identifiant unique du champ personnalisé",
            "example": 1
          },
          "name": {
            "type": "string",
            "description": "Nom interne du champ",
            "example": "first_name"
          },
          "personalisation_key": {
            "type": "string",
            "description": "Nom de variable utilisé pour la personnalisation dans les modèles d’e-mail (ex. `{{first_name}}`)",
            "example": "first_name"
          },
          "field_type": {
            "type": "string",
            "description": "Type de données du champ",
            "enum": [
              "text",
              "number",
              "date"
            ],
            "example": "text"
          }
        }
      },
      "FieldEntry": {
        "type": "object",
        "required": [
          "name",
          "id_field_type",
          "personalize_key"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Nom affiché du champ",
            "example": "First name"
          },
          "id_field_type": {
            "type": "integer",
            "description": "Identifiant du type de champ (types disponibles via `GET /Field/Types`) :\n- `1` = texte\n- `2` = nombre entier\n- `3` = date et heure\n- `4` = oui / non\n- `12` = nombre décimal\n",
            "example": 1
          },
          "personalize_key": {
            "type": "string",
            "description": "Clé de variable du modèle, pour insérer le champ dans un message",
            "example": "first_name"
          },
          "sort": {
            "type": "integer",
            "minimum": 0,
            "maximum": 255,
            "description": "Ordre d'affichage (0 à 255). Obligatoire pour modifier un champ (`PUT /Field/{id}`).",
            "example": 255
          }
        }
      },
      "CampaignEntry": {
        "type": "object",
        "properties": {
          "campaign_title": {
            "type": "string",
            "description": "Nom d’affichage de la campagne",
            "example": "Spring Newsletter April 2026"
          },
          "date_planned": {
            "type": "string",
            "format": "date-time",
            "description": "Heure d’envoi planifiée (ISO 8601). Omettre pour un envoi immédiat.",
            "example": "2026-04-20T09:00:00"
          }
        }
      },
      "SendEmailRequest": {
        "type": "object",
        "description": "Requête d’e-mail transactionnel. Fournissez **soit** `email` (adresse en ligne)\n**soit** `id_contact` (contact CRM) — pas les deux.\nFournissez **soit** `body_html` (HTML en ligne) **soit** `id_message` (modèle enregistré) — pas les deux.\n",
        "properties": {
          "id_sender_email": {
            "type": "integer",
            "description": "Identifiant de l’adresse e-mail expéditrice (issu de `GET /SenderEmail`). Obligatoire.",
            "example": 201
          },
          "email": {
            "type": "string",
            "description": "Adresse email du destinataire (en ligne). Fournissez ceci **OU** `id_contact`.\nSi les deux sont fournis, `id_contact` prévaut.\n\n**La partie locale** (avant le `@`) accepte les lettres, les chiffres et `. _ - +` —\ny compris le sous-adressage RFC 5322 (`user+tag@domain`, ex. Gmail, Fastmail,\nProton). Les autres caractères spéciaux autorisés par la RFC 5322\n(ex. `! # $ % & ' * / = ? ^ { | } ~`) et les parties locales entre guillemets ne\nsont **pas** acceptés.\n",
            "example": "alice@example.com"
          },
          "id_contact": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du contact CRM destinataire. Fournissez ceci **OU** `email`.\nUtiliser `id_contact` permet la personnalisation avec les valeurs des champs personnalisés du contact.\n",
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          "subject": {
            "type": "string",
            "description": "Objet de l’e-mail (255 caractères max). Obligatoire.",
            "example": "Your order #12345 has been shipped"
          },
          "body_html": {
            "type": "string",
            "description": "Contenu HTML en ligne. Fournissez ceci **OU** `id_message`.\nSi les deux sont fournis, `id_message` est prioritaire.\n",
            "example": "<p>Hello {{first_name}}, your order has shipped!</p>"
          },
          "id_message": {
            "type": "integer",
            "description": "Identifiant d’un modèle de message enregistré (issu de `GET /Message`).\nFournissez ceci **OU** `body_html`.\n",
            "example": 501
          },
          "reply_to": {
            "type": "string",
            "description": "Adresse de réponse (par défaut, l’adresse de l’expéditeur)",
            "example": "support@yourcompany.com"
          },
          "fields": {
            "type": "array",
            "description": "Champs de personnalisation personnalisés (utilisés uniquement avec un `email` en ligne, pas avec `id_contact`)",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "date_planned": {
            "type": "string",
            "format": "date-time",
            "description": "Planifier l’envoi à une date ultérieure. Omettre pour un envoi immédiat.",
            "example": "2026-04-15T14:00:00"
          },
          "attachments": {
            "type": "array",
            "maxItems": 5,
            "description": "Pièces jointes optionnelles (max **5**). Chaque élément est un objet avec un `filename` et un `content` encodé en Base64. Limites : **10 Mo par fichier** et **15 Mo au total**, mesurés après décodage Base64. Types acceptés : PDF, Word, Excel, PowerPoint, images (JPG, PNG, GIF, WebP), ZIP, CSV, TXT — les fichiers exécutables et scripts sont refusés. **Facturation :** l'envoi garde son crédit habituel, plus **1 crédit par 512 Ko de pièces jointes au-delà des 512 premiers Ko** (les pièces jointes ≤ 512 Ko n'ajoutent rien).",
            "items": {
              "$ref": "#/components/schemas/EmailAttachment"
            }
          },
          "keep_in_my_files": {
            "type": "boolean",
            "default": false,
            "description": "`false` (par défaut) : les pièces jointes ne servent qu'à cet envoi — elles sont stockées hors de\nvotre compte, ne comptent pas dans votre quota de stockage et sont supprimées automatiquement après\n35 jours. `true` : les pièces jointes sont aussi conservées dans les fichiers de votre compte (dossier\n`Secure/Files/Sending/api`) et comptent dans votre quota de stockage.\n",
            "example": false
          }
        }
      },
      "EmailAttachment": {
        "type": "object",
        "required": [
          "filename",
          "content"
        ],
        "description": "Une pièce jointe d'email avec un contenu encodé en Base64.",
        "properties": {
          "filename": {
            "type": "string",
            "description": "Nom de fichier original affiché au destinataire, par ex. invoice.pdf.",
            "example": "invoice.pdf"
          },
          "content": {
            "type": "string",
            "format": "byte",
            "description": "Contenu du fichier, encodé en Base64.",
            "example": "JVBERi0xLjQKJVBERi0xLjQK..."
          }
        }
      },
      "SendCampaignRequest": {
        "type": "object",
        "required": [
          "id_message",
          "id_sender_email",
          "lists",
          "campaign_title"
        ],
        "properties": {
          "id_message": {
            "type": "integer",
            "description": "Identifiant du modèle de message à utiliser (issu de `GET /Message`)",
            "example": 501
          },
          "id_sender_email": {
            "type": "integer",
            "description": "Identifiant de l’adresse expéditrice (issu de `GET /SenderEmail`)",
            "example": 201
          },
          "id_email_reply_to": {
            "type": "integer",
            "description": "Identifiant de l’adresse expéditrice de réponse (issu de `GET /SenderEmail`). Doit être l’une des adresses expéditrices de votre compte, sinon la requête est refusée. Par défaut : `id_sender_email`.",
            "example": 201
          },
          "lists": {
            "type": "array",
            "description": "Listes de contacts cibles (au moins une est obligatoire)",
            "items": {
              "$ref": "#/components/schemas/ListRef"
            },
            "example": [
              {
                "id": 101
              }
            ]
          },
          "id_segment": {
            "type": "integer",
            "description": "Filtrer les destinataires par un segment enregistré (facultatif)",
            "example": 7
          },
          "campaign_title": {
            "type": "string",
            "description": "Nom d’affichage de cette campagne (150 caractères max)",
            "example": "Spring Newsletter April 2026"
          },
          "date_planned": {
            "type": "string",
            "format": "date-time",
            "description": "Heure d’envoi planifiée (ISO 8601). Omettre pour un envoi immédiat.",
            "example": "2026-04-20T09:00:00"
          },
          "id_automation": {
            "type": "integer",
            "description": "Lier cet envoi à un workflow d’automatisation (facultatif)",
            "example": 3
          }
        }
      },
      "SmsSingleRequest": {
        "type": "object",
        "required": [
          "phone_number",
          "message"
        ],
        "properties": {
          "phone_number": {
            "type": "string",
            "description": "Numéro de téléphone du destinataire — accepte les formats `+41…`, `0041…` ou `41…` avec ou sans séparateurs. Voir **Format des numéros de téléphone** dans la description de l’API.",
            "example": "+41791234567"
          },
          "message": {
            "type": "string",
            "description": "Texte du SMS (160 caractères max en encodage standard)",
            "example": "Your verification code is 9473. Valid for 10 minutes."
          },
          "date_planned": {
            "type": "string",
            "format": "date-time",
            "description": "Planifier l’envoi. Omettre pour un envoi immédiat.",
            "example": "2026-04-15T14:00:00"
          }
        }
      },
      "SmsCampaignRequest": {
        "type": "object",
        "required": [
          "lists",
          "message"
        ],
        "properties": {
          "lists": {
            "type": "array",
            "description": "Listes de contacts cibles (utilise le champ phone_number de chaque contact)",
            "items": {
              "$ref": "#/components/schemas/ListRef"
            },
            "example": [
              {
                "id": 101
              }
            ]
          },
          "message": {
            "type": "string",
            "description": "Texte du SMS",
            "example": "Flash sale today — 20% off all products. Shop at mailpro.com"
          },
          "date_planned": {
            "type": "string",
            "format": "date-time",
            "description": "Planifier l’envoi. Omettre pour un envoi immédiat.",
            "example": "2026-04-20T10:00:00"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "Message": {
            "type": "string",
            "description": "Message d’erreur lisible par un humain",
            "example": "Contact cannot be created. Email cannot be empty."
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Identifiant du webhook (Mailpro)",
            "example": 42
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Endpoint HTTPS qui reçoit les événements",
            "example": "https://example.com/hooks/mailpro"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Types d’événements souscrits",
            "example": [
              "email.delivered",
              "email.bounced",
              "contact.created"
            ]
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Description libre (votre propre référence)",
            "example": "Slack integration"
          },
          "isActive": {
            "type": "boolean",
            "description": "Lorsque ce champ vaut `false`, aucun événement n’est délivré. Désactivé automatiquement après 10 échecs consécutifs.",
            "example": true
          },
          "consecutiveFailures": {
            "type": "integer",
            "description": "Nombre de tentatives de livraison échouées consécutives (remis à zéro en cas de succès)",
            "example": 0
          },
          "autoDisabledAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Renseigné lorsque `isActive` a été basculé automatiquement à `false`"
          },
          "autoDisabledReason": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookCreate": {
        "type": "object",
        "required": [
          "url",
          "events"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Endpoint HTTPS (HTTP refusé en production)",
            "example": "https://example.com/hooks/mailpro"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 50,
            "description": "Liste des types d’événements à souscrire (voir le catalogue ci-dessus). Utilisez `[\"*\"]` pour souscrire à tous les événements de cette API.",
            "example": [
              "email.delivered",
              "email.bounced",
              "contact.created"
            ]
          },
          "description": {
            "type": "string",
            "maxLength": 500,
            "description": "Description libre pour votre référence",
            "example": "Slack integration"
          }
        }
      },
      "WebhookCreateResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Webhook"
          },
          {
            "type": "object",
            "properties": {
              "secret": {
                "type": "string",
                "description": "Secret HMAC. **Affiché une seule fois ici, jamais à nouveau.** Conservez-le côté serveur — il sert à signer chaque payload envoyé à votre URL.",
                "example": "whsec_AbcD1234EfgH5678IjkL9012MnoP3456QrsT"
              }
            }
          }
        ]
      },
      "WebhookUpdate": {
        "type": "object",
        "description": "Mise à jour partielle — chaque champ est facultatif.",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 50
          },
          "description": {
            "type": "string",
            "maxLength": 500
          },
          "isActive": {
            "type": "boolean",
            "description": "Mettre à `true` pour réactiver un webhook désactivé automatiquement, une fois votre endpoint corrigé."
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Identifiant de la tentative de livraison"
          },
          "eventId": {
            "type": "string",
            "description": "UUID de l’événement (= le champ `id` dans le payload reçu par votre endpoint)",
            "example": "evt_8f3a9b2c4d1e5f6789abcdef01234567"
          },
          "eventType": {
            "type": "string",
            "example": "email.delivered"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "success",
              "retrying",
              "failed"
            ],
            "description": "État actuel de cette livraison"
          },
          "statusCode": {
            "type": "integer",
            "nullable": true,
            "description": "Code de statut HTTP renvoyé par votre endpoint (`null` si aucune tentative n’a encore été effectuée ou en cas de timeout)",
            "example": 200
          },
          "responseTimeMs": {
            "type": "integer",
            "nullable": true,
            "example": 142
          },
          "attemptCount": {
            "type": "integer",
            "description": "Nombre de tentatives effectuées jusqu’ici (1 à 7, puis échec définitif)"
          },
          "lastError": {
            "type": "string",
            "nullable": true,
            "description": "Premiers 2 Kio du corps de la réponse ou du message d’exception lorsqu’une tentative échoue"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastAttemptAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "nextAttemptAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Date à laquelle aura lieu la prochaine tentative — `null` si la livraison a réussi ou a été définitivement abandonnée."
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Authentication",
      "description": "Obtenir un jeton Bearer JWT pour authentifier toutes les autres requêtes API.\nLe point d’entrée de jeton accepte plusieurs grant types : `password`, `refresh_token`,\n`client_credentials`, ainsi que les intégrations tierces (`zapier`, `automateio`, `microsoft-flow`).\n"
    },
    {
      "name": "Account",
      "description": "Récupérer le profil du compte, le solde de crédits et les détails de l’abonnement."
    },
    {
      "name": "Contact",
      "description": "CRUD complet pour les contacts CRM. Les contacts stockent une adresse e-mail, un numéro de téléphone facultatif,\nles valeurs des champs personnalisés, et appartiennent à une ou plusieurs **Listes**. Ils peuvent également être étiquetés avec des **Tags**.\nLes identifiants de contact sont des UUID.\n"
    },
    {
      "name": "List",
      "description": "Gérer les listes de contacts. Les listes regroupent les contacts en vue des campagnes et de la segmentation.\nMaximum 1 000 listes par compte (même limite sur les trois API Mailpro et dans l'app). Maximum 1 000 000 de contacts par liste.\n"
    },
    {
      "name": "Tag",
      "description": "Gérer les tags pour étiqueter et segmenter les contacts.\nLes tags sont des étiquettes à plat (ex. « VIP », « Churned », « B2B »).\nMaximum 1 000 tags par compte.\n"
    },
    {
      "name": "Field",
      "description": "Gérer les champs personnalisés des contacts. Les champs vous permettent de stocker des données supplémentaires sur chaque contact\n(ex. prénom, ville, niveau de fidélité). Maximum 50 champs par compte.\nTypes de champs : `text`, `number`, `date`.\n"
    },
    {
      "name": "Message",
      "description": "Gérer les modèles d’e-mail HTML. Les modèles stockent l’objet, le corps HTML\net les paramètres du lien de désinscription. Réutilisez les modèles sur plusieurs campagnes.\n"
    },
    {
      "name": "Campaign",
      "description": "Consulter les statistiques de campagne et gérer les campagnes planifiées.\nLes campagnes peuvent être modifiées ou supprimées jusqu’à 5 minutes avant l’heure d’envoi planifiée.\n"
    },
    {
      "name": "Send",
      "description": "Envoyer des campagnes e-mail en masse (vers des listes de contacts) et des e-mails transactionnels individuels.\nLes campagnes peuvent être planifiées ou envoyées immédiatement.\n"
    },
    {
      "name": "Sender Addresses",
      "description": "Gérer les adresses e-mail expéditrices validées. Seules les adresses vérifiées peuvent être utilisées comme `From`.\n"
    },
    {
      "name": "History",
      "description": "Parcourir l’historique des envois d’e-mails et de SMS."
    },
    {
      "name": "Segment",
      "description": "Gérer les segments dynamiques — des règles de filtrage enregistrées qui sélectionnent automatiquement\nles contacts en fonction des valeurs de champs, du comportement ou de l’appartenance aux listes.\n"
    },
    {
      "name": "SMS",
      "description": "Envoyer des SMS à des contacts individuels ou à des listes de contacts entières.\nUtilise le champ `phone_number` de chaque contact.\n"
    },
    {
      "name": "Folders",
      "description": "Organiser les modèles de messages dans des dossiers."
    },
    {
      "name": "Import",
      "description": "Téléverser un fichier CSV ou XLSX pour importer des contacts dans une liste en masse.\nLe fichier est mis en file d’attente pour un traitement asynchrone — suivez la complétion via\nl’`import_job_id` renvoyé ou via le callback facultatif `webhook_url`.\n"
    },
    {
      "name": "Webhooks",
      "description": "Abonner votre serveur aux notifications d’événements en temps réel. Mailpro envoie en POST\nun payload JSON signé HMAC à votre URL dès qu’un événement souscrit se produit.\nVoir la section « Webhooks » de l’introduction pour le catalogue complet des événements,\nl’enveloppe du payload, des extraits de vérification de signature et la politique de relance.\n"
    },
    {
      "name": "Suppression",
      "description": "Bounces et désinscrits du compte : lister, ajouter, retirer."
    }
  ],
  "paths": {
    "/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "getAccessToken",
        "summary": "Obtenir un jeton d’accès",
        "security": [],
        "description": "Obtient un jeton Bearer JWT à partir des identifiants client.\nTransmettez l’`access_token` renvoyé sous la forme `Authorization: Bearer <token>` dans toutes les requêtes suivantes.\n\n### Grant types pris en charge\n\n| Grant type | Cas d’usage |\n|------------|----------|\n| `password` | Intégration standard — nom d’utilisateur + clé d’API |\n| `refresh_token` | Renouveler un jeton d’accès qui expire sans se ré-authentifier |\n| `client_credentials` | Serveur à serveur, avec l’identifiant client uniquement |\n| `zapier` / `automateio` / `microsoft-flow` | Intégrations de plateformes tierces |\n\n### Durée de vie des jetons\n- Jeton d’accès : **30 jours**\n- Refresh token : **90 jours**\n\nUtilisez `grant_type=refresh_token` pour obtenir un nouveau jeton d’accès sans\nvous ré-authentifier — votre SDK devrait le faire automatiquement lorsque le\njeton d’accès approche de son expiration (stratégie habituelle : rafraîchir\nlorsqu’il reste moins de 24 h).\n\n### Stocker les jetons en toute sécurité\nMême avec une durée de vie de 30 jours, quiconque obtient un `access_token` peut\nse faire passer pour votre compte jusqu’à son expiration. Stockez les jetons dans un\ngestionnaire de secrets côté serveur (Vault, AWS Secrets Manager, Azure Key Vault,\nvariables d’environnement sur un hôte durci). **N’incorporez jamais** un jeton dans :\n- du JavaScript côté client ni dans un bundle navigateur\n- des applications mobiles distribuées aux utilisateurs (le binaire peut être inspecté)\n- des dépôts Git publics ou des journaux de build CI\nSi un jeton est divulgué, révoquez-le depuis les paramètres de votre compte Mailpro\net demandez-en un nouveau.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "description": "Grant type d’authentification",
                    "enum": [
                      "password",
                      "refresh_token",
                      "client_credentials",
                      "zapier",
                      "automateio",
                      "microsoft-flow"
                    ],
                    "example": "password"
                  },
                  "username": {
                    "type": "string",
                    "description": "Votre nom d’utilisateur Mailpro (e-mail). Obligatoire pour le grant `password`.",
                    "example": "user@yourcompany.com"
                  },
                  "password": {
                    "type": "string",
                    "description": "Votre clé d’API Mailpro (format GUID). Obligatoire pour le grant `password`.",
                    "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  },
                  "refresh_token": {
                    "type": "string",
                    "description": "Refresh token précédent. Obligatoire pour le grant `refresh_token`."
                  },
                  "client_id": {
                    "type": "string",
                    "description": "Votre identifiant client Mailpro (entier). Obligatoire pour le grant `client_credentials`.",
                    "example": "12345"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jeton émis avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                },
                "example": {
                  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                  "token_type": "bearer",
                  "expires_in": 31535999,
                  "userName": "12345",
                  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                  "application": null
                }
              }
            }
          },
          "400": {
            "description": "Identifiants invalides ou grant type non pris en charge",
            "content": {
              "application/json": {
                "examples": {
                  "invalid_grant": {
                    "summary": "Identifiants incorrects",
                    "value": {
                      "error": "invalid_grant",
                      "error_description": "The username or password is incorrect."
                    }
                  },
                  "no_api_access": {
                    "summary": "Pas d’accès à l’API",
                    "value": {
                      "error": "no_api_access",
                      "error_description": "The account doesn't have API access."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/Account/Me": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getCrmAccountMe",
        "summary": "Obtenir les détails du compte",
        "description": "Renvoie les informations de profil et les paramètres du compte Mailpro authentifié.",
        "responses": {
          "200": {
            "description": "Détails du compte renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Account/Credits": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getCrmAccountCredits",
        "summary": "Obtenir le solde de crédits",
        "description": "Renvoie le solde de crédits e-mail et SMS restant pour le compte.",
        "responses": {
          "200": {
            "description": "Solde de crédits renvoyé avec succès",
            "content": {
              "application/json": {
                "example": {
                  "email_credits": 48500,
                  "sms_credits": 1200
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Account/SubscriptionEndDate": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getCrmSubscriptionEndDate",
        "summary": "Obtenir la date de fin d’abonnement",
        "description": "Renvoie la date d’expiration de l’abonnement Mailpro en cours.",
        "responses": {
          "200": {
            "description": "Date de fin d’abonnement renvoyée avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Account/Limits": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getCrmAccountLimits",
        "summary": "Obtenir le quota d’API mensuel",
        "description": "Renvoie le quota mensuel de requêtes API pour le client authentifié.\n\n**Calcul du quota :** la limite est calibrée à partir des crédits d’abonnement\ne-mail (typiquement `subscription_credits × 5`). Pour un forfait de 10 000\ne-mails/mois, cela donne un budget de 50 000 requêtes mensuelles. La correspondance\nexacte est définie par la tâche de facturation lors de la souscription à\nl’abonnement et réinitialisée à la **date anniversaire de l’abonnement**\n(et non au 1er du mois calendaire).\n\n**Ce qui consomme des crédits :**\n- Toute requête API authentifiée vers les endpoints `v2`, `v3` ou `sms`\n  (chaque appel = 1 crédit, quel que soit le résultat — les codes 2xx et la plupart\n  des erreurs métier 4xx sont décomptés).\n- Chaque événement webhook dispatché vers un endpoint souscrit\n  (ex. `contact.created` envoyé à votre webhook souscrit = 1 crédit\n  en plus de l’appel API qui l’a déclenché).\n\n**Ce qui est GRATUIT (ne consomme pas de crédits) :**\n- Les endpoints `/health` (supervision).\n- Les requêtes qui échouent avec `401` (échec d’authentification), `403` (interdit)\n  ou `429` (limitation de débit).\n- Les événements webhook pour les clients sans souscription correspondante\n  (court-circuit en cache — aucun impact sur le quota).\n",
        "security": [
          {
            "BearerAuth": []
          },
          {
            "LegacyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Instantané du quota",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monthly_limit": {
                      "type": "integer",
                      "description": "Total des crédits alloués pour le mois de facturation en cours.",
                      "example": 50000
                    },
                    "remaining": {
                      "type": "integer",
                      "description": "Crédits encore disponibles avant la prochaine réinitialisation.",
                      "example": 32847
                    },
                    "used": {
                      "type": "integer",
                      "description": "Crédits consommés depuis la dernière réinitialisation (max(0, monthly_limit - remaining)).",
                      "example": 17153
                    },
                    "is_free": {
                      "type": "boolean",
                      "description": "True pour les comptes en formule gratuite.",
                      "example": false
                    },
                    "remained_sends_for_free": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Compteur spécifique à la formule gratuite. `null` pour les comptes payants.",
                      "example": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Aucun enregistrement de quota trouvé pour ce compte (compte nouvellement créé n’ayant jamais effectué d’appel API ; la ligne sera créée lors de la première requête authentifiée)."
          }
        }
      }
    },
    "/Contact": {
      "post": {
        "tags": [
          "Contact"
        ],
        "operationId": "createContact",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Créer un contact",
        "description": "Crée un nouveau contact CRM avec champs personnalisés, affectations à des listes et tags facultatifs.\n\n**Obligatoire :**\n- `email` — doit être unique au sein du compte\n- `lists` — au moins une affectation à une liste est obligatoire\n\n**Les numéros de téléphone** acceptent les formats `+41…`, `0041…` ou `41…` avec ou sans séparateurs\n(12 chiffres max sur Contact — voir **Format des numéros de téléphone** dans la description de l’API).\n\n**Les champs personnalisés** sont identifiés par leur `id` (issu de `GET /Field`).\nLes valeurs sont validées par rapport au type du champ.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactEntry"
              },
              "example": {
                "email": "alice@example.com",
                "phone_number": "+41791234567",
                "lists": [
                  {
                    "id": 101
                  }
                ],
                "tags": [
                  {
                    "id": 5
                  }
                ],
                "custom_fields": [
                  {
                    "id": 1,
                    "value": "Alice"
                  },
                  {
                    "id": 2,
                    "value": "Dupont"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contact créé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "400": {
            "description": "Format d’e-mail invalide"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "406": {
            "description": "Violation de règle métier (voir la description pour la liste complète)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "email_exists": {
                    "summary": "E-mail en doublon",
                    "value": {
                      "Message": "Contact cannot be created. A contact already exist with the same Email"
                    }
                  },
                  "no_list": {
                    "summary": "Aucune liste fournie",
                    "value": {
                      "Message": "Contact cannot be created. At least one List is necessary"
                    }
                  },
                  "unsubscribed_forever": {
                    "summary": "Désinscrit définitivement",
                    "value": {
                      "Message": "Contact cannot be created. The email address was unsubscribed permanently"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/Contact/Multiple": {
      "post": {
        "tags": [
          "Contact"
        ],
        "operationId": "createContactsBulk",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Importer des contacts (en masse)",
        "description": "Crée jusqu’à **1 000 contacts** en une seule requête pour un import en masse efficace.\nLa réponse inclut la liste des contacts créés avec succès et la liste des erreurs\npour les contacts n’ayant pas pu être créés (ex. e-mail en doublon, données invalides).\n\n**Remarque :** les listes et tags utilisés lors d’imports en masse ne doivent **pas**\nêtre liés à des workflows d’automatisation actifs. Si c’est le cas, l’import entier\nest rejeté avec un HTTP 406.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ContactEntry"
                },
                "maxItems": 1000
              },
              "example": [
                {
                  "email": "alice@example.com",
                  "lists": [
                    {
                      "id": 101
                    }
                  ],
                  "custom_fields": [
                    {
                      "id": 1,
                      "value": "Alice"
                    }
                  ]
                },
                {
                  "email": "bob@example.com",
                  "lists": [
                    {
                      "id": 101
                    }
                  ],
                  "custom_fields": [
                    {
                      "id": 1,
                      "value": "Bob"
                    }
                  ]
                }
              ]
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import en masse terminé (vérifiez le tableau `errors` pour les échecs partiels)",
            "content": {
              "application/json": {
                "example": {
                  "Added": [
                    {
                      "Email": "alice@example.com",
                      "Id": "550e8400-e29b-41d4-a716-446655440000"
                    },
                    {
                      "Email": "bob@example.com",
                      "Id": "660e8400-e29b-41d4-a716-446655440001"
                    }
                  ],
                  "Errors": []
                }
              }
            }
          },
          "400": {
            "description": "Entrée invalide"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "406": {
            "description": "Violation de règle métier (ex. conflit d’automatisation, limite de liste atteinte)"
          }
        }
      }
    },
    "/Contact/{id}": {
      "get": {
        "tags": [
          "Contact"
        ],
        "operationId": "getContact",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Obtenir un contact",
        "description": "Renvoie les détails d’un contact spécifique.\nUtilisez `includeTags=true` pour renvoyer également les tags affectés au contact.\nUtilisez `includeLists=true` pour renvoyer également les listes auxquelles le contact appartient.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID du contact (issu de `POST /Contact` ou `GET /List/{id}/Contact`)",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "name": "includeTags",
            "in": "query",
            "description": "Définir à `true` pour inclure les affectations de tags du contact dans la réponse",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "includeLists",
            "in": "query",
            "description": "Définir à `true` pour inclure les appartenances aux listes du contact dans la réponse",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact renvoyé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                },
                "example": {
                  "id": "550e8400-e29b-41d4-a716-446655440000",
                  "email": "alice@example.com",
                  "phone_number": "41791234567",
                  "custom_fields": [
                    {
                      "id": 1,
                      "value": "Alice"
                    },
                    {
                      "id": 2,
                      "value": "Dupont"
                    }
                  ],
                  "tags": [
                    {
                      "id": 5
                    }
                  ],
                  "lists": [
                    {
                      "id": 101
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Contact introuvable"
          }
        }
      },
      "put": {
        "tags": [
          "Contact"
        ],
        "operationId": "updateContact",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Mettre à jour un contact",
        "description": "Met à jour l’e-mail, le numéro de téléphone, les champs personnalisés, les affectations aux listes et les tags d’un contact.\nLe corps de la requête remplace les données actuelles du contact — incluez toutes les listes et tous les tags\nauxquels vous souhaitez que le contact reste affecté.\n\n**Obligatoire dans le corps :** `email` et au moins une entrée `lists`.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID du contact",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactEntry"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contact mis à jour avec succès",
            "content": {
              "application/json": {
                "example": {
                  "Result": "Successful operation"
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Contact introuvable"
          },
          "422": {
            "description": "Erreur de validation (format d’e-mail, numéro de téléphone, incohérence de type de valeur de champ)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Contact"
        ],
        "operationId": "deleteContact",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Supprimer un contact",
        "description": "Supprime définitivement un contact du CRM.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID du contact",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          }
        ],
        "responses": {
          "200": {
            "description": "Contact supprimé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Contact introuvable"
          }
        }
      }
    },
    "/Contact/{id}/Activity": {
      "get": {
        "tags": [
          "Contact"
        ],
        "operationId": "getCrmContactActivity",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Activité d'un contact sur 90 jours",
        "description": "E-mails reçus, ouverts et cliqués, messagerie préférée et dernières campagnes du contact.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "GUID du contact",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Activité renvoyée",
            "content": {
              "application/json": {
                "example": {
                  "period_days": 90,
                  "email_received": 12,
                  "email_opened": 7,
                  "email_clicked": 2,
                  "favorite_email_client": "Gmail",
                  "last_campaigns": [
                    {
                      "id": 5001,
                      "name": "Spring Sale 2026",
                      "date": "2026-10-01T09:00:00"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Le contact demandé n'existe pas"
          }
        }
      }
    },
    "/Contact/{id}/Tag/{tagid}": {
      "put": {
        "tags": [
          "Contact"
        ],
        "operationId": "addTagToContact",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Ajouter un tag à un contact",
        "description": "Affecte un tag à un contact. Si le tag est déjà affecté, l’opération est sans effet.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID du contact",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "name": "tagid",
            "in": "path",
            "required": true,
            "description": "Identifiant du tag (issu de `GET /Tag`)",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 5
          }
        ],
        "responses": {
          "200": {
            "description": "Tag affecté avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Contact ou tag introuvable"
          }
        }
      },
      "delete": {
        "tags": [
          "Contact"
        ],
        "operationId": "removeTagFromContact",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Retirer un tag d’un contact",
        "description": "Supprime l’affectation d’un tag à un contact.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID du contact",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "name": "tagid",
            "in": "path",
            "required": true,
            "description": "Identifiant du tag",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 5
          }
        ],
        "responses": {
          "200": {
            "description": "Tag retiré avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Contact ou tag introuvable"
          }
        }
      }
    },
    "/Contact/{id}/List/{listid}": {
      "put": {
        "tags": [
          "Contact"
        ],
        "operationId": "addContactToList",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Ajouter un contact à une liste",
        "description": "Ajoute un contact à une liste spécifique.\nRenvoie HTTP 404 si le contact s’est précédemment désinscrit de cette liste.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID du contact",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "name": "listid",
            "in": "path",
            "required": true,
            "description": "Identifiant de la liste (issu de `GET /List`)",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 101
          }
        ],
        "responses": {
          "200": {
            "description": "Contact ajouté à la liste avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Contact ou liste introuvable, ou contact désinscrit de cette liste"
          }
        }
      },
      "delete": {
        "tags": [
          "Contact"
        ],
        "operationId": "removeContactFromList",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Retirer un contact d’une liste",
        "description": "Retire un contact d’une liste spécifique. Le contact lui-même n’est pas supprimé.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID du contact",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "name": "listid",
            "in": "path",
            "required": true,
            "description": "Identifiant de la liste",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 101
          }
        ],
        "responses": {
          "200": {
            "description": "Contact retiré de la liste avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Contact ou liste introuvable"
          }
        }
      }
    },
    "/List": {
      "get": {
        "tags": [
          "List"
        ],
        "operationId": "getLists",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les listes de contacts",
        "description": "Renvoie toutes les listes de contacts du compte.\nFiltrez par nom ou par date de création. Triez par ordre alphabétique avec `sortByName=true`.\nIncluez le nombre total de contacts par liste avec `includeTotalContacts=true`.\n",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "description": "Filtrer par nom de liste (correspondance partielle)",
            "schema": {
              "type": "string"
            },
            "example": "newsletter"
          },
          {
            "name": "dateCreationFrom",
            "in": "query",
            "description": "Filtrer par date de création (à partir de, ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "dateCreationTo",
            "in": "query",
            "description": "Filtrer par date de création (jusqu’à, ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "description": "Éléments par page — 5 000 par défaut et au maximum",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "description": "Numéro de page basé sur 1",
            "schema": {
              "type": "integer"
            },
            "example": 1
          },
          {
            "name": "sortByName",
            "in": "query",
            "description": "Définir à `true` pour trier alphabétiquement par nom",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "includeTotalContacts",
            "in": "query",
            "description": "Définir à `true` pour inclure le décompte `total_contacts` dans chaque résultat",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Listes renvoyées avec succès",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": 101,
                    "name": "Newsletter subscribers",
                    "total_contacts": 4521,
                    "date_creation": "2025-01-15T10:30:00"
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      },
      "post": {
        "tags": [
          "List"
        ],
        "operationId": "createList",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Créer une liste de contacts",
        "description": "Crée une nouvelle liste de contacts. Maximum 1 000 listes par compte.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListEntry"
              },
              "example": {
                "name": "Spring Campaign 2026"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Liste créée avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/List"
                },
                "example": {
                  "id": 102,
                  "name": "Spring Campaign 2026",
                  "date_creation": "2026-04-14T12:00:00"
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "406": {
            "description": "Limite de listes (1 000) atteinte"
          }
        }
      }
    },
    "/List/{id}": {
      "get": {
        "tags": [
          "List"
        ],
        "operationId": "getList",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Obtenir une liste de contacts",
        "description": "Renvoie les détails d’une liste de contacts spécifique.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant de la liste",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 101
          }
        ],
        "responses": {
          "200": {
            "description": "Liste renvoyée avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Liste introuvable"
          }
        }
      },
      "put": {
        "tags": [
          "List"
        ],
        "operationId": "updateList",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Mettre à jour une liste de contacts",
        "description": "Renomme une liste de contacts existante.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant de la liste",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 101
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListEntry"
              },
              "example": {
                "name": "Newsletter subscribers — Europe"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Liste mise à jour avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Liste introuvable"
          }
        }
      },
      "delete": {
        "tags": [
          "List"
        ],
        "operationId": "deleteList",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Supprimer une liste de contacts",
        "description": "Supprime définitivement une liste de contacts.\nLes contacts eux-mêmes ne sont pas supprimés — seule leur appartenance à cette liste est retirée.\nLes listes utilisées par des workflows d’automatisation actifs ne peuvent pas être supprimées.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant de la liste",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 101
          }
        ],
        "responses": {
          "200": {
            "description": "Liste supprimée avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Liste introuvable"
          },
          "406": {
            "description": "La liste est utilisée par un workflow d’automatisation actif"
          }
        }
      }
    },
    "/List/{id}/Contact": {
      "get": {
        "tags": [
          "List"
        ],
        "operationId": "getListContacts",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les contacts d’une liste",
        "description": "Renvoie les contacts appartenant à une liste spécifique.\nFiltrez par e-mail ou date d’ajout. Incluez les tags et appartenances aux listes via les indicateurs correspondants.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant de la liste",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 101
          },
          {
            "name": "email",
            "in": "query",
            "description": "Filtrer par e-mail (correspondance partielle)",
            "schema": {
              "type": "string"
            },
            "example": "@example.com"
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "description": "Éléments par page — 5 000 par défaut et au maximum",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          },
          {
            "name": "sortByEmail",
            "in": "query",
            "description": "Trier alphabétiquement par e-mail",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "includeTags",
            "in": "query",
            "description": "Inclure les affectations de tags dans les résultats",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "includeLists",
            "in": "query",
            "description": "Inclure les appartenances aux listes dans les résultats",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contacts renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Liste introuvable ou aucun contact ne correspond aux critères"
          }
        }
      }
    },
    "/List/{listId}/Unsubscribers": {
      "get": {
        "tags": [
          "List"
        ],
        "operationId": "getListUnsubscribers",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les désinscrits",
        "description": "Renvoie les contacts qui se sont désinscrits d’une liste spécifique.",
        "parameters": [
          {
            "name": "listId",
            "in": "path",
            "required": true,
            "description": "Identifiant de la liste",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 101
          }
        ],
        "responses": {
          "200": {
            "description": "Désinscrits renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Liste introuvable"
          }
        }
      }
    },
    "/Tag": {
      "get": {
        "tags": [
          "Tag"
        ],
        "operationId": "getTags",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les tags",
        "description": "Renvoie tous les tags du compte.",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "description": "Filtrer par nom (correspondance partielle)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          },
          {
            "name": "sortByName",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tags renvoyés avec succès",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": 5,
                    "name": "VIP customers"
                  },
                  {
                    "id": 6,
                    "name": "Newsletter openers"
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      },
      "post": {
        "tags": [
          "Tag"
        ],
        "operationId": "createTag",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Créer un tag",
        "description": "Crée un nouveau tag. Maximum 1 000 tags par compte.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagEntry"
              },
              "example": {
                "name": "VIP customers"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tag créé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "406": {
            "description": "Limite de tags (1 000) atteinte"
          }
        }
      }
    },
    "/Tag/{id}": {
      "get": {
        "tags": [
          "Tag"
        ],
        "operationId": "getTag",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Obtenir un tag",
        "description": "Renvoie un tag spécifique par son identifiant.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant du tag",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 5
          }
        ],
        "responses": {
          "200": {
            "description": "Tag renvoyé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Tag introuvable"
          }
        }
      },
      "put": {
        "tags": [
          "Tag"
        ],
        "operationId": "updateTag",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Mettre à jour un tag",
        "description": "Renomme un tag existant.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 5
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagEntry"
              },
              "example": {
                "name": "VIP customers — updated"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tag mis à jour avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Tag introuvable"
          }
        }
      },
      "delete": {
        "tags": [
          "Tag"
        ],
        "operationId": "deleteTag",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Supprimer un tag",
        "description": "Supprime définitivement un tag. Le tag est retiré de tous les contacts.\nLes tags utilisés par des workflows d’automatisation actifs ne peuvent pas être supprimés.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 5
          }
        ],
        "responses": {
          "200": {
            "description": "Tag supprimé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Tag introuvable"
          },
          "406": {
            "description": "Le tag est utilisé par un workflow d’automatisation actif"
          }
        }
      }
    },
    "/Tag/{id}/Contact": {
      "get": {
        "tags": [
          "Tag"
        ],
        "operationId": "getTagContacts",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les contacts portant un tag",
        "description": "Renvoie les contacts auxquels un tag spécifique a été affecté.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant du tag",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 5
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Contacts renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Tag introuvable"
          }
        }
      }
    },
    "/Field": {
      "get": {
        "tags": [
          "Field"
        ],
        "operationId": "listFields",
        "summary": "Lister les champs personnalisés",
        "description": "Renvoie tous les champs personnalisés de contact définis pour le compte.\nUtilisez les valeurs `id` renvoyées lors de la création ou de la mise à jour de contacts.\n",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "description": "Filtrer par nom de champ",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "idFieldType",
            "in": "query",
            "description": "Filtrer par identifiant de type de champ",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          },
          {
            "name": "sortByName",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Champs personnalisés renvoyés avec succès",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": 1,
                    "name": "first_name",
                    "personalisation_key": "first_name",
                    "field_type": "text"
                  },
                  {
                    "id": 2,
                    "name": "last_name",
                    "personalisation_key": "last_name",
                    "field_type": "text"
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      },
      "post": {
        "tags": [
          "Field"
        ],
        "operationId": "createField",
        "summary": "Créer un champ personnalisé",
        "description": "Crée un nouveau champ personnalisé de contact.\nMaximum 50 champs par compte.\nUtilisez l’`id` renvoyé lors de l’affectation de valeurs aux contacts.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FieldEntry"
              },
              "example": {
                "name": "First name",
                "id_field_type": 1,
                "personalize_key": "first_name"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Champ créé avec succès",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Field"
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "406": {
            "description": "Limite de champs (50) atteinte"
          }
        }
      }
    },
    "/Field/Types": {
      "get": {
        "tags": [
          "Field"
        ],
        "operationId": "listFieldTypes",
        "summary": "Lister les types de champs",
        "description": "Renvoie les identifiants de types de champs disponibles à utiliser lors de la création d’un champ personnalisé.\nTypes courants : `1` = texte, `2` = nombre, `3` = date.\n",
        "responses": {
          "200": {
            "description": "Types de champs renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Field/{id}": {
      "get": {
        "tags": [
          "Field"
        ],
        "operationId": "getField",
        "summary": "Obtenir un champ personnalisé",
        "description": "Renvoie la définition et les métadonnées d’un champ personnalisé spécifique.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant du champ",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Champ renvoyé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Champ introuvable"
          }
        }
      },
      "put": {
        "tags": [
          "Field"
        ],
        "operationId": "updateField",
        "summary": "Mettre à jour un champ personnalisé",
        "description": "Met à jour le nom ou la clé de personnalisation d’un champ personnalisé.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 1
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FieldEntry"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Champ mis à jour avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Champ introuvable"
          }
        }
      },
      "delete": {
        "tags": [
          "Field"
        ],
        "operationId": "deleteField",
        "summary": "Supprimer un champ personnalisé",
        "description": "Supprime définitivement un champ personnalisé et retire sa valeur de tous les contacts.\nCette action est irréversible.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Champ supprimé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Champ introuvable"
          }
        }
      }
    },
    "/Campaign": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "listCrmCampaigns",
        "summary": "Lister les campagnes",
        "description": "Renvoie une liste paginée des campagnes e-mail du compte.",
        "parameters": [
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 20
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Campagnes renvoyées avec succès",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": 8001,
                    "title": "Spring Newsletter April 2026",
                    "date_planned": "2026-04-20T09:00:01",
                    "count_open": 1823,
                    "count_sent": 4521,
                    "count_click": 342,
                    "count_badmail": 12,
                    "count_unsubscription": 8,
                    "count_clicking_users": 289,
                    "count_complaints": 1,
                    "spam_trap": 0,
                    "deliverability_score": 98
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaign",
        "summary": "Obtenir une campagne",
        "description": "Renvoie la configuration et un résumé des statistiques d’une campagne spécifique.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          }
        ],
        "responses": {
          "200": {
            "description": "Campagne renvoyée avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Campagne introuvable"
          }
        }
      },
      "put": {
        "tags": [
          "Campaign"
        ],
        "operationId": "updateCrmCampaign",
        "summary": "Mettre à jour une campagne",
        "description": "Met à jour le titre ou la date d’envoi d’une campagne planifiée.\nLes campagnes ne peuvent être modifiées que jusqu’à **5 minutes avant** l’heure d’envoi planifiée.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CampaignEntry"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campagne mise à jour avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Campagne introuvable"
          }
        }
      },
      "delete": {
        "tags": [
          "Campaign"
        ],
        "operationId": "deleteCrmCampaign",
        "summary": "Supprimer une campagne",
        "description": "Annule et supprime définitivement une campagne par son identifiant.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          }
        ],
        "responses": {
          "200": {
            "description": "Campagne supprimée avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Campagne introuvable"
          }
        }
      }
    },
    "/Campaign/Scheduled": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "listScheduledCrmCampaigns",
        "summary": "Lister mes campagnes planifiées",
        "description": "Renvoie les campagnes dont la `date_planned` est dans le futur et qui n’ont pas encore été\nexpédiées. Triées par `date_planned` croissante.\n",
        "parameters": [
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 5000,
              "maximum": 5000
            }
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des campagnes planifiées",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": 5001,
                    "id_client": 12345,
                    "subject": "Spring Sale 2026",
                    "date_planned": "2026-05-01T09:00:00",
                    "title": "Spring Sale",
                    "id_campaign": 7001,
                    "id_list": 101,
                    "id_segment": null,
                    "number_email": 1250
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}/Cancel": {
      "post": {
        "tags": [
          "Campaign"
        ],
        "operationId": "cancelScheduledCrmCampaign",
        "summary": "Annuler une campagne planifiée",
        "description": "Annule une campagne planifiée dans le futur. Renvoie **409 Conflict** si la\ncampagne a déjà été expédiée ou n’existe pas.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campagne annulée",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "cancelled": 5001
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "409": {
            "description": "Déjà expédiée ou introuvable"
          }
        }
      }
    },
    "/Campaign/{id}/Reschedule": {
      "patch": {
        "tags": [
          "Campaign"
        ],
        "operationId": "rescheduleCrmCampaign",
        "summary": "Replanifier une campagne planifiée",
        "description": "Met à jour la date d’envoi planifiée d’une campagne planifiée. La nouvelle date doit être\ndans le futur et dans les 90 jours.\n\n**Nom du champ du corps :** l’API accepte `date_planned` (snake_case, principal)\nainsi que les alias historiques `PlanDate` et `scheduled_time`. Fournissez l’un\nquelconque d’entre eux — le premier non null est utilisé.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Fournissez l’un de `date_planned`, `PlanDate` ou `scheduled_time`.",
                "properties": {
                  "date_planned": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Nom de champ principal.",
                    "example": "2026-12-31T14:30:00"
                  },
                  "PlanDate": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Alias historique accepté pour compatibilité ascendante."
                  },
                  "scheduled_time": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Alias historique accepté pour compatibilité ascendante."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campagne replanifiée",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "id": 5001,
                  "date_planned": "2026-12-31T14:30:00"
                }
              }
            }
          },
          "400": {
            "description": "Date manquante — fournissez l’un de date_planned / PlanDate / scheduled_time"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "409": {
            "description": "Déjà expédiée",
            "not found": null,
            "or out of range": null
          }
        }
      }
    },
    "/Campaign/{id}/ByHour": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaignStatsByHour",
        "summary": "Statistiques par heure",
        "description": "Renvoie la répartition horaire des ouvertures et des clics pour la campagne.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          }
        ],
        "responses": {
          "200": {
            "description": "Statistiques horaires renvoyées avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}/ByDay": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaignStatsByDay",
        "summary": "Statistiques par jour",
        "description": "Renvoie la répartition quotidienne des ouvertures et des clics pour la campagne.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          }
        ],
        "responses": {
          "200": {
            "description": "Statistiques quotidiennes renvoyées avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}/Openings": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaignOpenings",
        "summary": "Lister les ouvertures d’e-mail",
        "description": "Renvoie une liste paginée des contacts ayant ouvert l’e-mail de la campagne.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          },
          {
            "name": "email",
            "in": "query",
            "description": "Filtrer par e-mail du destinataire",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des ouvertures renvoyée avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}/Clicks": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaignClicks",
        "summary": "Lister les clics sur les liens",
        "description": "Renvoie une liste paginée des contacts ayant cliqué sur un lien dans la campagne.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          },
          {
            "name": "email",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des clics renvoyée avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}/HardBounces": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaignHardBounces",
        "summary": "Lister les hard bounces",
        "description": "Renvoie les adresses e-mail ayant fait l’objet d’un hard bounce lors de cette campagne.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          },
          {
            "name": "email",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Liste des hard bounces renvoyée avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}/Links": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaignLinks",
        "summary": "Lister les liens suivis",
        "description": "Renvoie les données de performance des clics pour chaque lien suivi dans la campagne.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          },
          {
            "name": "link",
            "in": "query",
            "description": "Filtrer par URL du lien (correspondance partielle)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Statistiques de liens renvoyées avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}/OS": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaignStatsByOS",
        "summary": "Statistiques par OS / appareil",
        "description": "Renvoie la répartition des ouvertures et des clics par système d’exploitation et type d’appareil.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          }
        ],
        "responses": {
          "200": {
            "description": "Statistiques par OS renvoyées avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}/EmailClients": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaignStatsByEmailClient",
        "summary": "Statistiques par client de messagerie",
        "description": "Renvoie la répartition des ouvertures par client de messagerie (Gmail, Outlook, Apple Mail…).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          }
        ],
        "responses": {
          "200": {
            "description": "Statistiques par client de messagerie renvoyées avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Campaign/{id}/Countries": {
      "get": {
        "tags": [
          "Campaign"
        ],
        "operationId": "getCrmCampaignStatsByCountry",
        "summary": "Statistiques par pays",
        "description": "Renvoie la répartition géographique des ouvertures et des clics.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          }
        ],
        "responses": {
          "200": {
            "description": "Statistiques par pays renvoyées avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Send/Mail": {
      "post": {
        "tags": [
          "Send"
        ],
        "operationId": "sendTransactionalEmailCrm",
        "summary": "Envoyer un e-mail transactionnel",
        "description": "Envoie un e-mail transactionnel ponctuel à un destinataire unique.\n\n**Destinataire :** fournissez `email` (adresse en ligne) **OU** `id_contact` (contact CRM) — pas les deux.\nUtiliser `id_contact` permet la personnalisation automatique avec les valeurs des champs personnalisés du contact.\n\n**Contenu :** fournissez `body_html` (HTML en ligne) **OU** `id_message` (modèle enregistré) — pas les deux.\nSi les deux sont fournis, `id_message` est prioritaire.\n\n**Obligatoire :** `id_sender_email`, `subject`, et l’un de (`email` ou `id_contact`),\nainsi que l’un de (`body_html` ou `id_message`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendEmailRequest"
              },
              "example": {
                "id_sender_email": 201,
                "email": "alice@example.com",
                "subject": "Your order #12345 has shipped",
                "body_html": "<p>Hello {{first_name}}, your order is on its way!</p>",
                "reply_to": "support@yourcompany.com",
                "fields": [
                  {
                    "id": 1,
                    "value": "Alice"
                  }
                ],
                "attachments": [
                  {
                    "filename": "invoice.pdf",
                    "content": "JVBERi0xLjQKJVBERi0xLjQK..."
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "E-mail envoyé ou planifié avec succès",
            "content": {
              "application/json": {
                "example": {
                  "message": "Successful operation",
                  "id_single_send": 8001
                }
              }
            }
          },
          "400": {
            "description": "Format d’e-mail invalide ou champ obligatoire manquant"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Expéditeur, message ou contact introuvable ; ou destinataire désinscrit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "no_credits": {
                    "summary": "Crédits insuffisants",
                    "value": {
                      "Message": "Not enough credit available"
                    }
                  },
                  "unsubscribed": {
                    "summary": "Destinataire désinscrit",
                    "value": {
                      "Message": "Email address unsubscribed."
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Erreur de validation (objet trop long, corps et message tous les deux vides, etc.)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/Send/{id}/Status": {
      "get": {
        "tags": [
          "Send"
        ],
        "operationId": "getTransactionalStatusCrm",
        "summary": "Obtenir le statut de livraison d'un e-mail transactionnel",
        "description": "Renvoie le statut de livraison actuel d'un e-mail transactionnel envoyé via `POST /Send/Mail`. Transmettez l'`id_single_send` renvoyé par cet appel.\n\nValeurs possibles : `delivered` (accepté par le serveur du destinataire, voir `diag_code`), `bounced` (refusé — `bounce_type: hard` adresse invalide ou refus définitif, `soft` refus après nouvelles tentatives), `sent` (remis au serveur de messagerie, pas encore de réponse), `scheduled` (pas encore envoyé, `date_planned` dans le futur) et `queued` (en attente d'expédition). `delivered` et `bounced` sont finaux : arrêtez d'interroger. Chaque e-mail a un statut final au plus tard 48 heures après l'envoi ; interrogez toutes les 15 minutes au plus (chaque appel compte dans votre quota API) ou utilisez les webhooks `email.delivered` / `email.bounced`. Seul le client qui a envoyé l'e-mail peut lire son statut — tout autre id renvoie 404.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "L'`id_single_send` renvoyé par `POST /Send/Mail`.",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8001
          }
        ],
        "responses": {
          "200": {
            "description": "Statut de livraison actuel",
            "content": {
              "application/json": {
                "example": {
                  "id_single_send": 8001,
                  "email": "alice@example.com",
                  "status": "delivered",
                  "date_planned": "2026-04-15T14:30:00",
                  "status_date": "2026-04-15T14:30:07",
                  "diag_code": "2.0.0 (success)",
                  "bounce_type": null
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Aucun envoi transactionnel trouvé avec cet id pour ce client"
          }
        }
      }
    },
    "/Send/Campaign": {
      "post": {
        "tags": [
          "Send"
        ],
        "operationId": "sendCampaignCrm",
        "summary": "Envoyer une campagne e-mail",
        "description": "Envoie une campagne e-mail en masse à une ou plusieurs listes de contacts.\n\n**Obligatoire :**\n- `id_message` — le modèle à envoyer (issu de `GET /Message`)\n- `id_sender_email` — l’adresse expéditrice (issue de `GET /SenderEmail`)\n- `lists` — au moins une liste de contacts cible\n- `campaign_title` — libellé pour le suivi statistique\n\n**Segment :** filtrez facultativement les destinataires par un segment enregistré (`id_segment`).\nLe segment filtre les contacts au sein des listes spécifiées.\n\n**Planification :** définissez `date_planned` à une date/heure ISO 8601 future.\nLes campagnes peuvent être modifiées ou annulées jusqu’à 5 minutes avant l’heure planifiée.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendCampaignRequest"
              },
              "example": {
                "id_message": 501,
                "id_sender_email": 201,
                "lists": [
                  {
                    "id": 101
                  }
                ],
                "campaign_title": "Spring Newsletter April 2026",
                "date_planned": "2026-04-20T09:00:00"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campagne planifiée ou envoyée avec succès",
            "content": {
              "application/json": {
                "example": {
                  "Message": "Successful operation. Campaigns can be edited or deleted until 5 minutes prior to send.",
                  "IdCampaign": 8001
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "402": {
            "description": "Crédits insuffisants",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Message, expéditeur, liste ou segment introuvable ; ou crédits insuffisants",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Titre de campagne vide ou trop long (150 caractères max)"
          }
        }
      }
    },
    "/Send": {
      "get": {
        "tags": [
          "Send"
        ],
        "operationId": "getSendWaiting",
        "summary": "Lister les envois planifiés",
        "description": "Renvoie la liste des campagnes e-mail en attente d’envoi (planifiées, pas encore traitées).",
        "parameters": [
          {
            "name": "subjectMessage",
            "in": "query",
            "description": "Filtrer par objet (correspondance partielle)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Envois en attente renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Send/{id}": {
      "delete": {
        "tags": [
          "Send"
        ],
        "operationId": "deleteSendWaiting",
        "summary": "Annuler un envoi planifié",
        "description": "Annule et supprime un envoi d’e-mail planifié qui n’a pas encore été traité.\nNe peut pas annuler des envois en cours de traitement.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant d’envoi (issu de `GET /Send`)",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 3001
          }
        ],
        "responses": {
          "200": {
            "description": "Envoi planifié annulé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Envoi planifié introuvable"
          }
        }
      }
    },
    "/Send/Mail/Scheduled": {
      "get": {
        "tags": [
          "Send"
        ],
        "operationId": "listScheduledCrmMails",
        "security": [
          {
            "OAuth2": [
              "email:send"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les e-mails unitaires programmés",
        "description": "Renvoie les e-mails unitaires envoyés avec `POST /Send/Mail` et une date future, pas encore partis. Les campagnes sont listées par `GET /Campaign/Scheduled`.",
        "responses": {
          "200": {
            "description": "E-mails programmés renvoyés",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id_single_send": 880123,
                    "email": "jane@example.com",
                    "subject": "Your invoice",
                    "date_planned": "2026-12-01T09:00:00"
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Send/Mail/{id}/Cancel": {
      "post": {
        "tags": [
          "Send"
        ],
        "operationId": "cancelCrmMail",
        "security": [
          {
            "OAuth2": [
              "email:send"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Annuler un e-mail unitaire programmé",
        "description": "Annule l'e-mail tant qu'il n'est pas parti.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "`id_single_send` renvoyé par `POST /Send/Mail`",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "E-mail annulé",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "cancelled": 880123
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "409": {
            "description": "Envoi introuvable, ou déjà parti"
          }
        }
      }
    },
    "/Send/Mail/{id}/Reschedule": {
      "patch": {
        "tags": [
          "Send"
        ],
        "operationId": "rescheduleCrmMail",
        "security": [
          {
            "OAuth2": [
              "email:send"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Reprogrammer un e-mail unitaire programmé",
        "description": "Change la date d'envoi. La nouvelle date doit être future et à moins de 90 jours. `POST` est aussi accepté.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "`id_single_send` renvoyé par `POST /Send/Mail`",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "date_planned"
                ],
                "properties": {
                  "date_planned": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Nouvelle date d'envoi (ISO 8601)",
                    "example": "2026-12-15T09:00:00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "E-mail reprogrammé",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "id": 880123,
                  "date_planned": "2026-12-15T09:00:00"
                }
              }
            }
          },
          "400": {
            "description": "Date manquante"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "409": {
            "description": "Envoi introuvable, déjà parti, ou date hors limites"
          }
        }
      }
    },
    "/SenderEmail": {
      "get": {
        "tags": [
          "Sender Addresses"
        ],
        "operationId": "listCrmSenderEmails",
        "summary": "Lister les adresses expéditrices",
        "description": "Renvoie les adresses e-mail expéditrices validées configurées pour le compte.",
        "parameters": [
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Adresses expéditrices renvoyées avec succès",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": 201,
                    "email": "newsletter@yourcompany.com",
                    "sender_name": "Your Company Newsletter",
                    "is_validated": true
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      },
      "post": {
        "tags": [
          "Sender Addresses"
        ],
        "operationId": "addCrmSenderEmail",
        "security": [
          {
            "OAuth2": [
              "email:templates:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Ajouter une adresse d'expéditeur",
        "description": "Ajoute l'adresse avec les mêmes contrôles que l'application et lui envoie un e-mail de validation. Elle est utilisable une fois le lien de cet e-mail cliqué. Un compte peut avoir jusqu'à 25 expéditeurs, validés ou en attente.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "sender_name"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Adresse de l'expéditeur",
                    "example": "news@example.com"
                  },
                  "sender_name": {
                    "type": "string",
                    "description": "Nom affiché aux destinataires",
                    "example": "Example News"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Adresse ajoutée. `domain_has_mx` vaut false quand le domaine n'a pas d'enregistrement MX.",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "confirmation_sent": true,
                  "domain_has_mx": true
                }
              }
            }
          },
          "400": {
            "description": "`email` ou `sender_name` manquant"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "422": {
            "description": "Adresse refusée : invalide, déjà parmi les expéditeurs du compte, domaine de messagerie public, nom interdit, ou limite de 25 atteinte. Le message donne la raison."
          },
          "503": {
            "description": "Indisponible pour le moment, réessayez"
          }
        }
      }
    },
    "/SenderEmail/{id}/ResendValidation": {
      "post": {
        "tags": [
          "Sender Addresses"
        ],
        "operationId": "resendCrmSenderValidation",
        "security": [
          {
            "OAuth2": [
              "email:templates:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Renvoyer l'e-mail de validation",
        "description": "Pour une adresse encore en attente de validation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "`id_sender_email` de `GET /SenderEmail`",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "E-mail renvoyé",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "confirmation_sent": true
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Expéditeur introuvable"
          },
          "409": {
            "description": "Cet expéditeur est déjà validé"
          },
          "503": {
            "description": "Indisponible pour le moment, réessayez"
          }
        }
      }
    },
    "/SenderEmail/{id}/Authentication": {
      "get": {
        "tags": [
          "Sender Addresses"
        ],
        "operationId": "getCrmSenderAuthentication",
        "security": [
          {
            "OAuth2": [
              "email:templates:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Contrôler le domaine d'un expéditeur (SPF, DKIM, DMARC, MX)",
        "description": "Contrôle le domaine en direct dans le DNS et renvoie les enregistrements à publier. Lecture seule. Le DKIM s'active dans l'application : `dkim_settings_url` ouvre le bon écran.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "`id_sender_email` de `GET /SenderEmail`",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "État du domaine. `spf.recommended_record` est construit à partir de votre enregistrement actuel : publiez-le seulement si `spf.change_needed` vaut true.",
            "content": {
              "application/json": {
                "example": {
                  "domain": "example.com",
                  "spf": {
                    "valid": true,
                    "record": "v=spf1 include:spf.mailpro.com -all",
                    "recommended_record": "v=spf1 include:spf.mailpro.com -all",
                    "change_needed": false
                  },
                  "dkim": {
                    "valid": true,
                    "records": [
                      {
                        "host": "key._domainkey.example.com",
                        "value": "v=DKIM1;k=rsa;p=MIIBIjANBgkq...",
                        "validated": true
                      }
                    ]
                  },
                  "dmarc": {
                    "valid": true,
                    "record": "v=DMARC1; p=quarantine"
                  },
                  "mx": {
                    "valid": true,
                    "record": "mail.example.com"
                  },
                  "dkim_settings_url": "https://app.mailpro.com/dkim/list?d=example.com"
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Expéditeur introuvable"
          },
          "503": {
            "description": "Indisponible pour le moment, réessayez"
          }
        }
      }
    },
    "/History/Email": {
      "get": {
        "tags": [
          "History"
        ],
        "operationId": "getCrmHistoryEmail",
        "summary": "Lister l’historique des envois d’e-mails",
        "description": "Renvoie l’historique des envois d’e-mails passés pour le compte.",
        "parameters": [
          {
            "name": "subjectMessage",
            "in": "query",
            "description": "Filtrer par objet (correspondance partielle)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "idCampaign",
            "in": "query",
            "description": "Filtrer par identifiant de campagne",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "includeBodyHTML",
            "in": "query",
            "description": "Inclure le corps HTML complet dans la réponse (plus lent)",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Historique des e-mails renvoyé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/History/SMS": {
      "get": {
        "tags": [
          "History"
        ],
        "operationId": "getCrmHistorySms",
        "summary": "Lister l’historique des envois de SMS",
        "description": "Renvoie l’historique des envois de SMS passés pour le compte.",
        "parameters": [
          {
            "name": "dateSend",
            "in": "query",
            "description": "Filtrer à partir de cette date (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "dateSendEnd",
            "in": "query",
            "description": "Filtrer jusqu’à cette date (ISO 8601)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "phoneNumber",
            "in": "query",
            "description": "Filtrer par téléphone du destinataire (partiel)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Filtre par type de SMS",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "statusCode",
            "in": "query",
            "description": "Filtre par code de statut DLR",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sortByStatus",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Historique des SMS renvoyé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Suppression/Bounces": {
      "get": {
        "tags": [
          "Suppression"
        ],
        "operationId": "listCrmBounces",
        "security": [
          {
            "OAuth2": [
              "email:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les bounces du compte",
        "description": "Les plus récents d'abord. Le total est dans l'en-tête `X-Total-Count`.",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "`hard` ou `soft`",
            "schema": {
              "type": "string",
              "enum": [
                "hard",
                "soft"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Origine du bounce : `list`, `contact`, `api` ou `smtp`",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "email",
            "in": "query",
            "description": "Adresse exacte",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateFrom",
            "in": "query",
            "description": "À partir de cette date",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "dateTo",
            "in": "query",
            "description": "Jusqu'à cette date",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bounces renvoyés",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": 5521,
                    "source": "contact",
                    "type": "hard",
                    "email": "old@example.com",
                    "date": "2026-10-12T08:30:00",
                    "diag_code": "550 5.1.1 User unknown",
                    "id_list": 101
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`type` doit valoir `hard` ou `soft`"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Suppression/Bounces/{id}": {
      "delete": {
        "tags": [
          "Suppression"
        ],
        "operationId": "deleteCrmBounce",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Retirer un bounce",
        "description": "`source` et `type` sont obligatoires, tels que renvoyés par `GET /Suppression/Bounces` : un `id` seul n'est pas unique.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "`id` du bounce",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": true,
            "description": "`list`, `contact`, `api` ou `smtp`",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": true,
            "description": "`hard` ou `soft`",
            "schema": {
              "type": "string",
              "enum": [
                "hard",
                "soft"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bounce retiré",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "id": 5521,
                  "restored": false
                }
              }
            }
          },
          "400": {
            "description": "`source` ou `type` manquant"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Bounce introuvable"
          }
        }
      }
    },
    "/Suppression/Bounces/{id}/Restore": {
      "post": {
        "tags": [
          "Suppression"
        ],
        "operationId": "restoreCrmBounce",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Remettre en liste un contact en hard bounce",
        "description": "Uniquement pour un hard bounce de source `contact`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "`id` du bounce",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact remis en liste",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "id": 5521,
                  "restored": true
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Hard bounce de contact introuvable, ou adresse plus active"
          }
        }
      }
    },
    "/Suppression/Unsubscribers": {
      "get": {
        "tags": [
          "Suppression"
        ],
        "operationId": "listCrmUnsubscribers",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les désinscrits du compte",
        "description": "Les plus récents d'abord. Le total est dans l'en-tête `X-Total-Count`.",
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "description": "Origine de la désinscription : `contact`, `list`, `smtp`, `api` ou `api3`",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "email",
            "in": "query",
            "description": "Adresse exacte",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateFrom",
            "in": "query",
            "description": "À partir de cette date",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "dateTo",
            "in": "query",
            "description": "Jusqu'à cette date",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Désinscrits renvoyés",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": 9012,
                    "source": "api3",
                    "email": "stop@example.com",
                    "date": "2026-10-20T16:05:00",
                    "list_name": null,
                    "comment": "Asked by phone"
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      },
      "post": {
        "tags": [
          "Suppression"
        ],
        "operationId": "addCrmUnsubscriber",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Ajouter un désinscrit",
        "description": "Sans `id_list`, l'adresse est désinscrite pour tout le compte. Avec, de cette liste de désinscription seulement.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Adresse à désinscrire",
                    "example": "stop@example.com"
                  },
                  "comment": {
                    "type": "string",
                    "description": "Commentaire libre",
                    "example": "Asked by phone"
                  },
                  "id_list": {
                    "type": "integer",
                    "format": "int64",
                    "description": "Liste de désinscription du compte (facultatif)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "C'est fait. `already_unsubscribed` vaut true quand l'adresse l'était déjà.",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "already_unsubscribed": false
                }
              }
            }
          },
          "400": {
            "description": "Une adresse valide est obligatoire"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "`id_list` n'est pas une liste de désinscription de ce compte"
          }
        }
      }
    },
    "/Suppression/Unsubscribers/{id}": {
      "delete": {
        "tags": [
          "Suppression"
        ],
        "operationId": "deleteCrmUnsubscriber",
        "security": [
          {
            "OAuth2": [
              "contacts:write"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Retirer un désinscrit",
        "description": "`source` est obligatoire, tel que renvoyé par `GET /Suppression/Unsubscribers` : un `id` seul n'est pas unique.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "`id` du désinscrit",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": true,
            "description": "`contact`, `list`, `smtp`, `api` ou `api3`",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Désinscrit retiré",
            "content": {
              "application/json": {
                "example": {
                  "result": "ok",
                  "removed": 9012
                }
              }
            }
          },
          "400": {
            "description": "`source` est obligatoire"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Désinscrit introuvable"
          }
        }
      }
    },
    "/Segment": {
      "get": {
        "tags": [
          "Segment"
        ],
        "operationId": "getSegments",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les segments",
        "description": "Renvoie tous les segments dynamiques enregistrés pour le compte.",
        "parameters": [
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Segments renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Segment/{id}/Contact": {
      "get": {
        "tags": [
          "Segment"
        ],
        "operationId": "getCrmSegmentContacts",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Lister les contacts d'un segment",
        "description": "Avec `id_list`, renvoie seulement les contacts de cette liste qui entrent dans le segment : ceux que toucherait une campagne liste + segment. Le total est dans l'en-tête `X-Total-Count`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID du segment",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "id_list",
            "in": "query",
            "description": "Limiter à cette liste (facultatif)",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contacts renvoyés"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Segment ou liste introuvable"
          }
        }
      }
    },
    "/Segment/{id}/Count": {
      "get": {
        "tags": [
          "Segment"
        ],
        "operationId": "countCrmSegmentContacts",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Compter les contacts d'un segment",
        "description": "Le nombre seul, pour connaître la taille d'un envoi à l'avance. Avec `id_list`, compte seulement les contacts de cette liste qui entrent dans le segment.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID du segment",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "id_list",
            "in": "query",
            "description": "Limiter à cette liste (facultatif)",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Nombre de contacts",
            "content": {
              "application/json": {
                "example": {
                  "total": 1250
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Segment ou liste introuvable"
          }
        }
      }
    },
    "/Segment/{id}/contact/{contactId}/check": {
      "get": {
        "tags": [
          "Segment"
        ],
        "operationId": "checkContactInSegment",
        "security": [
          {
            "OAuth2": [
              "contacts:read"
            ]
          },
          {
            "bearerAuth": []
          },
          {
            "IdClientHeader": [],
            "ApiKeyHeader": []
          },
          {
            "IdClientQuery": [],
            "ApiKeyQuery": []
          }
        ],
        "summary": "Vérifier si un contact correspond à un segment",
        "description": "Vérifie si un contact spécifique (par GUID) correspond actuellement aux règles\nd’un segment donné. Utile pour déclencher de la logique d’automatisation à la demande.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID du segment",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 4001
          },
          {
            "name": "contactId",
            "in": "path",
            "required": true,
            "description": "GUID du contact",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "11111111-2222-3333-4444-555555555555"
          }
        ],
        "responses": {
          "200": {
            "description": "Renvoie le résultat de la correspondance"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Segment ou contact introuvable"
          }
        }
      }
    },
    "/FolderList": {
      "get": {
        "tags": [
          "Folders"
        ],
        "operationId": "getFolderLists",
        "summary": "Lister les dossiers de listes",
        "description": "Renvoie les dossiers utilisés pour organiser les listes de contacts.",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "description": "Filtrer par nom de dossier (partiel)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Dossiers de listes renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/FolderList/{id}": {
      "get": {
        "tags": [
          "Folders"
        ],
        "operationId": "getFolderListById",
        "summary": "Obtenir un dossier de listes unique",
        "description": "Renvoie les détails d’un dossier de listes par son identifiant.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant du dossier",
            "schema": {
              "type": "integer"
            },
            "example": 12
          }
        ],
        "responses": {
          "200": {
            "description": "Dossier renvoyé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Dossier introuvable"
          }
        }
      }
    },
    "/Sms/SingleSMS": {
      "post": {
        "tags": [
          "SMS"
        ],
        "operationId": "sendSingleSmsCrm",
        "summary": "Envoyer un SMS unique",
        "description": "Envoie un SMS unique vers un numéro de téléphone directement.\nUtilisez-le pour les messages transactionnels (OTP, confirmations de commande, alertes).\nLe numéro de téléphone accepte les formats `+41…`, `0041…` ou `41…` avec ou sans séparateurs —\nvoir **Format des numéros de téléphone** dans la description de l’API pour les entrées acceptées / rejetées.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SmsSingleRequest"
              },
              "example": {
                "phone_number": "+41791234567",
                "message": "Your verification code is 9473. Valid for 10 minutes."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SMS envoyé ou planifié avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "422": {
            "description": "Format de numéro de téléphone invalide"
          }
        }
      }
    },
    "/Sms/Campaign": {
      "post": {
        "tags": [
          "SMS"
        ],
        "operationId": "sendSmsCampaignCrm",
        "summary": "Envoyer une campagne SMS",
        "description": "Envoie un message SMS au champ `phone_number` de tous les contacts d’une ou plusieurs listes.\nLes contacts sans numéro de téléphone sont ignorés.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SmsCampaignRequest"
              },
              "example": {
                "lists": [
                  {
                    "id": 101
                  }
                ],
                "message": "Flash sale today — 20% off. Shop at mailpro.com",
                "date_planned": "2026-04-20T10:00:00"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campagne SMS envoyée ou planifiée avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Sms": {
      "get": {
        "tags": [
          "SMS"
        ],
        "operationId": "getSmsSends",
        "summary": "Lister l’historique des SMS",
        "description": "Renvoie l’historique des envois de SMS (à la fois unitaires et par campagne) pour le compte.",
        "parameters": [
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Historique des SMS renvoyé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Sms/{id}": {
      "delete": {
        "tags": [
          "SMS"
        ],
        "operationId": "deleteSmsSendCrm",
        "summary": "Annuler un SMS planifié",
        "description": "Annule un envoi de SMS planifié en attente.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant de l’envoi SMS",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 5001
          }
        ],
        "responses": {
          "200": {
            "description": "SMS planifié annulé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "404": {
            "description": "Envoi SMS introuvable"
          }
        }
      }
    },
    "/Sms/Scheduled": {
      "get": {
        "tags": [
          "SMS"
        ],
        "operationId": "listScheduledSmsCrm",
        "summary": "Lister les envois de SMS planifiés",
        "description": "Renvoie les envois de SMS avec une `date_planned` future (pas encore expédiés), triés par date croissante.",
        "parameters": [
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 100
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Envois de SMS planifiés renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Sms/{id}/Cancel": {
      "post": {
        "tags": [
          "SMS"
        ],
        "operationId": "cancelScheduledSmsCrm",
        "summary": "Annuler un SMS planifié",
        "description": "Annule un envoi de SMS planifié uniquement s’il n’a pas encore été expédié.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant de l’envoi SMS",
            "schema": {
              "type": "integer"
            },
            "example": 5001
          }
        ],
        "responses": {
          "200": {
            "description": "SMS planifié annulé avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "409": {
            "description": "Envoi SMS introuvable, non détenu par le client, ou déjà expédié"
          }
        }
      }
    },
    "/Sms/{id}/Reschedule": {
      "patch": {
        "tags": [
          "SMS"
        ],
        "operationId": "rescheduleScheduledSmsCrm",
        "summary": "Replanifier un SMS planifié",
        "description": "Met à jour la date d’envoi planifiée d’un SMS planifié. La nouvelle date doit être\ndans le futur et dans les 90 jours. Échoue si le SMS a déjà été expédié.\n\n**Nom du champ du corps :** l’API accepte `date_planned` (snake_case, principal)\nainsi que les alias historiques `PlanDate` et `scheduled_time`. Fournissez\nl’un quelconque d’entre eux.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Identifiant de l’envoi SMS",
            "schema": {
              "type": "integer"
            },
            "example": 5001
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Fournissez l’un de `date_planned`, `PlanDate` ou `scheduled_time`.",
                "properties": {
                  "date_planned": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Nom de champ principal.",
                    "example": "2026-05-10T14:00:00Z"
                  },
                  "PlanDate": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Alias historique accepté pour compatibilité ascendante."
                  },
                  "scheduled_time": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Alias historique accepté pour compatibilité ascendante."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SMS planifié replanifié avec succès"
          },
          "400": {
            "description": "Date manquante — fournissez l’un de date_planned / PlanDate / scheduled_time"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "409": {
            "description": "Replanification échouée (introuvable, déjà expédié ou date hors plage)"
          }
        }
      }
    },
    "/FolderMessage": {
      "get": {
        "tags": [
          "Folders"
        ],
        "operationId": "getFolderMessages",
        "summary": "Lister les dossiers de messages",
        "description": "Renvoie les dossiers utilisés pour organiser les modèles de messages e-mail.",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "description": "Filtrer par nom de dossier",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "numberOfRecords",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "pageNumber",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Dossiers de messages renvoyés avec succès"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Import/Upload": {
      "post": {
        "tags": [
          "Import"
        ],
        "operationId": "uploadImportFile",
        "summary": "Téléverser un fichier de contacts (CSV / XLSX) pour import en masse",
        "description": "Téléversez un fichier CSV ou XLSX pour importer des contacts dans une liste.\nLe fichier est stocké et mis en file d’attente pour un traitement asynchrone — le endpoint\nrenvoie immédiatement un `import_job_id`.\n\n**Type de requête :** `multipart/form-data`\n\n**Champs de formulaire obligatoires :**\n- `id_list` (entier) — identifiant de la liste cible\n- `file` (binaire) — le fichier CSV ou XLSX à téléverser\n- `json_match_fields` (chaîne, JSON) — quelle colonne du fichier va dans\n  quel champ du contact. `email_position` = colonne de l’e-mail, numérotée\n  à partir de 0 (obligatoire), `phone_position` = colonne du numéro de\n  mobile ou `-1`, `fields` = identifiants des autres champs (voir\n  `GET /Field`), `fields_position` = colonne de chacun d’eux,\n  `update_fields` = mettre à jour les contacts existants. Correspondance\n  absente ou invalide : réponse `400`, aucun import n’est créé.\n\n**Facultatif :**\n- `webhook_url` (chaîne) — URL HTTPS appelée à la fin du traitement.\n  Reçoit un payload JSON contenant le statut final (succès / échec) et les\n  nombres de lignes importées / ignorées / invalides.\n\n**Format de fichier :**\n- Encodé en UTF-8\n- La première ligne peut contenir les en-têtes (détection automatique : une\n  première ligne sans aucune adresse e-mail est ignorée)\n- Les colonnes sont associées aux champs par `json_match_fields`\n- Taille de fichier maximale : 50 Mo\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "id_list",
                  "file",
                  "json_match_fields"
                ],
                "properties": {
                  "id_list": {
                    "type": "integer",
                    "format": "int64",
                    "description": "Identifiant de la liste de destination",
                    "example": 11088
                  },
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Fichier CSV ou XLSX (UTF-8, 50 Mo max)"
                  },
                  "json_match_fields": {
                    "type": "string",
                    "description": "Correspondance JSON entre les colonnes du fichier et les champs du contact",
                    "example": "{\"email_position\":0,\"phone_position\":-1,\"fields\":[1315597,1315598],\"fields_position\":[1,2],\"update_fields\":true}"
                  },
                  "webhook_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "URL de callback invoquée à la fin du traitement",
                    "example": "https://example.com/hooks/mailpro-import"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Fichier accepté et mis en file d’attente",
            "content": {
              "application/json": {
                "example": {
                  "result": "OK",
                  "import_job_id": 14,
                  "internal_name": "230575-20260420142615-contacts.csv"
                }
              }
            }
          },
          "400": {
            "description": "Aucun fichier téléversé, id_list manquant, ou json_match_fields absent / invalide"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          },
          "500": {
            "description": "Erreur serveur ou de stockage"
          }
        }
      }
    },
    "/Webhook": {
      "servers": [
        {
          "url": "https://webhook.mailpro.com/api/v3",
          "description": "Service de gestion des webhooks (distinct de l’hôte principal de l’API)"
        }
      ],
      "get": {
        "summary": "Lister les webhooks",
        "description": "Renvoie tous les webhooks configurés sur le compte authentifié.",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Tableau de webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Webhook"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      },
      "post": {
        "summary": "Créer un webhook",
        "description": "Crée une nouvelle souscription webhook. La réponse contient un `secret` qui\nest **affiché une seule fois** — stockez-le immédiatement, côté serveur. Utilisez-le\npour vérifier la signature HMAC de chaque payload entrant.\n\n**Limites** : 20 webhooks max par compte, 50 événements max par webhook.\n",
        "tags": [
          "Webhooks"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookCreate"
              },
              "example": {
                "url": "https://example.com/hooks/mailpro",
                "events": [
                  "email.delivered",
                  "email.bounced",
                  "contact.created"
                ],
                "description": "Intégration Slack"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook créé — `secret` n’est renvoyé qu’ici",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookCreateResponse"
                },
                "example": {
                  "id": 42,
                  "url": "https://example.com/hooks/mailpro",
                  "events": [
                    "email.delivered",
                    "email.bounced",
                    "contact.created"
                  ],
                  "description": "Intégration Slack",
                  "isActive": true,
                  "consecutiveFailures": 0,
                  "autoDisabledAt": null,
                  "autoDisabledReason": null,
                  "createdAt": "2026-05-25T10:00:00Z",
                  "updatedAt": "2026-05-25T10:00:00Z",
                  "secret": "whsec_AbcD1234EfgH5678IjkL9012MnoP3456QrsT"
                }
              }
            }
          },
          "400": {
            "description": "URL invalide, liste d’événements invalide, ou limite dépassée"
          },
          "401": {
            "description": "Jeton invalide ou expiré"
          }
        }
      }
    },
    "/Webhook/{id}": {
      "servers": [
        {
          "url": "https://webhook.mailpro.com/api/v3",
          "description": "Service de gestion des webhooks"
        }
      ],
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "get": {
        "summary": "Obtenir un webhook",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Le webhook",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "404": {
            "description": "Webhook introuvable"
          }
        }
      },
      "put": {
        "summary": "Mettre à jour un webhook",
        "description": "Mise à jour partielle — seuls les champs envoyés sont modifiés.",
        "tags": [
          "Webhooks"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook mis à jour",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "400": {
            "description": "Payload invalide"
          },
          "404": {
            "description": "Webhook introuvable"
          }
        }
      },
      "delete": {
        "summary": "Supprimer un webhook",
        "description": "Supprime définitivement le webhook et tout son historique de livraisons.",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "204": {
            "description": "Supprimé"
          },
          "404": {
            "description": "Webhook introuvable"
          }
        }
      }
    },
    "/Webhook/{id}/test": {
      "servers": [
        {
          "url": "https://webhook.mailpro.com/api/v3",
          "description": "Service de gestion des webhooks"
        }
      ],
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "post": {
        "summary": "Envoyer un événement de test",
        "description": "Met en file d’attente un événement `ping` immédiat (ou n’importe quel autre type d’événement\nvia le champ `eventType` du corps) vers votre endpoint. Utile pour vérifier l’accessibilité\net la vérification de signature avant de s’appuyer sur des événements réels.\n",
        "tags": [
          "Webhooks"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "eventType": {
                    "type": "string",
                    "default": "ping",
                    "description": "Remplacer le type d’événement (doit être souscrit par ce webhook). `ping` fonctionne toujours."
                  }
                }
              },
              "example": {
                "eventType": "ping"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Événement mis en file d’attente pour expédition",
            "content": {
              "application/json": {
                "example": {
                  "deliveryId": 1234,
                  "eventId": "evt_8f3a9b2c4d1e5f6789abcdef01234567",
                  "eventType": "ping"
                }
              }
            }
          },
          "404": {
            "description": "Webhook introuvable"
          }
        }
      }
    },
    "/Webhook/{id}/rotate-secret": {
      "servers": [
        {
          "url": "https://webhook.mailpro.com/api/v3",
          "description": "Service de gestion des webhooks"
        }
      ],
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "post": {
        "summary": "Faire tourner le secret HMAC",
        "description": "Génère un nouveau secret HMAC et invalide immédiatement l’ancien.\nLe nouveau secret est renvoyé **une seule fois** dans la réponse — stockez-le. Toute\nlivraison à partir de ce moment sera signée avec le nouveau secret.\n\nUtilisez ceci si vous suspectez que votre secret a été compromis.\n",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Nouveau secret (visible une seule fois)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secret": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "secret": "whsec_XyzA9876BcdE5432FghI1098JklM7654NopQ"
                }
              }
            }
          },
          "404": {
            "description": "Webhook introuvable"
          }
        }
      }
    },
    "/Webhook/{id}/deliveries": {
      "servers": [
        {
          "url": "https://webhook.mailpro.com/api/v3",
          "description": "Service de gestion des webhooks"
        }
      ],
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        },
        {
          "in": "query",
          "name": "limit",
          "schema": {
            "type": "integer",
            "default": 50,
            "maximum": 200
          }
        },
        {
          "in": "query",
          "name": "cursor",
          "schema": {
            "type": "integer",
            "format": "int64"
          },
          "description": "Curseur de pagination (= dernier identifiant de livraison vu). Renvoie les livraisons avec `id < cursor`."
        }
      ],
      "get": {
        "summary": "Lister les tentatives de livraison",
        "description": "Renvoie l’historique des tentatives d’expédition pour ce webhook (rétention de 30 jours).\nUtile pour le débogage — quels événements ont été envoyés, codes de statut reçus,\nnombres de tentatives, messages d’erreur.\n",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Tableau des tentatives de livraison (les plus récentes en premier)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WebhookDelivery"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Webhook introuvable"
          }
        }
      }
    },
    "/Webhook/{id}/deliveries/{deliveryId}/replay": {
      "servers": [
        {
          "url": "https://webhook.mailpro.com/api/v3",
          "description": "Service de gestion des webhooks"
        }
      ],
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        },
        {
          "in": "path",
          "name": "deliveryId",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "post": {
        "summary": "Rejouer une livraison",
        "description": "Met en file d’attente une nouvelle tentative de livraison avec le même payload que l’original.\nUtile pour redéclencher une livraison ayant échoué après avoir corrigé votre endpoint.\nUne NOUVELLE ligne `Webhook_Delivery` est créée (l’original est conservé\npour audit). Le rejeu suit la politique de relance standard\n(10 s → 1 min → 5 min → 30 min → 2 h → 6 h → 24 h, puis abandon).\n",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "202": {
            "description": "Rejeu mis en file d’attente",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "new_delivery_id": {
                      "type": "integer",
                      "format": "int64",
                      "example": 12999
                    },
                    "queued_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Webhook ou livraison introuvable"
          }
        }
      }
    }
  }
}