# email.messages.upsert

Ajouter une variante texte ou modifier une variante existante en texte, HTML assaini ou blocs du constructeur. Pour format et blocs, transmettre variante_id.

POST `/api/mcp/v1/email.messages.upsert` · contrat 2.0.0 · OAuth `mcp:write` · **aucune offre requise**.

Outil MCP : [`prescriptio_campagne`](/docs/api/prescriptio_campagne.md) avec action=variante ; même handler métier.

## Entrée

```json
{
  "additionalProperties": false,
  "properties": {
    "blocs": {
      "description": "variante avec format=builder : blocs dans l'ordre. Images HTTPS, boutons HTTPS ou [lien1] à [lien3].",
      "items": {
        "additionalProperties": false,
        "properties": {
          "kind": {
            "enum": [
              "titre",
              "texte",
              "image",
              "bouton",
              "separateur"
            ],
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "url": {
            "type": "string"
          }
        },
        "required": [
          "kind"
        ],
        "type": "object"
      },
      "maxItems": 30,
      "minItems": 1,
      "type": "array"
    },
    "campagne_id": {
      "description": "UUID de la campagne. Requis partout sauf lister, creer, stats (global), desinscrits, expediteur et quota.",
      "type": "string"
    },
    "corps": {
      "description": "creer/variante : le message. Memes champs. Pour un lien mesure, ecrire [lien1], [lien2] et les declarer par action liens.",
      "type": "string"
    },
    "etiquette": {
      "description": "variante : etiquette courte (B, C, « version courte »). Avec objet et corps, ajoute une variante ; avec variante_id, reecrit celle-ci.",
      "type": "string"
    },
    "format": {
      "description": "variante : format d'une version existante. html est assaini ; builder compile les blocs comme l'interface.",
      "enum": [
        "texte",
        "html",
        "builder"
      ],
      "type": "string"
    },
    "objet": {
      "description": "creer/variante : objet du message. Champs : [prenom] [nom] [entreprise] [ville] [fonction] (l'accent est optionnel : [prenom] et [prénom] valent pareil, [societe] vaut [entreprise]).",
      "type": "string"
    },
    "variante_id": {
      "description": "variante : UUID d'une variante existante a reecrire.",
      "type": "string"
    }
  },
  "required": [
    "campagne_id"
  ],
  "type": "object"
}
```

## Exemple synthétique

```json
{
  "blocs": [
    {
      "kind": "titre",
      "text": "Bonjour [prenom]"
    },
    {
      "kind": "texte",
      "text": "Découvrez notre équipe."
    },
    {
      "kind": "bouton",
      "text": "En savoir plus",
      "url": "[lien1]"
    }
  ],
  "campagne_id": "00000000-0000-0000-0000-000000000001",
  "format": "builder",
  "objet": "Bonjour [prenom]",
  "variante_id": "00000000-0000-0000-0000-000000000002"
}
```

## Sortie

```json
{
  "properties": {
    "action": {
      "enum": [
        "variante_ajoutee",
        "variante_modifiee",
        "message_enregistre"
      ],
      "type": "string"
    },
    "fields": {
      "properties": {
        "format": {
          "enum": [
            "texte",
            "html",
            "builder"
          ],
          "type": "string"
        },
        "variante_id": {
          "type": "string"
        }
      },
      "required": [
        "variante_id"
      ],
      "type": "object"
    },
    "note": {
      "type": "string"
    },
    "ok": {
      "const": true
    },
    "url": {
      "type": "string"
    }
  },
  "required": [
    "ok",
    "action",
    "fields"
  ],
  "type": "object"
}
```

## Erreurs

Le connecteur MCP renvoie une erreur JSON-RPC pour ce qui empêche l'appel, et une erreur *métier* (`isError`) lisible par le modèle pour ce que l'appel n'a pas pu faire.

| Code JSON-RPC | Quand | Que faire |
|---|---|---|
| `-32601` | Outil inconnu | Appeler `tools/list` ; ne pas deviner un nom |
| `-32602` | Argument absent ou hors schéma | Corriger l'argument nommé dans le message |
| `-32003` | Portée OAuth absente de l'autorisation | Reconnecter en acceptant `mcp:read` ou `mcp:write` |
| `-32004` | Quota dépassé | Attendre le délai indiqué ; ne jamais boucler |
| `-32603` | Dépendance indisponible | Réessayer plus tard ; ne pas rejouer une écriture sans vérifier |

Sur les routes REST `/api/mcp/v1/*`, les mêmes situations sont des codes HTTP : `400 invalid_arguments` / `invalid_cursor`, `401 unauthenticated`, `403 insufficient_scope` / `access_denied` / `user_required` / `organization_required` / `role_denied`, `404 not_found`, `409 conflict`, `429 quota_exceeded` (avec `Retry-After` quand il est connu), `503 unavailable`, `504 timeout`. Le corps porte `error.code`, `error.message`, `error.retry_after_seconds` et `api_contract_version`.

⚠ Un message d'erreur n'est jamais une donnée : ne pas le recopier comme un fait, et ne pas inventer un identifiant ou une URL qu'il ne contient pas.

## Quotas

Un appel compte pour **une unité** dans les deux compteurs d'appels, partagés entre le connecteur MCP et les routes REST :

| Compteur | Compte gratuit | Abonné | Accès interne |
|---|---|---|---|
| Appels / 24 h glissantes | 100 | 5000 | illimité |
| Appels / 60 s (anti-rafale) | 5 | 30 | illimité |
| Dossiers distincts / jour civil (Paris) | 5 | 500 | illimité |

Cet outil ne consomme aucune unité de dossier : chercher est gratuit, c'est la lecture d'un dossier (`prescriptio_dce`, `prescriptio_mairies` et leurs téléchargements) qui est décomptée.

**Aucun outil n'est réservé à une offre** (décision du 2026-09-17) : un compte gratuit voit et appelle le catalogue entier, ce sont ces plafonds qui bornent son usage.

[Authentification, quotas, pagination et erreurs](/docs/reference/concepts.md). Les propriétés additionnelles des résultats historiques restent autorisées ; ce schéma n'invente pas les champs manquants.
