# API e-mails

Les routes d’envoi créent un e-mail et le mettent dans la file. Il part dans la seconde qui suit, sauf quand Amazon SES demande de ralentir.

Les routes partent de `https://api.mail.hgdc.ch` (voir [Référence de l’API](/docs/reference/api/)) et demandent toutes `Authorization: Bearer am_…`.

## POST `/emails`

Envoie un e-mail à un ou plusieurs destinataires. Le contenu vient de `html`, de `text`, ou d’un modèle.

### Paramètres

| Champ         | Type                | Obligatoire                         | Description                                                                                                                                                                                       |
| ------------- | ------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from`        | string              | Oui, sauf avec `template`           | Expéditeur, sous la forme `Nom <adresse>` ou `adresse`. Son domaine doit être vérifié dans ton espace, sous-domaines compris.                                                                     |
| `to`          | string ou string\[] | Oui                                 | Destinataire ou destinataires. 50 adresses au plus avec `cc` et `bcc`.                                                                                                                            |
| `subject`     | string              | Oui, sauf avec `template`           | Objet, 998 caractères au plus.                                                                                                                                                                    |
| `html`        | string              | Oui, sauf avec `text` ou `template` | Contenu HTML, 512 000 caractères au plus.                                                                                                                                                         |
| `text`        | string              | Oui, sauf avec `html` ou `template` | Contenu texte, 512 000 caractères au plus.                                                                                                                                                        |
| `template`    | objet               | Non                                 | Un modèle et ses valeurs : `{ "id": "…", "variables": { … } }`. Ne se combine pas avec `html` ni `text`. Voir [Envoi avec un modèle](#envoi-avec-un-modele).                                      |
| `cc`          | string ou string\[] | Non                                 | Destinataires en copie.                                                                                                                                                                           |
| `bcc`         | string ou string\[] | Non                                 | Destinataires en copie cachée.                                                                                                                                                                    |
| `reply_to`    | string ou string\[] | Non                                 | Adresse de réponse.                                                                                                                                                                               |
| `headers`     | objet               | Non                                 | En-têtes supplémentaires, sous forme de paires texte à texte.                                                                                                                                     |
| `attachments` | tableau             | Non                                 | Pièces jointes, 40 Mo au plus par e-mail une fois encodées en base64. Voir [Pièces jointes](#pieces-jointes).                                                                                     |
| `tags`        | tableau             | Non                                 | Au plus 10 paires `{ "name": "…", "value": "…" }`. Chaque nom et valeur ne contient que des lettres, des chiffres, `_` ou `-`, et tient en 256 caractères au plus. Le nom `email_id` est réservé. |

**Pas encore pris en charge** : `scheduled_at`, `react` et `topic_id`. Une requête qui les contient reçoit `422`, pour qu’un envoi programmé ne disparaisse pas en silence.

En-tête optionnel : `Idempotency-Key`, pour éviter un doublon en cas de nouvel essai. Voir [Envoyer sans doublon](/docs/guides/envoyer-sans-doublon/).

### Exemple

```bash
curl -X POST https://api.mail.hgdc.ch/emails \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: facture-118" \
  -d '{
    "from": "Factures <factures@envoi.atelier.ch>",
    "to": "client@exemple.ch",
    "subject": "Ta facture d'\''octobre",
    "html": "<p>Merci pour ta commande.</p>",
    "tags": [{ "name": "type", "value": "facture" }]
  }'
```

```ts
const response = await fetch("https://api.mail.hgdc.ch/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AGENTMAIL_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "facture-118",
  },
  body: JSON.stringify({
    from: "Factures <factures@envoi.atelier.ch>",
    to: "client@exemple.ch",
    subject: "Ta facture d'octobre",
    html: "<p>Merci pour ta commande.</p>",
    tags: [{ name: "type", value: "facture" }],
  }),
})
const { id } = await response.json()
```

```ts
import { Agentmail } from "./agentmail"

