{
  "openapi": "3.1.0",
  "info": {
    "title": "API Plumail",
    "version": "1.0.0",
    "summary": "Envoi d'e-mails, abonnés, listes de suppression et campagnes.",
    "description": "L'API publique de Plumail, logiciel d'e-mailing français.\n\nElle couvre deux usages : l'envoi d'e-mails unitaires (transactionnels) depuis votre application, et le pilotage de vos listes et campagnes — depuis du code ou depuis un agent IA, via le serveur MCP.\n\n## Se brancher en trois minutes\n\n1. **Créez une clé** dans Réglages → API de votre espace. Elle commence par `plm_live_` et porte des droits que vous choisissez.\n2. **Appelez `GET /api/v1/me`.** Il vous rend l'espace, les droits de la clé, le quota restant, les domaines **vérifiés** et les expéditeurs enregistrés. C'est ce qui dit avec quelle adresse écrire : sans lui, votre premier envoi échoue sur `from`.\n3. **Envoyez** avec `POST /api/v1/emails`.\n\n```bash\ncurl https://plumail.fr/api/v1/emails \\\n  -H \"Authorization: Bearer plm_live_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"from\": \"Votre marque <bonjour@votredomaine.fr>\",\n    \"to\": \"client@exemple.fr\",\n    \"subject\": \"Votre facture de septembre\",\n    \"html\": \"<p>La voici.</p>\"\n  }'\n```\n\n## Ce qui vaut pour tous les appels\n\n- **Adresse de base** `https://plumail.fr` ; tous les chemins commencent par `/api/v1`. Corps et réponses en JSON (`Content-Type: application/json`).\n- **Authentification** `Authorization: Bearer plm_live_…`, ou l'en-tête `X-Api-Key` si votre passerelle mange le premier.\n- **Débit** 600 requêtes par minute et par clé. Au-delà : 429, avec `Retry-After` en secondes.\n- **Erreurs** toujours la même forme — lisez `error.code` (ou `name`), jamais `message`, qui est écrit pour un humain et peut changer.\n- **Un espace par clé.** Rien ne traverse d'un espace à l'autre.\n\n## Vous venez de Resend\n\nLes champs et la réponse de `POST /api/v1/emails` sont calqués sur les siens : `reply_to`, `tags` en tableau `{name, value}`, et une erreur qui porte aussi `statusCode` / `message` / `name`. Une migration se résume à changer l'adresse de base et la clé. Une différence à connaître : la réponse est **202**, pas 200 — le message est accepté, pas encore remis.\n\n## Depuis un agent IA (MCP)\n\nLe serveur MCP est à `https://plumail.fr/api/mcp` — JSON-RPC en **POST**, transport Streamable HTTP, sans session : chaque requête porte la clé dans le même en-tête `Authorization`. Il n'ouvre aucun flux (un `GET` répond 405, ce que les clients savent lire). Les outils rappellent cette API-ci avec votre clé : mêmes droits, même quota, mêmes refus.\n\n## Cinq règles s'appliquent à tout envoi, sans exception\n\n1. `from` doit être sur un domaine vérifié de l'espace ;\n2. une adresse en liste de suppression est refusée, avec son motif — et l'appel entier échoue, jamais un envoi partiel en silence ;\n3. le quota mensuel de l'offre compte ces envois comme les campagnes ;\n4. un taux de rebonds ou de plaintes trop élevé suspend l'envoi ;\n5. un abonné ajouté par l'API reçoit une confirmation, sauf `double_opt_in: false` explicite — et c'est alors vous qui répondez du consentement.\n\n## Webhooks — Plumail vous appelle\nL'API répond quand on l'interroge ; un webhook prévient sans qu'on demande. Abonnez une adresse à vous (`POST /api/v1/webhooks`, ou Réglages → API) et Plumail y poste un JSON à chaque événement choisi.\n\n**Vérifiez la signature, toujours.** Chaque appel porte l'en-tête `Plumail-Signature: t=<horodatage unix>,v1=<hex>` où `v1` est le HMAC-SHA256 de `\"<t>.<corps brut>\"` signé avec le secret de l'abonnement. Comparez à temps constant, et refusez un `t` vieux de plus de 300 secondes — sans ce second contrôle, un appel intercepté reste rejouable indéfiniment.\n\n```js\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nfunction valide(secret, entete, corpsBrut) {\n  const [t, v1] = [/t=(\\d+)/.exec(entete)?.[1], /v1=([a-f0-9]+)/.exec(entete)?.[1]];\n  if (!t || !v1) return false;\n  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;\n  const attendu = createHmac(\"sha256\", secret).update(`${t}.${corpsBrut}`).digest(\"hex\");\n  return timingSafeEqual(Buffer.from(attendu), Buffer.from(v1));\n}\n```\n\n**Répondez 2xx en moins de quinze secondes.** Tout le reste est un échec : Plumail réessaie quatre fois (30 s, 2 min, 10 min, 1 h) puis abandonne en le journalisant. Faites votre travail en arrière-plan et répondez tout de suite — un traitement long est indiscernable d'une panne.\n\n**Un même événement peut arriver deux fois** (un réessai après une réponse perdue). `id` est stable d'une tentative à l'autre : gardez-le et ignorez les doublons.\n\nLes corps ont tous la même forme : `{ \"id\": \"evt_…\", \"type\": \"…\", \"created_at\": \"…\", \"data\": { … } }`.",
    "contact": {
      "name": "Plumail",
      "url": "https://plumail.fr/docs/api",
      "email": "bonjour@plumail.fr"
    },
    "license": {
      "name": "Conditions générales Plumail",
      "url": "https://plumail.fr/cgv"
    }
  },
  "servers": [
    {
      "url": "https://plumail.fr",
      "description": "Production. Il n'y a pas d'environnement de test séparé : essayez sur un espace à vous, avec une adresse à vous. Tous les chemins ci-dessous incluent déjà le préfixe `/api/v1`."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "E-mails",
      "description": "Envoi unitaire et suivi."
    },
    {
      "name": "Abonnés",
      "description": "Les personnes inscrites à vos listes."
    },
    {
      "name": "Suppression",
      "description": "Les adresses qui ne recevront plus rien."
    },
    {
      "name": "Campagnes",
      "description": "Brouillons, envoi et statistiques."
    },
    {
      "name": "Espace",
      "description": "À qui appartient la clé."
    },
    {
      "name": "Webhooks",
      "description": "Plumail appelle VOTRE service quand il se passe quelque chose."
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "getApiMap",
        "tags": ["Espace"],
        "summary": "La carte de l'API",
        "description": "Sans clé : liste les points d'entrée, les droits et l'adresse de la documentation.",
        "security": [],
        "responses": {
          "200": {
            "description": "La carte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Carte"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "getAccount",
        "tags": ["Espace"],
        "summary": "À qui appartient cette clé",
        "description": "Espace, droits de la clé, offre, e-mails restants ce mois-ci, domaines d'envoi et expéditeurs. À appeler en premier : c'est ce qui dit avec quelle adresse écrire. Aucun droit particulier n'est exigé.",
        "responses": {
          "200": {
            "description": "L'état de l'espace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "429": {
            "$ref": "#/components/responses/TropDAppels"
          }
        }
      }
    },
    "/api/v1/emails": {
      "post": {
        "operationId": "sendEmail",
        "tags": ["E-mails"],
        "summary": "Envoyer un e-mail",
        "description": "Droit requis : `emails:send`.\n\nRépond **202** : Amazon a accepté le message, il n'est pas encore remis. La remise se constate quelques secondes plus tard avec `GET /api/v1/emails/{id}`.\n\nUtilisez l'en-tête `Idempotency-Key` : rejouer le même appel rend la même réponse sans second envoi.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailRequest"
              },
              "examples": {
                "simple": {
                  "summary": "Un destinataire",
                  "value": {
                    "from": "Votre marque <bonjour@votredomaine.fr>",
                    "to": "client@exemple.fr",
                    "subject": "Votre facture de septembre",
                    "html": "<p>La voici, en pièce jointe de ce message.</p>"
                  }
                },
                "complet": {
                  "summary": "Plusieurs destinataires, réponse et étiquettes",
                  "value": {
                    "from": "bonjour@votredomaine.fr",
                    "to": ["marie@exemple.fr", "paul@exemple.fr"],
                    "subject": "Votre commande est prête",
                    "html": "<p>Elle vous attend.</p>",
                    "text": "Elle vous attend.",
                    "replyTo": "sav@votredomaine.fr",
                    "headers": {
                      "X-Entity-Ref-ID": "cmd-4192"
                    },
                    "tags": {
                      "type": "commande"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepté par notre fournisseur d'envoi.",
            "headers": {
              "Idempotent-Replay": {
                "description": "`true` si la réponse est celle d'un appel identique déjà traité.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Email"
                },
                "examples": {
                  "accepte": {
                    "summary": "Accepté — à relire quelques secondes plus tard",
                    "value": {
                      "id": "eml_5c1a9",
                      "object": "email",
                      "from": "bonjour@votredomaine.fr",
                      "to": ["client@exemple.fr"],
                      "subject": "Votre facture de septembre",
                      "created_at": "2026-09-18T09:41:12.000Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "402": {
            "$ref": "#/components/responses/QuotaAtteint"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "409": {
            "$ref": "#/components/responses/Conflit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          },
          "429": {
            "$ref": "#/components/responses/TropDAppels"
          },
          "502": {
            "$ref": "#/components/responses/EnvoiEchoue"
          }
        }
      }
    },
    "/api/v1/emails/{id}": {
      "get": {
        "operationId": "getEmail",
        "tags": ["E-mails"],
        "summary": "État d'un e-mail",
        "description": "Droit requis : `emails:send`. Le statut évolue après l'envoi, au rythme des retours de notre fournisseur.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "L'e-mail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/subscribers": {
      "get": {
        "operationId": "listSubscribers",
        "tags": ["Abonnés"],
        "summary": "Lister les abonnés",
        "description": "Droit requis : `subscribers:read`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "subscribed",
                "unsubscribed",
                "bounced",
                "complained"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Recherche sur l'adresse, le prénom ou le nom.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La page demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Subscriber"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          }
        }
      },
      "post": {
        "operationId": "upsertSubscriber",
        "tags": ["Abonnés"],
        "summary": "Ajouter ou mettre à jour un abonné",
        "description": "Droit requis : `subscribers:write`.\n\nPar défaut la personne entre en `pending` et reçoit un e-mail de confirmation. `double_opt_in: false` l'inscrit directement — à n'utiliser que si le consentement a été recueilli ailleurs, et c'est alors l'appelant qui en répond.\n\nUne adresse en liste de suppression, désinscrite ou revenue en rebond définitif est refusée.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubscriberRequest"
              },
              "examples": {
                "simple": {
                  "summary": "Une adresse et un prénom",
                  "value": {
                    "email": "marie@exemple.fr",
                    "firstName": "Marie"
                  }
                },
                "complet": {
                  "summary": "Avec étiquettes et champs personnalisés",
                  "value": {
                    "email": "marie@exemple.fr",
                    "firstName": "Marie",
                    "lastName": "Dupont",
                    "tags": ["client", "boutique-paris"],
                    "fields": {
                      "ville": "Ajaccio"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Créé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriberResult"
                }
              }
            }
          },
          "200": {
            "description": "L'abonné existait : il a été mis à jour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriberResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "409": {
            "$ref": "#/components/responses/Conflit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/subscribers/{email}": {
      "get": {
        "operationId": "getSubscriber",
        "tags": ["Abonnés"],
        "summary": "Une fiche d'abonné",
        "description": "Droit requis : `subscribers:read`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EmailPath"
          }
        ],
        "responses": {
          "200": {
            "description": "La fiche.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subscriber"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      },
      "delete": {
        "operationId": "unsubscribeSubscriber",
        "tags": ["Abonnés"],
        "summary": "Désinscrire",
        "description": "Droit requis : `subscribers:write`.\n\n**N'efface pas la fiche** : la personne passe en `unsubscribed` et son adresse entre en liste de suppression. Effacer la trace ferait revenir l'adresse au prochain import de fichier.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EmailPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Désinscrit.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "subscriber"
                    },
                    "email": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "const": "unsubscribed"
                    },
                    "suppressed": {
                      "type": "boolean"
                    },
                    "deleted": {
                      "type": "boolean",
                      "const": false,
                      "description": "Toujours `false` : la fiche est conservée."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/suppression": {
      "get": {
        "operationId": "listSuppressions",
        "tags": ["Suppression"],
        "summary": "Les adresses écartées",
        "description": "Droit requis : `subscribers:read`. Le champ `scope` distingue les adresses écartées pour cet espace (`organization`) de celles écartées pour toute la plateforme (`platform`).",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "email",
            "in": "query",
            "description": "Vérifier une adresse précise.",
            "schema": {
              "type": "string",
              "format": "email"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La page demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Suppression"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          }
        }
      },
      "post": {
        "operationId": "createSuppression",
        "tags": ["Suppression"],
        "summary": "Écarter une adresse",
        "description": "Droit requis : `subscribers:write`. L'abonné correspondant, s'il existe, passe en `unsubscribed` dans la même opération.\n\n**Aucun point d'entrée ne retire une adresse de cette liste**, et c'est délibéré : c'est le geste qui fait suspendre une capacité d'envoi. Il se fait à la main, dans l'espace Plumail.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email"],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "reason": {
                    "type": "string",
                    "enum": ["unsubscribe", "bounce", "complaint", "manual"],
                    "default": "manual"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Écartée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Suppression"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/campaigns": {
      "get": {
        "operationId": "listCampaigns",
        "tags": ["Campagnes"],
        "summary": "Lister les campagnes",
        "description": "Droit requis : `campaigns:read`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["draft", "scheduled", "sending", "sent", "archived"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La page demandée.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Campaign"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          }
        }
      },
      "post": {
        "operationId": "createCampaign",
        "tags": ["Campagnes"],
        "summary": "Créer un brouillon",
        "description": "Droit requis : `campaigns:write`. **Rien ne part** : l'envoi est un second geste, volontairement séparé.\n\nLe contenu s'écrit soit en blocs (`content`), soit en texte simple (`text`) : une ligne vide sépare deux paragraphes, `# ` en début de ligne fait un titre.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CampaignRequest"
              },
              "examples": {
                "texte": {
                  "summary": "Une lettre écrite en texte simple",
                  "value": {
                    "name": "Lettre de septembre",
                    "subject": "Ce que nous avons changé ce mois-ci",
                    "preheader": "Trois nouveautés, et une qu'on vous devait.",
                    "from": "bonjour@votredomaine.fr",
                    "text": "# Trois nouveautés\n\nVoici ce qui a changé ce mois-ci.\n\nBonne lecture."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Brouillon créé.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "type": "string",
                      "const": "campaign"
                    },
                    "name": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "const": "draft"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/campaigns/{id}": {
      "get": {
        "operationId": "getCampaign",
        "tags": ["Campagnes"],
        "summary": "État et statistiques",
        "description": "Droit requis : `campaigns:read`. Les taux sont calculés sur les messages délivrés, pas sur les destinataires : une adresse morte ne doit pas faire baisser le taux d'ouverture de ceux qui ont reçu.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La campagne.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Campaign"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/campaigns/{id}/stats": {
      "get": {
        "operationId": "getCampaignStats",
        "tags": ["Campagnes"],
        "summary": "Le rapport complet",
        "description": "Droit requis : `campaigns:read`. Tout ce que la fiche campagne de Plumail montre, pour l'afficher dans un autre logiciel : compteurs, taux (parts entre 0 et 1, `null` tant que rien n'est parti), courbe des 48 premières heures en huit tranches de six heures (vide si la campagne n'est pas partie), sur quoi les gens lisent (cinq premières lignes, parts entre 0 et 1, `proxiedShare` = part des ouvertures venues d'un relais de confidentialité comme Apple Mail ou Gmail), liens du plus au moins cliqué, et le HTML rendu à l'envoi (chaîne vide sinon). Les taux d'ouverture et de clic sont calculés sur les messages délivrés ; ceux de délivrance et de rebond sur les destinataires.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le rapport.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignStats"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/campaigns/{id}/send": {
      "post": {
        "operationId": "sendCampaign",
        "tags": ["Campagnes"],
        "summary": "Lancer l'envoi",
        "description": "Droit requis : `campaigns:write`. **Irréversible.**\n\n⚠️ **La campagne part à TOUS les abonnés de l'espace au statut `subscribed`** — moins ceux qui sont en liste de suppression, recalculée au dernier moment. L'API n'expose aucun ciblage par segment : pour n'écrire qu'à une partie de votre base, composez la campagne dans l'interface Plumail. Comptez vos destinataires AVANT avec le champ `subscribers` de `GET /api/v1/me`.\n\nL'appel fige la liste des destinataires et remplit la file. Les messages partent ensuite au débit autorisé par notre fournisseur : la réponse dit `sending` et un nombre `queued`, jamais `sent`. Suivez l'avancement avec `GET /api/v1/campaigns/{id}`.\n\nUne campagne déjà en `sending` ou `sent` répond 409 : on ne réexpédie pas, on duplique. Un quota mensuel insuffisant, un espace à la réputation abîmée, aucun expéditeur ou une liste vide répondent 422 avec le motif.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Destinataires figés et mis en file.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "type": "string",
                      "const": "campaign"
                    },
                    "status": {
                      "type": "string",
                      "const": "sending"
                    },
                    "queued": {
                      "type": "integer",
                      "description": "Le nombre de destinataires réellement figés dans la file."
                    },
                    "note": {
                      "type": "string",
                      "description": "Ce qu'il reste à faire, en clair. Écrit pour un humain ; n'écrivez pas de logique dessus."
                    }
                  }
                },
                "examples": {
                  "file": {
                    "summary": "Mise en file",
                    "value": {
                      "id": "cmp_8d2",
                      "object": "campaign",
                      "status": "sending",
                      "queued": 3182,
                      "note": "3182 destinataire(s) mis en file. Les messages partent ensuite au débit autorisé par notre fournisseur d'envoi ; suivez l'avancement avec GET /v1/campaigns/cmp_8d2."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          },
          "409": {
            "$ref": "#/components/responses/Conflit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "tags": ["Webhooks"],
        "summary": "Vos abonnements",
        "description": "Droit requis : `webhooks:read`. Les secrets ne figurent pas dans la liste — lisez l'abonnement pour l'obtenir.",
        "responses": {
          "200": {
            "description": "Les abonnements de l'espace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Webhook"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "tags": ["Webhooks"],
        "summary": "Créer un abonnement",
        "description": "Droit requis : `webhooks:write`. La réponse porte le `secret` — rangez-le comme un mot de passe, il sert à vérifier chaque appel.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Créé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "L'identifiant de l'abonnement.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getWebhook",
        "tags": ["Webhooks"],
        "summary": "Un abonnement, secret compris",
        "description": "Droit requis : `webhooks:read`. Le secret est lisible, contrairement à une clé d'API : il ne donne accès à rien chez nous, et un service qu'on redéploie en a besoin pour vérifier les signatures.",
        "responses": {
          "200": {
            "description": "L'abonnement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "tags": ["Webhooks"],
        "summary": "Modifier un abonnement",
        "description": "Droit requis : `webhooks:write`. Passez `enabled: false` pour suspendre sans perdre le journal.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "email.delivered",
                        "email.opened",
                        "email.clicked",
                        "email.bounced",
                        "email.complained",
                        "subscriber.created",
                        "subscriber.unsubscribed"
                      ]
                    }
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Modifié.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          },
          "422": {
            "$ref": "#/components/responses/Refuse"
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "tags": ["Webhooks"],
        "summary": "Supprimer un abonnement",
        "description": "Droit requis : `webhooks:write`. Le journal des remises part avec lui.",
        "responses": {
          "200": {
            "description": "Supprimé.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "webhook"
                    },
                    "id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/test": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "L'identifiant de l'abonnement.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "testWebhook",
        "tags": ["Webhooks"],
        "summary": "Envoyer un événement d'essai",
        "description": "Droit requis : `webhooks:write`. Même signature, mêmes en-têtes, même journal qu'un vrai événement — et la réponse de votre service est ATTENDUE, pour que vous sachiez tout de suite. Le corps porte `\"data\": { \"test\": true }`.",
        "responses": {
          "200": {
            "description": "L'essai a été tenté — lisez `status`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "webhook_test"
                    },
                    "delivery_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": ["delivered", "pending", "failed"]
                    },
                    "attempts": {
                      "type": "integer"
                    },
                    "response_status": {
                      "type": ["integer", "null"]
                    },
                    "error": {
                      "type": ["string", "null"]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/deliveries": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "L'identifiant de l'abonnement.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "listWebhookDeliveries",
        "tags": ["Webhooks"],
        "summary": "Le journal des remises",
        "description": "Droit requis : `webhooks:read`. C'est ce qui répond à « je ne reçois rien » : on y voit la tentative, le code rendu par votre service et le début de sa réponse.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Les dernières remises.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Liste"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/WebhookDelivery"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/deliveries/{deliveryId}/replay": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "L'identifiant de l'abonnement.",
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "deliveryId",
          "in": "path",
          "required": true,
          "description": "L'identifiant de la remise à rejouer.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "replayWebhookDelivery",
        "tags": ["Webhooks"],
        "summary": "Rejouer une remise",
        "description": "Droit requis : `webhooks:write`. Le corps rejoué est celui d'origine, à l'octet près — donc le même `id` d'événement. Si votre service l'avait finalement reçu, il reconnaîtra un doublon.",
        "responses": {
          "200": {
            "description": "Rejoué — lisez `status`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "webhook_delivery"
                    },
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": ["delivered", "pending", "failed"]
                    },
                    "attempts": {
                      "type": "integer"
                    },
                    "response_status": {
                      "type": ["integer", "null"]
                    },
                    "error": {
                      "type": ["string", "null"]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NonAuthentifie"
          },
          "403": {
            "$ref": "#/components/responses/Interdit"
          },
          "404": {
            "$ref": "#/components/responses/Introuvable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Une clé d'API de l'espace, créée dans Réglages → API de votre espace Plumail. Forme : `plm_live_…`.\n\n`Authorization: Bearer plm_live_…`\n\nLa clé porte des droits (`scopes`) : chaque opération dit lequel elle exige, et `GET /api/v1/me` dit ceux que porte la vôtre. Une clé vaut pour UN espace — il n'y a pas d'appel inter-espaces."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "La même clé, dans un en-tête dédié. Strictement équivalent au Bearer : certains clients d'entreprise et passerelles ne laissent pas passer `Authorization`. N'envoyez pas les deux."
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Nombre d'éléments par page, de 1 à 100.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "description": "Le `next_cursor` de la réponse précédente.",
        "schema": {
          "type": "string"
        }
      },
      "EmailPath": {
        "name": "email",
        "in": "path",
        "required": true,
        "description": "L'adresse, encodée pour l'URL.",
        "schema": {
          "type": "string",
          "format": "email"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "description": "Une valeur unique par envoi. Rejouer le même appel rend la même réponse, sans second envoi. La même clé avec un corps différent répond 409.",
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      }
    },
    "schemas": {
      "EmailRequest": {
        "type": "object",
        "required": ["from", "to", "subject"],
        "description": "Le corps de `POST /api/v1/emails`.\n\n`from`, `to` et `subject` sont obligatoires, et **au moins l'un de `html` ou `text`** — un corps vide est refusé en 422.\n\nLes clés inconnues sont ignorées en silence : un corps écrit pour Resend passe tel quel.",
        "properties": {
          "from": {
            "type": "string",
            "minLength": 3,
            "maxLength": 320,
            "description": "**Obligatoire.** Adresse sur un domaine vérifié de l'espace — sinon 422 `unverified_from_domain`, qui vous rend la liste des domaines utilisables. `GET /api/v1/me` la donne aussi, avant d'essayer. Les deux formes sont acceptées : l'adresse seule, ou « Nom <adresse@domaine.fr> » pour que le nom s'affiche dans la boîte de réception.",
            "examples": [
              "Votre marque <bonjour@votredomaine.fr>",
              "bonjour@votredomaine.fr"
            ]
          },
          "to": {
            "type": ["string", "array"],
            "description": "**Obligatoire.** Un destinataire, ou un tableau de 1 à 50 destinataires. Au-delà de 50, découpez en plusieurs appels — ou passez par une campagne. Chaque entrée accepte la forme « Nom <adresse@domaine.fr> » comme l'adresse seule, et fait de 3 à 320 caractères.\n\n⚠️ **Les destinataires se voient entre eux** : ils partent dans le même message, pas en copie cachée. Pour que chacun reçoive le sien, faites un appel par personne.",
            "oneOf": [
              {
                "title": "Un seul destinataire",
                "type": "string",
                "minLength": 3,
                "maxLength": 320,
                "examples": [
                  "client@exemple.fr",
                  "Marie Dupont <marie@exemple.fr>"
                ]
              },
              {
                "title": "Plusieurs destinataires",
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 320
                },
                "minItems": 1,
                "maxItems": 50,
                "examples": [["marie@exemple.fr", "paul@exemple.fr"]]
              }
            ],
            "examples": [
              "client@exemple.fr",
              ["marie@exemple.fr", "paul@exemple.fr"]
            ]
          },
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 998,
            "description": "**Obligatoire.** L'objet du message. Le plafond de 998 caractères est celui de la norme e-mail ; en pratique une boîte de réception en montre de 40 à 60.",
            "examples": ["Votre facture de septembre"]
          },
          "html": {
            "type": "string",
            "maxLength": 400000,
            "description": "Le corps en HTML. `html` ou `text` : au moins l'un des deux, les deux si possible — un message sans version texte est mal noté par les filtres.",
            "examples": ["<p>La voici, en pièce jointe de ce message.</p>"]
          },
          "text": {
            "type": "string",
            "maxLength": 400000,
            "description": "Le corps en texte simple, pour les clients qui n'affichent pas le HTML.",
            "examples": ["La voici, en pièce jointe de ce message."]
          },
          "replyTo": {
            "type": ["string", "array"],
            "description": "Adresse à laquelle répondront les destinataires, quand elle diffère de `from`. Une seule, ou un tableau de 1 à 10. Même forme que `to` : l'adresse seule ou « Nom <adresse> ».",
            "oneOf": [
              {
                "title": "Une adresse",
                "type": "string",
                "minLength": 3,
                "maxLength": 320,
                "examples": ["sav@votredomaine.fr"]
              },
              {
                "title": "Plusieurs adresses",
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 320
                },
                "minItems": 1,
                "maxItems": 10,
                "examples": [["sav@votredomaine.fr", "compta@votredomaine.fr"]]
              }
            ],
            "examples": ["sav@votredomaine.fr"]
          },
          "reply_to": {
            "type": ["string", "array"],
            "description": "Alias de `replyTo`, pour les clients venus de Resend. Adresse à laquelle répondront les destinataires, quand elle diffère de `from`. Une seule, ou un tableau de 1 à 10. Même forme que `to` : l'adresse seule ou « Nom <adresse> ».\n\nNe renseignez qu'un seul des deux.",
            "oneOf": [
              {
                "title": "Une adresse",
                "type": "string",
                "minLength": 3,
                "maxLength": 320,
                "examples": ["sav@votredomaine.fr"]
              },
              {
                "title": "Plusieurs adresses",
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 320
                },
                "minItems": 1,
                "maxItems": 10,
                "examples": [["sav@votredomaine.fr", "compta@votredomaine.fr"]]
              }
            ],
            "examples": ["sav@votredomaine.fr"]
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "En-têtes libres, en paires nom → valeur. Ceux qui portent l'identité du message (From, To, Reply-To, List-Unsubscribe…) sont ignorés : c'est nous qui en répondons.",
            "examples": [
              {
                "X-Entity-Ref-ID": "cmd-4192"
              }
            ]
          },
          "tags": {
            "type": ["object", "array"],
            "description": "Étiquettes libres pour votre suivi ; elles reviennent dans les notifications de remise et sur `GET /api/v1/emails/{id}`. Deux écritures au choix : un objet clé → valeur, ou un tableau `{name, value}` comme chez Resend (10 entrées au plus dans cette seconde forme).\n\nNoms et valeurs sont **réécrits, pas refusés** : tout caractère hors `A-Z a-z 0-9 _ -` devient `_`, et au-delà de 256 caractères la valeur est coupée. Un accent ou un espace passe donc sans erreur, mais pas tel quel.",
            "oneOf": [
              {
                "title": "Objet clé/valeur",
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "examples": [
                  {
                    "type": "commande",
                    "canal": "boutique"
                  }
                ]
              },
              {
                "title": "Tableau {name, value} (forme Resend)",
                "type": "array",
                "items": {
                  "type": "object",
                  "required": ["name", "value"],
                  "properties": {
                    "name": {
                      "type": "string",
                      "examples": ["type"]
                    },
                    "value": {
                      "type": "string",
                      "examples": ["commande"]
                    }
                  }
                },
                "maxItems": 10,
                "examples": [
                  [
                    {
                      "name": "type",
                      "value": "commande"
                    }
                  ]
                ]
              }
            ],
            "examples": [
              {
                "type": "commande"
              }
            ]
          }
        }
      },
      "Email": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "L'identifiant à repasser à `GET /api/v1/emails/{id}` pour connaître la remise."
          },
          "object": {
            "type": "string",
            "const": "email"
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Toujours un tableau en réponse, même si vous aviez passé une seule adresse."
          },
          "subject": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EmailDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Email"
          },
          {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "queued",
                  "sent",
                  "delivered",
                  "bounced",
                  "complained",
                  "failed"
                ],
                "description": "`queued` à l'acceptation, puis `sent`, `delivered`, `bounced`, `complained` ou `failed` au fil des retours de notre fournisseur. Comptez quelques secondes avant `delivered`."
              },
              "error": {
                "type": ["string", "null"],
                "description": "Le motif du refus, quand `status` vaut `bounced`, `complained` ou `failed`. `null` sinon."
              },
              "tags": {
                "type": ["object", "null"],
                "description": "Les étiquettes de l'envoi, après réécriture des caractères interdits.",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "sent_at": {
                "type": ["string", "null"],
                "format": "date-time"
              },
              "delivered_at": {
                "type": ["string", "null"],
                "format": "date-time"
              }
            }
          }
        ]
      },
      "SubscriberRequest": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 320,
            "description": "**Obligatoire.** Mise en minuscules à l'enregistrement.",
            "examples": ["marie@exemple.fr"]
          },
          "firstName": {
            "type": "string",
            "maxLength": 120,
            "examples": ["Marie"]
          },
          "lastName": {
            "type": "string",
            "maxLength": 120,
            "examples": ["Dupont"]
          },
          "prenom": {
            "type": "string",
            "maxLength": 120,
            "examples": ["Marie"],
            "description": "Alias de `firstName`. N'en renseignez qu'un des deux."
          },
          "nom": {
            "type": "string",
            "maxLength": 120,
            "examples": ["Dupont"],
            "description": "Alias de `lastName`. N'en renseignez qu'un des deux."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 60
            },
            "description": "Étiquettes de l'espace. Celles qui n'existent pas encore sont créées.",
            "maxItems": 20,
            "examples": [["client", "boutique-paris"]]
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            },
            "description": "Champs personnalisés, par leur clé technique. Les clés inconnues ne créent rien : elles reviennent dans `ignored_fields`.",
            "examples": [
              {
                "ville": "Ajaccio"
              }
            ]
          },
          "double_opt_in": {
            "type": "boolean",
            "default": true,
            "description": "`false` inscrit directement, sans e-mail de confirmation. L'appelant répond alors du consentement.",
            "examples": [true]
          }
        }
      },
      "Subscriber": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "subscriber"
          },
          "email": {
            "type": "string"
          },
          "first_name": {
            "type": ["string", "null"]
          },
          "last_name": {
            "type": ["string", "null"]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "subscribed",
              "unsubscribed",
              "bounced",
              "complained"
            ]
          },
          "source": {
            "type": ["string", "null"]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "confirmed_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "unsubscribed_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SubscriberResult": {
        "type": "object",
        "properties": {
          "subscriber": {
            "$ref": "#/components/schemas/Subscriber"
          },
          "updated": {
            "type": "boolean",
            "description": "`true` si l'abonné existait déjà."
          },
          "confirmation_sent": {
            "type": "boolean",
            "description": "`true` si un e-mail de confirmation est réellement parti."
          },
          "ignored_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Clés de `fields` inconnues de cet espace, écrites nulle part."
          }
        }
      },
      "Suppression": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "suppression"
          },
          "email": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "enum": ["unsubscribe", "bounce", "complaint", "manual"]
          },
          "scope": {
            "type": "string",
            "enum": ["organization", "platform"]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CampaignRequest": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 160,
            "description": "**Obligatoire.** Nom interne, jamais vu par les destinataires.",
            "examples": ["Lettre de septembre"]
          },
          "subject": {
            "type": "string",
            "maxLength": 400,
            "description": "L'objet. Peut rester vide au brouillon, mais l'envoi le refusera.",
            "examples": ["Ce que nous avons changé ce mois-ci"]
          },
          "preheader": {
            "type": "string",
            "maxLength": 400,
            "description": "Aperçu affiché après l'objet dans la boîte de réception.",
            "examples": ["Trois nouveautés, et une qu'on vous devait."]
          },
          "kind": {
            "type": "string",
            "maxLength": 40,
            "default": "newsletter",
            "description": "Votre propre étiquette de classement. Texte libre, aucune valeur imposée.",
            "examples": ["newsletter"]
          },
          "from": {
            "type": "string",
            "maxLength": 320,
            "description": "L'adresse d'un expéditeur **déjà enregistré** dans l'espace — pas n'importe quelle adresse sur un domaine vérifié, contrairement à l'envoi unitaire. Inconnue : 404, avec la liste des adresses disponibles. `GET /api/v1/me` les donne dans `senders`.",
            "examples": ["bonjour@votredomaine.fr"]
          },
          "sender_id": {
            "type": "string",
            "maxLength": 60,
            "description": "L'identifiant de l'expéditeur, si vous le connaissez (`senders[].id` de `GET /api/v1/me`). Prioritaire sur `from`.\n\nSans `from` ni `sender_id`, Plumail prend l'expéditeur par défaut de l'espace ; s'il n'y en a pas et qu'un seul expéditeur existe, celui-là. Si plusieurs existent sans défaut, la campagne est créée **sans expéditeur** et c'est l'envoi qui échouera : désignez-en un.",
            "examples": ["snd_71a"]
          },
          "text": {
            "type": "string",
            "maxLength": 200000,
            "description": "Le contenu en texte simple — la forme à préférer depuis du code ou un agent. Une ligne vide sépare deux paragraphes, `# ` en début de ligne fait un titre, `## ` un sous-titre. Ignoré si `content` est fourni.",
            "examples": [
              "# Trois nouveautés\n\nVoici ce qui a changé ce mois-ci.\n\nBonne lecture."
            ]
          },
          "content": {
            "type": "object",
            "description": "Le contenu en blocs, la forme complète de l'éditeur. N'écrivez ceci que si vous reprenez le contenu d'une campagne existante : pour composer, `text` fait le même travail sans qu'il faille fabriquer des identifiants de blocs.",
            "properties": {
              "preheader": {
                "type": "string"
              },
              "blocks": {
                "type": "array",
                "description": "Blocs de l'éditeur, chacun avec son `id` et son `type` (`heading`, `text`, …).",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        },
        "description": "Le corps de `POST /api/v1/campaigns`. Seul `name` est obligatoire — mais un brouillon sans objet ni contenu ne pourra pas être envoyé. Donnez le contenu par `text` (simple) ou par `content` (blocs), jamais les deux."
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "campaign"
          },
          "name": {
            "type": "string"
          },
          "subject": {
            "type": ["string", "null"]
          },
          "kind": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["draft", "scheduled", "sending", "sent", "archived"]
          },
          "from": {
            "type": ["string", "null"]
          },
          "scheduled_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "sent_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "schedule_error": {
            "type": ["string", "null"],
            "description": "Pourquoi un envoi programmé n'est pas parti."
          },
          "stats": {
            "type": "object",
            "properties": {
              "recipients": {
                "type": "integer"
              },
              "sent": {
                "type": "integer"
              },
              "delivered": {
                "type": "integer"
              },
              "opened": {
                "type": "integer"
              },
              "clicked": {
                "type": "integer"
              },
              "bounced": {
                "type": "integer"
              },
              "complained": {
                "type": "integer"
              },
              "open_rate": {
                "type": ["number", "null"]
              },
              "click_rate": {
                "type": ["number", "null"]
              }
            }
          }
        }
      },
      "CampaignStats": {
        "type": "object",
        "properties": {
          "campaign": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "subject": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "draft",
                  "scheduled",
                  "sending",
                  "testing",
                  "sent",
                  "archived"
                ]
              },
              "kind": {
                "type": "string"
              },
              "fromName": {
                "type": "string"
              },
              "fromEmail": {
                "type": "string"
              },
              "sentAt": {
                "type": ["string", "null"],
                "format": "date-time"
              },
              "scheduledAt": {
                "type": ["string", "null"],
                "format": "date-time"
              },
              "updatedAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "counts": {
            "type": "object",
            "properties": {
              "recipients": {
                "type": "integer"
              },
              "delivered": {
                "type": "integer"
              },
              "opened": {
                "type": "integer"
              },
              "clicked": {
                "type": "integer"
              },
              "bounced": {
                "type": "integer"
              },
              "complained": {
                "type": "integer"
              },
              "unsubscribed": {
                "type": "integer"
              }
            }
          },
          "rates": {
            "type": "object",
            "description": "Parts entre 0 et 1 ; `null` tant que le dénominateur est vide.",
            "properties": {
              "delivered": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1
              },
              "opened": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1
              },
              "clicked": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1
              },
              "bounced": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1
              }
            }
          },
          "timeline": {
            "type": "array",
            "description": "Huit tranches de six heures depuis l'envoi, `+0h` à `+42h`. Vide si la campagne n'est pas partie.",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string",
                  "example": "+6h"
                },
                "opened": {
                  "type": "integer"
                },
                "clicked": {
                  "type": "integer"
                }
              }
            }
          },
          "audience": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "description": "Ouvertures dont l'appareil est connu."
              },
              "proxiedShare": {
                "type": ["number", "null"],
                "minimum": 0,
                "maximum": 1,
                "description": "Part des ouvertures venues d'un relais de confidentialité (Apple Mail, Gmail) : reçues, pas forcément lues."
              },
              "device": {
                "type": "array",
                "description": "Cinq lignes au plus, de la plus à la moins fréquente.",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "share": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    }
                  }
                }
              },
              "os": {
                "type": "array",
                "description": "Cinq lignes au plus, de la plus à la moins fréquente.",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "share": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    }
                  }
                }
              },
              "client": {
                "type": "array",
                "description": "Cinq lignes au plus, de la plus à la moins fréquente.",
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "share": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    }
                  }
                }
              }
            }
          },
          "links": {
            "type": "array",
            "description": "Du plus au moins cliqué.",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "clicks": {
                  "type": "integer"
                }
              }
            }
          },
          "html": {
            "type": "string",
            "description": "Le HTML rendu à l'envoi ; chaîne vide tant qu'il n'existe pas."
          }
        }
      },
      "Account": {
        "type": "object",
        "description": "Ce que la clé donne accès à, et ce qu'il reste à consommer. Le premier appel à faire : il donne les domaines vérifiés et les expéditeurs enregistrés, c'est-à-dire les seules adresses avec lesquelles vos envois passeront.",
        "properties": {
          "object": {
            "type": "string",
            "const": "account"
          },
          "organization": {
            "type": "object",
            "description": "L'espace auquel la clé appartient. `null` si l'espace a été supprimé.",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              }
            }
          },
          "key": {
            "type": "object",
            "description": "La clé utilisée pour cet appel. Son secret n'est jamais rendu.",
            "properties": {
              "name": {
                "type": "string",
                "description": "Le nom que vous lui avez donné."
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "emails:send",
                    "subscribers:read",
                    "subscribers:write",
                    "campaigns:read",
                    "campaigns:write"
                  ]
                },
                "description": "Les droits de CETTE clé. Un appel hors de cette liste répond 403."
              }
            }
          },
          "plan": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "emails_per_month": {
                "type": ["integer", "null"],
                "description": "`null` = illimité."
              }
            }
          },
          "usage": {
            "type": "object",
            "properties": {
              "period": {
                "type": "string",
                "description": "Le mois en cours, `AAAA-MM`.",
                "examples": ["2026-09"]
              },
              "emails_sent": {
                "type": "integer",
                "description": "Envois de ce mois, API et campagnes confondus."
              },
              "emails_remaining": {
                "type": ["integer", "null"],
                "description": "`null` = illimité. À 0, les envois répondent 402."
              }
            }
          },
          "subscribers": {
            "type": "integer",
            "description": "Abonnés au statut `subscribed` — ceux qu'une campagne toucherait."
          },
          "sending_domains": {
            "type": "array",
            "description": "Les domaines de l'espace. Seuls ceux à `verified: true` peuvent servir de `from`.",
            "items": {
              "type": "object",
              "properties": {
                "domain": {
                  "type": "string",
                  "examples": ["votredomaine.fr"]
                },
                "verified": {
                  "type": "boolean"
                }
              }
            }
          },
          "senders": {
            "type": "array",
            "description": "Les expéditeurs enregistrés. Une CAMPAGNE ne peut partir que de l'un d'eux ; un e-mail unitaire, lui, accepte n'importe quelle adresse sur un domaine vérifié.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "À passer en `sender_id` à la création d'une campagne."
                },
                "from": {
                  "type": "string",
                  "format": "email"
                },
                "name": {
                  "type": ["string", "null"],
                  "description": "Le nom affiché dans la boîte de réception."
                },
                "default": {
                  "type": "boolean",
                  "description": "Celui qu'une campagne prend si vous n'en désignez aucun."
                }
              }
            }
          }
        },
        "examples": [
          {
            "object": "account",
            "organization": {
              "id": "org_3f9",
              "name": "Votre marque",
              "slug": "votre-marque"
            },
            "key": {
              "name": "Production",
              "scopes": ["emails:send", "subscribers:read"]
            },
            "plan": {
              "id": "pro",
              "name": "Pro",
              "emails_per_month": 50000
            },
            "usage": {
              "period": "2026-09",
              "emails_sent": 1240,
              "emails_remaining": 48760
            },
            "subscribers": 3182,
            "sending_domains": [
              {
                "domain": "votredomaine.fr",
                "verified": true
              }
            ],
            "senders": [
              {
                "id": "snd_71a",
                "from": "bonjour@votredomaine.fr",
                "name": "Votre marque",
                "default": true
              }
            ]
          }
        ]
      },
      "Liste": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "has_more": {
            "type": "boolean",
            "description": "Encore une page après celle-ci."
          },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "À repasser en `?cursor=` pour la page suivante. `null` sur la dernière page."
          }
        },
        "description": "L'enveloppe de toutes les listes. Pour parcourir : tant que `has_more` vaut `true`, rappelez la même adresse avec `?cursor=<next_cursor>`. L'ordre est stable ; une page rend au plus `limit` éléments (50 par défaut, 100 au plus)."
      },
      "Erreur": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "missing_api_key",
                  "invalid_api_key",
                  "revoked_api_key",
                  "insufficient_scope",
                  "rate_limit_exceeded",
                  "validation_error",
                  "unverified_from_domain",
                  "suppressed_recipient",
                  "quota_exceeded",
                  "reputation_blocked",
                  "not_found",
                  "conflict",
                  "idempotency_key_reused",
                  "send_failed",
                  "internal_error"
                ],
                "description": "Écrivez votre code contre ce champ, jamais contre `message`."
              },
              "message": {
                "type": "string",
                "description": "Écrit pour être lu par un humain ; peut être reformulé sans préavis."
              },
              "details": {
                "type": "object",
                "description": "De quoi corriger sans deviner : `issues` (le champ fautif et pourquoi) sur une erreur de validation, `verifiedDomains` sur un domaine refusé, `available` sur un expéditeur inconnu.",
                "additionalProperties": true
              }
            }
          },
          "statusCode": {
            "type": "integer",
            "examples": [422]
          },
          "message": {
            "type": "string",
            "description": "Le même texte que `error.message` — écrit pour un humain."
          },
          "name": {
            "type": "string",
            "enum": [
              "missing_api_key",
              "invalid_api_key",
              "revoked_api_key",
              "insufficient_scope",
              "rate_limit_exceeded",
              "validation_error",
              "unverified_from_domain",
              "suppressed_recipient",
              "quota_exceeded",
              "reputation_blocked",
              "not_found",
              "conflict",
              "idempotency_key_reused",
              "send_failed",
              "internal_error"
            ],
            "description": "La même valeur que `error.code`. Écrivez votre logique contre lui."
          }
        },
        "description": "Deux lectures du même contenu. `error` est la forme de Plumail, structurée. `statusCode`, `message` et `name` reprennent la forme de Resend, pour que du code écrit contre cette API-là affiche un message juste sans être relu. `name` porte la même valeur que `error.code`, et `message` le même texte que `error.message`.",
        "examples": [
          {
            "error": {
              "code": "unverified_from_domain",
              "message": "Le domaine « exemple.fr » n'est pas vérifié dans cet espace. Domaines vérifiés : votredomaine.fr.",
              "details": {
                "from": "bonjour@exemple.fr",
                "verifiedDomains": ["votredomaine.fr"]
              }
            },
            "statusCode": 422,
            "message": "Le domaine « exemple.fr » n'est pas vérifié dans cet espace. Domaines vérifiés : votredomaine.fr.",
            "name": "unverified_from_domain"
          }
        ]
      },
      "Carte": {
        "type": "object",
        "description": "Le sommaire de l'API, lisible sans clé.",
        "properties": {
          "name": {
            "type": "string",
            "examples": ["Plumail API"]
          },
          "version": {
            "type": "string",
            "examples": ["1"]
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "format": "uri",
            "description": "Ce document."
          },
          "mcp": {
            "type": "string",
            "format": "uri",
            "description": "L'adresse du serveur MCP."
          },
          "authentication": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string"
              },
              "header": {
                "type": "string"
              },
              "where": {
                "type": "string"
              }
            }
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "emails:send",
                "subscribers:read",
                "subscribers:write",
                "campaigns:read",
                "campaigns:write"
              ]
            },
            "description": "Tous les droits qu'une clé peut porter."
          },
          "rate_limit": {
            "type": "string",
            "examples": ["600 requêtes par minute et par clé"]
          },
          "endpoints": {
            "type": "array",
            "description": "Chaque point d'entrée et le droit qu'il exige.",
            "items": {
              "type": "object",
              "properties": {
                "method": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                },
                "scope": {
                  "type": "string",
                  "description": "`—` si aucun droit particulier n'est exigé."
                }
              }
            }
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "webhook"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Le nom que vous lui donnez."
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "email.delivered",
                "email.opened",
                "email.clicked",
                "email.bounced",
                "email.complained",
                "subscriber.created",
                "subscriber.unsubscribed"
              ]
            }
          },
          "enabled": {
            "type": "boolean"
          },
          "secret": {
            "type": "string",
            "description": "Le secret de signature (`whsec_…`). Rendu à la création, à la lecture d'un abonnement précis et après un changement de secret — **jamais dans la liste**, qu'on journalise volontiers en entier."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookRequest": {
        "type": "object",
        "required": ["name", "url", "events"],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 80,
            "example": "Mon CRM"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "`https` obligatoire, et jamais une adresse de réseau privé.",
            "example": "https://mon-service.fr/plumail/evenements"
          },
          "events": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "enum": [
                "email.delivered",
                "email.opened",
                "email.clicked",
                "email.bounced",
                "email.complained",
                "subscriber.created",
                "subscriber.unsubscribed"
              ]
            }
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "webhook_delivery"
          },
          "id": {
            "type": "string"
          },
          "webhook_id": {
            "type": "string"
          },
          "event_id": {
            "type": "string",
            "description": "Stable d'une tentative à l'autre : c'est la clé de déduplication."
          },
          "event_type": {
            "type": "string",
            "enum": [
              "email.delivered",
              "email.opened",
              "email.clicked",
              "email.bounced",
              "email.complained",
              "subscriber.created",
              "subscriber.unsubscribed"
            ]
          },
          "status": {
            "type": "string",
            "enum": ["pending", "delivered", "failed"]
          },
          "attempts": {
            "type": "integer"
          },
          "response_status": {
            "type": ["integer", "null"],
            "description": "Le code HTTP rendu par votre service."
          },
          "response_body": {
            "type": ["string", "null"],
            "description": "Les 500 premiers caractères de sa réponse."
          },
          "error": {
            "type": ["string", "null"]
          },
          "next_attempt_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "delivered_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "responses": {
      "NonAuthentifie": {
        "description": "Clé absente, inconnue ou révoquée.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Interdit": {
        "description": "La clé n'a pas le droit demandé, ou les envois de l'espace sont suspendus.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Introuvable": {
        "description": "L'objet n'existe pas dans cet espace.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Conflit": {
        "description": "État incompatible, ou clé d'idempotence réutilisée avec un corps différent.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "Refuse": {
        "description": "Champ mal formé, domaine non vérifié, ou destinataire en liste de suppression.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "QuotaAtteint": {
        "description": "Le quota mensuel de l'offre est atteint.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "TropDAppels": {
        "description": "Plus de 600 requêtes par minute pour cette clé.",
        "headers": {
          "Retry-After": {
            "description": "Secondes à attendre.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      },
      "EnvoiEchoue": {
        "description": "Notre fournisseur d'envoi a refusé le message.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Erreur"
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "La documentation rédigée, avec les exemples par langage.",
    "url": "https://plumail.fr/docs/api"
  }
}