const mail = new Agentmail()
const { id } = await mail.emails.send(
  {
    from: "Factures <factures@envoi.atelier.ch>",
    to: "client@exemple.ch",
    subject: "Ta facture d'octobre",
    html: "<p>Merci pour ta commande.</p>",
    tags: [{ name: "type", value: "facture" }],
  },
  { idempotencyKey: "facture-118" },
)
```

### Réponse

Statut `200`. L’`id` sert à relire l’e-mail avec `GET /emails/:id`.

```json
{
  "id": "kd7b3m9q2x8r4v6n1c0z5h7t"
}
```

### Pièces jointes

Chaque élément de `attachments` suit la forme de Resend :

| Champ          | Type   | Obligatoire | Description                                                                                                     |
| -------------- | ------ | ----------- | --------------------------------------------------------------------------------------------------------------- |
| `filename`     | string | Oui         | Nom du fichier tel que le destinataire le voit. Son extension donne le type quand `content_type` manque.        |
| `content`      | string | Oui         | Le fichier encodé en base64.                                                                                    |
| `content_type` | string | Non         | Type du fichier, par exemple `application/pdf` ou `text/calendar; method=REQUEST` pour une invitation d’agenda. |
| `content_id`   | string | Non         | Pour une image dans le corps de l’e-mail : le HTML l’appelle par `<img src="cid:…">`. Moins de 128 caractères.  |

```json
{
  "from": "Factures <factures@ton-domaine.ch>",
  "to": "client@exemple.ch",
  "subject": "Votre facture",
  "html": "<p>Votre facture est jointe.</p>",
  "attachments": [
    { "filename": "facture-118.pdf", "content": "JVBERi0xLjQK…" }
  ]
}
```

- 40 Mo au plus par e-mail, comptés après encodage en base64, et 100 pièces jointes au plus.
- Les fichiers exécutables sont refusés (`.exe`, `.bat`, `.js`, `.msi`, `.vbs`, `.ps1`…, même liste que Resend) : `422` avec `name: "invalid_attachment"`.
- `path` (un fichier à télécharger depuis une adresse) n’est pas encore pris en charge : envoie le fichier dans `content`.
- Mail garde le fichier jusqu’à l’envoi, puis l’efface. Le journal ne montre que son nom, son type et sa taille.

### Envoi avec un modèle

Au lieu de `html` et `text`, passe un modèle et ses valeurs. Le modèle se crée dans l’application, l’API le lit (voir [API modèles](/docs/reference/api-modeles/)).

```bash
curl -X POST https://api.mail.hgdc.ch/emails \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "lea@exemple.ch",
    "template": {
      "id": "k57w8c2n5d9f1h6j3m0p4q7r",
      "variables": { "entreprise": "Atelier Nord" }
    }
  }'
```

```ts
import { Agentmail } from "./agentmail"

const mail = new Agentmail()
const { id } = await mail.emails.send({
  to: "lea@exemple.ch",
  template: {
    id: "k57w8c2n5d9f1h6j3m0p4q7r",
    variables: { entreprise: "Atelier Nord" },
  },
})
```

La réponse est la même que pour un envoi direct : `{ "id": "…" }`.

- L’expéditeur et l’objet du modèle servent par défaut. Envoie `from` ou `subject` pour les remplacer.
- Une variable sans valeur ni repli bloque l’envoi : rien ne part, et la réponse est un `422` qui nomme la variable, par exemple `template.variables : valeur manquante pour prenom`.
- Un modèle sans contenu, ou inconnu de ton espace, renvoie `422`.
- Si `to` contient une seule adresse et que c’est déjà un contact, ses champs servent de valeurs : `prenom`, `nom`, `email`, puis ses propriétés. Les valeurs de `variables` l’emportent sur elles.

Les variables s’écrivent `{{prenom}}`, avec une valeur de secours après une barre verticale : `{{prenom|à toi}}`. La liste complète est dans [Variables](/docs/reference/variables/).

### Lien de désabonnement

Mets `{{lien_desabonnement}}` dans le HTML ou le texte d’un e-mail envoyé à **une seule adresse** dans `to`. À l’envoi, la variable est remplacée par le lien propre au destinataire, et les en-têtes `List-Unsubscribe` sont ajoutés. Les boîtes de réception affichent alors un bouton de désabonnement.

Sans cette variable, l’e-mail n’a pas de lien de désabonnement : c’est normal pour un e-mail transactionnel. Un envoi à plusieurs adresses dans `to` n’en reçoit pas non plus.

Le lien passe par `GET /unsubscribe?token=…`, qui redirige vers la page de désabonnement de Mail. `POST /unsubscribe?token=…` sert au désabonnement en un clic, demandé par les boîtes de réception.

### Mention de pied de page

Chaque e-mail envoyé par l’API porte la mention « Propulsé par HGDC », en HTML comme en texte. Elle est ajoutée à l’envoi, et ni la clé ni le modèle ne peuvent la retirer.

### Liens et suivi

Les liens d’un e-mail envoyé par l’API arrivent tels que tu les as écrits : aucune redirection de suivi, pour qu’un lien d’invitation ou de connexion s’ouvre aussi chez qui bloque le pistage. Les ouvertures sont mesurées, pas les clics : un e-mail de l’API ne passe jamais au statut `clicked`.

## POST `/emails/batch`

Envoie de 1 à 100 e-mails en une requête.

Le corps est un tableau de 1 à 100 objets, chacun au format de `POST /emails`, pièces jointes comprises (42,5 Mo au plus pour tout le lot). Le lot est tout ou rien : soit tous les e-mails sont créés, soit aucun.

Si un élément est invalide (`422`), si le débit ou le quota du jour est atteint (`429`) ou si un domaine n’est pas vérifié, aucun e-mail du lot n’est créé. Tu peux relancer le même lot sans risque. Avec une `Idempotency-Key`, chaque élément reçoit sa propre clé (`<clé>:<position>`) : un lot déjà accepté, rejoué, renvoie les mêmes `id` sans second envoi.

```bash
curl -X POST https://api.mail.hgdc.ch/emails/batch \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: factures-octobre" \
  -d '[
    {
      "from": "Factures <factures@envoi.atelier.ch>",
      "to": "client@exemple.ch",
      "subject": "Ta facture 118",
      "html": "<p>Merci pour ta commande.</p>"
    },
    {
      "from": "Factures <factures@envoi.atelier.ch>",
      "to": "lea@exemple.ch",
      "subject": "Ta facture 119",
      "html": "<p>Merci pour ta commande.</p>"
    }
  ]'
```

```ts
import { Agentmail } from "./agentmail"

const mail = new Agentmail()
const { data } = await mail.emails.batch(
  [
    {
      from: "Factures <factures@envoi.atelier.ch>",
      to: "client@exemple.ch",
      subject: "Ta facture 118",
      html: "<p>Merci pour ta commande.</p>",
    },
    {
      from: "Factures <factures@envoi.atelier.ch>",
      to: "lea@exemple.ch",
      subject: "Ta facture 119",
      html: "<p>Merci pour ta commande.</p>",
    },
  ],
  { idempotencyKey: "factures-octobre" },
)
```

Réponse : statut `200`, avec un `id` par élément, dans l’ordre du tableau.

```json
{
  "data": [
    { "id": "kd7b3m9q2x8r4v6n1c0z5h7t" },
    { "id": "kf2n8w4c6r1x9m3q7b5z0v2j" }
  ]
}
```

## GET `/emails/:id`

Renvoie un e-mail de ton espace. Une clé ne voit jamais l’e-mail d’un autre espace.

Une clé « envoi seulement » peut aussi appeler cette route, mais seulement pour les e-mails qu’elle a elle-même envoyés. Pour un e-mail envoyé par une autre clé, ou avant que Mail enregistre la clé d’origine (2026-10-17), elle reçoit le même `404` que pour un identifiant inconnu. Une clé à accès complet lit tous les e-mails de l’espace.

| Champ | Type   | Obligatoire         | Description                             |
| ----- | ------ | ------------------- | --------------------------------------- |
| `id`  | string | Oui, dans le chemin | Identifiant renvoyé par `POST /emails`. |

```bash
curl https://api.mail.hgdc.ch/emails/kd7b3m9q2x8r4v6n1c0z5h7t \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"
```

```ts
import { Agentmail } from "./agentmail"

const mail = new Agentmail()
const email = await mail.emails.get("kd7b3m9q2x8r4v6n1c0z5h7t")
```

### Réponse

Statut `200`. `to` est toujours un tableau, même si l’envoi a donné une chaîne.

```json
{
  "object": "email",
  "id": "kd7b3m9q2x8r4v6n1c0z5h7t",
  "from": "Factures <factures@envoi.atelier.ch>",
  "to": ["client@exemple.ch"],
  "subject": "Ta facture d'octobre",
  "created_at": "2026-10-10T08:00:00.000Z",
  "last_event": "delivered"
}
```

`last_event` prend l’une des valeurs de [Statuts des e-mails](/docs/reference/statuts-des-e-mails/). Si l’e-mail n’existe pas dans ton espace, ou si une clé « envoi seulement » ne l’a pas envoyé, la réponse est un `404` avec `name: "not_found"`.

## Si ça ne marche pas

- **`422 validation_error` avec `from`** : l’adresse n’appartient pas à un domaine vérifié de ton espace, ou le domaine n’est pas encore **Prêt**.
- **`422` sur `scheduled_at`** : l’envoi programmé n’est pas encore pris en charge. Retire-le.
- **`422 invalid_attachment`** : la pièce jointe n’a pas de `content`, utilise `path`, ou son extension est refusée.
- **`413 payload_too_large`** : le corps dépasse 42,5 Mo. Une pièce jointe de plus de 2,5 Mo demande aussi l’en-tête `Content-Length`, que `fetch` et les SDK envoient d’eux-mêmes.
- **Le lot est refusé en entier** : un seul élément est invalide. Corrige l’élément invalide, puis renvoie le lot complet.
