Menu de la documentation

L'API et le serveur MCP

Envoyer des e-mails, gérer vos abonnés et vos campagnes depuis votre code ou depuis un agent IA.

Mailcheer se pilote de l'extérieur : depuis votre application, depuis un script, ou depuis un agent comme Claude Code, ChatGPT ou Codex. Deux portes, la même clé.

Pour quiAdresse
API RESTDu code — n'importe quel langage sait faire une requête HTTP.https://mailcheer.com/api/v1
Serveur MCPLes agents IA, qui découvrent seuls les outils disponibles.https://mailcheer.com/api/mcp

Votre première clé

Dans votre espace Mailcheer : Compte → API & agents IA → Nouvelle clé. Vous lui donnez un nom et vous cochez ce qu'elle a le droit de faire.

La clé complète s'affiche une seule fois. Nous n'en gardons qu'une empreinte : si vous la perdez, personne ne peut vous la rendre — vous en créez une autre et vous coupez l'ancienne. C'est le prix à payer pour qu'un vol de notre base ne donne aucune clé utilisable, et c'est le bon prix.

Rangez-la comme un mot de passe : dans les variables d'environnement de votre service, jamais dans du code partagé ni dans une page publique.

Les droits d'une clé

DroitCe qu'il ouvre
emails:sendEnvoyer des e-mails unitaires, et lire leur état.
subscribers:readLire les abonnés et la liste de suppression.
subscribers:writeAjouter, modifier et désinscrire des abonnés.
campaigns:readLire les campagnes et leurs statistiques.
campaigns:writeCréer et envoyer des campagnes.
webhooks:readLire les abonnements aux événements et leur journal.
webhooks:writeCréer, modifier et supprimer des abonnements aux événements.

Ne cochez que le nécessaire. Un appel hors du périmètre de la clé répond 403, et rien ne le contourne — c'est le seul garde-fou qui tienne face à un agent autonome : on ne compte pas sur sa prudence, on lui retire le bouton.

Les droits se choisissent à la création et ne bougent plus. Une clé dont on peut élargir le périmètre après coup ne veut plus rien dire : celui qui l'a reçue croit détenir un accès en lecture et se retrouve avec le droit d'envoyer, sans avoir été prévenu.

Envoyer un e-mail

C'est le point d'entrée le plus utilisé : la facture, l'alerte, le mot de passe oublié — tout ce que votre application écrit à une personne à la fois.

curl -X POST https://mailcheer.com/api/v1/emails \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: facture-2026-0412" \
  -d '{
    "from": "Votre marque <[email protected]>",
    "to": "[email protected]",
    "subject": "Votre facture de septembre",
    "html": "<p>La voici.</p>"
  }'

La réponse arrive en 202 :

{
  "id": "cmu651xf200021n7nm68sikfw",
  "object": "email",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "subject": "Votre facture de septembre",
  "created_at": "2026-09-18T07:12:44.102Z"
}

202, et pas 200 : notre fournisseur d'envoi a accepté le message, il n'est pas encore dans une boîte aux lettres. La remise se constate quelques secondes plus tard :

curl https://mailcheer.com/api/v1/emails/cmu651xf200021n7nm68sikfw \
  -H "Authorization: Bearer mch_live_…"

Le champ status passe de sent à delivered, ou à bounced si l'adresse n'existe pas, ou à complained si la personne a signalé le message comme indésirable. Dans ces deux derniers cas, l'adresse entre automatiquement en liste de suppression : votre application n'a pas à s'en occuper.

La désinscription en un clic

Si vous écrivez à des gens qui ne vous ont pas écrit les premiers — une lettre d'information, une alerte à laquelle on s'abonne, une page de statut — Gmail et Yahoo attendent un lien de désinscription dans les en-têtes du message, et pas seulement en bas de page. C'est leur exigence depuis février 2024.

Donnez-en l'adresse avec unsubscribe_url, et Mailcheer pose les deux en-têtes qui vont ensemble :

curl -X POST https://mailcheer.com/api/v1/emails \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Votre marque <[email protected]>",
    "to": "[email protected]",
    "subject": "Votre rapport du mois",
    "html": "<p>Le voici.</p>",
    "unsubscribe_url": "https://votredomaine.fr/desinscription/abc123"
  }'

Votre adresse doit accepter un POST et désinscrire sans demander de confirmation : c'est le principe du « un clic ». Un GET sur la même adresse peut mener à une page lisible, pour les clients de messagerie qui font l'un ou l'autre.

Sans cet en-tête, la seule sortie offerte au destinataire est le bouton « Spam » — et c'est la réputation de votre domaine d'envoi qui la paie, pas celle du message.

⚠️ List-Unsubscribe reste refusé dans headers : c'est Mailcheer qui l'écrit, vous n'en donnez que la cible. Cela garantit que List-Unsubscribe-Post l'accompagne toujours — sans ce second en-tête, Gmail n'affiche aucun bouton.

Les pièces jointes

Un devis, une facture, une plaquette : donnez-les dans attachments, au format de Resend — filename et content, le fichier encodé en base64.

curl -X POST https://mailcheer.com/api/v1/emails \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Votre marque <[email protected]>",
    "to": "[email protected]",
    "subject": "Votre devis",
    "text": "Le devis est en pièce jointe.",
    "attachments": [
      {
        "filename": "devis-2026-09.pdf",
        "content": "JVBERi0xLjQKJcfsj6IK…",
        "content_type": "application/pdf"
      }
    ]
  }'

En Node, le contenu se prépare en une ligne :

import { readFileSync } from "node:fs";

const devis = {
  filename: "devis-2026-09.pdf",
  content: readFileSync("./devis-2026-09.pdf").toString("base64"),
};

content_type est facultatif : il se déduit de l'extension (.pdfapplication/pdf), et vaut application/octet-stream en dernier recours. Vingt pièces au plus par message.

La limite est celle d'Amazon, notre fournisseur d'envoi : 40 Mo par message une fois encodé, soit environ 30 Mo de fichiers réels — le base64 ajoute un tiers. Au-delà, l'appel est refusé en 422, avec le nom du fichier, son poids et le poids atteint. C'est délibéré : un refus qui explique vaut mieux qu'un message qui part sans sa pièce.

Sont refusés de la même façon, et toujours en le disant :

  • les extensions que les messageries rejettent — .exe, .bat, .js, .vbs, .scr… Mettez le fichier dans une archive .zip, ou envoyez un lien de téléchargement ;
  • un content qui n'est pas du base64 valide ;
  • un champ path, qui donnerait une URL à aller chercher : notre serveur ne suit pas une adresse que vous choisissez. Encodez le fichier.

Quand un message porte une pièce, il part en MIME complet plutôt qu'en message simple. Tout le reste ne bouge pas : cc, bcc, reply_to, vos en-têtes, la désinscription en un clic et vos étiquettes fonctionnent à l'identique — et le bcc n'apparaît toujours dans aucun en-tête du message reçu.

Les copies

cc et bcc acceptent une adresse ou un tableau de 1 à 50, comme to. Les deux comptent dans votre quota et passent par les mêmes contrôles — une adresse en liste de suppression refuse l'appel entier, qu'elle soit destinataire ou en copie.

Les cinq règles de tout envoi

Elles ne sont pas contournables, et ce sont les mêmes que pour une campagne envoyée depuis l'interface.

1. from doit être sur un domaine vérifié de votre espace. Sinon 422 unverified_from_domain, avec la liste de vos domaines vérifiés dans le message. GET /api/v1/me vous les donne aussi.

2. Une adresse en liste de suppression est refusée, avec son motif — désinscription, adresse morte, plainte. L'appel entier échoue, y compris les autres destinataires : un envoi partiel dont vous ne sauriez rien est le pire des résultats possibles, parce que vous croiriez avoir prévenu tout le monde.

3. Le quota mensuel de votre offre compte ces envois comme les campagnes. C'est le même compte d'envoi, la même facture.

4. Un taux de rebonds ou de plaintes trop élevé suspend l'envoi. Les seuils sont ceux d'Amazon : 5 % de rebonds, 0,1 % de plaintes. Une application qui écrit à des adresses inventées fait les mêmes dégâts qu'une campagne sur liste achetée.

5. Rien ne contourne le double opt-in. 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.

Ne jamais envoyer deux fois

Une bibliothèque HTTP qui n'a pas reçu notre réponse rejoue l'appel. C'est son travail, et sans précaution votre client reçoit deux fois la même facture.

Ajoutez l'en-tête Idempotency-Key avec une valeur unique par envoi — le numéro de la facture, l'identifiant de la commande, un UUID :

Idempotency-Key: facture-2026-0412

Rejouer le même appel rend la même réponse, avec le même id, sans second envoi. L'en-tête Idempotent-Replay: true vous dit que c'est un rejeu. La même clé avec un corps différent répond 409 : ce n'est pas un réessai, c'est une erreur de votre côté, et vous renvoyer la réponse d'un autre envoi serait pire que de vous le dire.

Les abonnés

# Ajouter — la personne reçoit une confirmation et entre en « pending »
curl -X POST https://mailcheer.com/api/v1/subscribers \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","firstName":"Marie","tags":["clients"]}'

# Lister, page par page
curl "https://mailcheer.com/api/v1/subscribers?limit=50&status=subscribed" \
  -H "Authorization: Bearer mch_live_…"

# Désinscrire
curl -X DELETE https://mailcheer.com/api/v1/subscribers/marie%40exemple.fr \
  -H "Authorization: Bearer mch_live_…"

DELETE n'efface pas la fiche : la personne passe en unsubscribed et son adresse entre en liste de suppression. Effacer la trace la ferait revenir au prochain import de fichier — on aurait respecté le verbe HTTP et trahi la personne.

Une adresse désinscrite ne se réinscrit pas par l'API. Seule la personne peut revenir, par un formulaire. Une désinscription qu'un programme peut annuler ne vaut rien.

La pagination

Les listes rendent { data, has_more, next_cursor }. Passez next_cursor en ?cursor= pour la page suivante.

Pas de numéro de page, volontairement : sur une liste où l'on écrit en même temps qu'on lit — ce qui est exactement le cas d'une API — page=2 saute des lignes et en montre d'autres deux fois. Le curseur, lui, ne bouge pas.

Les campagnes

Créer et envoyer sont deux gestes séparés. Ce n'est pas une lourdeur : c'est ce qui permet de faire relire une lettre avant qu'elle parte à trois mille personnes.

# 1. Le brouillon — rien ne part
curl -X POST https://mailcheer.com/api/v1/campaigns \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Lettre de septembre",
    "subject": "Ce que nous avons appris cet été",
    "text": "# Bonjour\n\nVoici les nouvelles du mois."
  }'

# 2. L'envoi — irréversible
curl -X POST https://mailcheer.com/api/v1/campaigns/CAMP_ID/send \
  -H "Authorization: Bearer mch_live_…"

# 3. Le suivi
curl https://mailcheer.com/api/v1/campaigns/CAMP_ID \
  -H "Authorization: Bearer mch_live_…"

Dans text, une ligne vide sépare deux paragraphes et # en début de ligne fait un titre. Pour la mise en page complète (images, boutons, séparateurs), passez content avec les blocs de l'éditeur.

L'envoi répond 202 avec queued : les destinataires sont figés, les messages partent ensuite au débit autorisé par notre fournisseur. Un envoi de cinquante mille e-mails ne tient pas dans une requête HTTP, et prétendre le contraire vous donnerait un « envoyé » pour un travail qui commence à peine.

Les taux d'ouverture et de clic sont calculés sur les messages délivrés, jamais sur le nombre de destinataires : une adresse morte ne doit pas faire baisser le taux de ceux qui ont bien reçu.

Écrire à un groupe plutôt qu'à toute la liste

Sans corps, l'envoi part à toute la cible de la campagne : tous les abonnés actifs de l'espace (ou du segment choisi dans l'interface), moins la liste de suppression. Pour n'écrire qu'à une partie — ceux qui n'ont pas répondu, vos clients, les inscrits d'un atelier — passez leurs adresses dans to :

curl -X POST https://mailcheer.com/api/v1/campaigns/CAMP_ID/send \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "to": ["[email protected]", "[email protected]", "[email protected]"] }'

to ne peut que restreindre. La lettre part aux adresses demandées qui sont aussi des abonnés actifs de la cible, et jamais à une adresse de la liste de suppression : une personne désinscrite, dont l'adresse a rebondi ou qui s'est plainte ne reçoit rien, même si son adresse est dans to. La réponse dit qui est retenu, qui est écarté, et pourquoi :

{
  "id": "cmp_8d2", "object": "campaign", "status": "sending", "queued": 2,
  "audience": {
    "requested": 3, "duplicates": 0, "retained": 2, "excluded": 1,
    "reasons": { "invalid": 0, "not_in_list": 0, "pending": 0, "unsubscribed": 1,
                 "bounced": 0, "complained": 0, "suppressed": 0, "outside_segment": 0 },
    "excluded_addresses": [ { "email": "[email protected]", "reason": "unsubscribed" } ]
  },
  "note": "2 destinataire(s) mis en file. …"
}

Le compte tombe toujours juste : requested = duplicates + retained + excluded. La casse et les espaces sont ignorés ([email protected] est la même personne). Les raisons : invalid (pas une adresse), not_in_list (aucun abonné de l'espace ne la porte), pending (inscription pas encore confirmée), unsubscribed, bounced, complained, suppressed (abonné, mais sur la liste de suppression) et outside_segment (hors du segment que vise la campagne).

Trois règles à connaître :

  • Une liste vide n'est pas « pas de liste ». "to": [] n'écrit à personne : l'envoi est refusé (422). Pour écrire à toute la liste, n'envoyez pas de champ to du tout.
  • Pas de to sur une campagne avec test A/B. La version gagnante part des heures plus tard, calculée sur toute la cible de la campagne ; la liste d'adresses n'y survivrait pas. L'envoi est refusé plutôt que de partir à tout le monde.
  • Les champs inconnus sont ignorés, sauf les sosies de dry_run et de to. dryRun, dry-run, DRY_RUN, test, simulate, preview… et To, TO, recipients, emails, to_emails, destinataires, adresses, audience… sont refusés (422) en nommant le bon champ : ignorés, les premiers feraient partir la campagne pour de vrai, les seconds la feraient partir à toute la liste. POST /api/v1/audience refuse tout champ inconnu.

Jusqu'à 50 000 adresses par appel.

Simuler avant d'envoyer

"dry_run": true fait tous les contrôles d'un vrai envoi — expéditeur, contenu, réputation, quota, destinataires — et ne met rien en file. La réponse est un 200 :

{ "id": "cmp_8d2", "object": "send_preview", "dry_run": true,
  "would_send": true, "blocked_reason": null, "recipients": 2,
  "audience": { "requested": 3, "retained": 2, "excluded": 1, … } }

Si le vrai envoi serait refusé, would_send vaut false et blocked_reason donne le message exact qu'il rendrait. C'est le nombre à montrer à la personne avant qu'elle confirme.

Pour poser la même question avant d'avoir créé la campagne — pendant qu'on choisit à qui écrire, dans votre propre logiciel — POST /api/v1/audience rend le même bilan sans rien créer (droit subscribers:read) :

curl -X POST https://mailcheer.com/api/v1/audience \
  -H "Authorization: Bearer mch_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "to": ["[email protected]", "[email protected]"] }'
# → { "object": "audience", "recipients": 2, "audience": { … } }

Sans to, il rend le nombre d'abonnés qu'atteindrait une campagne envoyée à toute la liste. Les contrôles propres à une campagne (objet, contenu, quota) restent ceux de dry_run.

Le rapport complet

GET /api/v1/campaigns/CAMP_ID/stats rend tout ce que la fiche campagne de Mailcheer montre, pour l'afficher dans votre propre logiciel — un CRM, un tableau de bord :

{
  "campaign": { "id": "…", "name": "…", "subject": "…", "status": "sent", "kind": "newsletter",
                "fromName": "…", "fromEmail": "…", "sentAt": "…", "scheduledAt": null, "updatedAt": "…" },
  "counts": { "recipients": 66, "delivered": 60, "opened": 30, "clicked": 10,
              "bounced": 6, "complained": 1, "unsubscribed": 0 },
  "rates": { "delivered": 0.9091, "opened": 0.5, "clicked": 0.1667, "bounced": 0.0909 },
  "timeline": [ { "label": "+0h", "opened": 12, "clicked": 5 }, … ],
  "audience": { "total": 30, "proxiedShare": 0.4,
                "device": [ { "label": "Téléphone", "count": 15, "share": 0.5 }, … ],
                "os": [ … ], "client": [ … ] },
  "links": [ { "url": "https://…", "clicks": 6 }, … ],
  "html": "<!doctype html>…"
}

Les parts (rates, share, proxiedShare) sont entre 0 et 1, et null tant qu'il n'y a rien à diviser. timeline compte les ouvertures et les clics par tranche de six heures pendant les 48 premières heures après l'envoi — vide tant que la campagne n'est pas partie. audience ne garde que les cinq premières lignes de chaque répartition ; proxiedShare est la part des ouvertures venues d'un relais de confidentialité (Apple Mail, Gmail) : reçues, pas forcément lues. links va du plus au moins cliqué. html est le message tel qu'il est parti, chaîne vide sinon.

La langue des réponses

Les messages d'erreur suivent votre en-tête Accept-Language : français si vous le demandez, anglais par défaut.

curl https://mailcheer.com/api/v1/me
# {"error":{"code":"missing_api_key","message":"Missing API key. Add the header …"}}

curl https://mailcheer.com/api/v1/me -H "Accept-Language: fr"
# {"error":{"code":"missing_api_key","message":"Clé d'API absente. Ajoutez l'en-tête …"}}

Cela vaut pour tout ce qu'un programme lit : les messages d'erreur de l'API, les outils du serveur MCP (leur nom, ce qu'ils font, leurs paramètres) et la référence servie sur mailcheer://docs.

Seule la première préférence est lue : fr-FR,fr;q=0.9,en;q=0.8 demande du français, même si l'anglais y figure — et en-US,fr;q=0.9 demande de l'anglais.

⚠️ Le code, lui, ne change jamais de langue — c'est contre lui qu'on écrit sa logique, jamais contre le message.

Les erreurs

Toujours la même forme, avec deux lectures possibles du même contenu :

{
  "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": "[email protected]", "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"
}

error est la forme de Mailcheer : structurée, avec dans details ce qu'il faut pour corriger. Les trois champs à plat — statusCode, message, name — sont ceux de Resend, et ils sont là pour que du code écrit contre l'ancienne API affiche un message juste sans être relu.

Écrivez votre logique contre code (ou name, c'est la même valeur), jamais contre message. Le message est fait pour être lu par un humain, et nous nous autorisons à le reformuler.

CodeStatutCe que ça veut dire
missing_api_key401Aucun en-tête Authorization.
invalid_api_key401Clé inconnue.
revoked_api_key401Clé coupée dans les réglages.
insufficient_scope403La clé n'a pas le droit demandé.
reputation_blocked403Vos envois sont suspendus : trop de rebonds ou de plaintes.
quota_exceeded402Le quota mensuel de l'offre est atteint.
not_found404L'objet n'existe pas dans cet espace.
conflict409État incompatible : campagne déjà partie, abonné sorti.
idempotency_key_reused409Même Idempotency-Key, corps différent.
validation_error422Un champ est absent ou mal formé.
unverified_from_domain422Le domaine de from n'est pas vérifié.
suppressed_recipient422Un destinataire est en liste de suppression.
rate_limit_exceeded429Plus de 600 requêtes par minute (voir Retry-After).
send_failed502Notre fournisseur d'envoi a refusé le message.
internal_error500Une panne de notre côté.

Limite de débit

600 requêtes par minute et par clé. Chaque réponse porte X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset ; un refus porte en plus Retry-After, en secondes.

Besoin de plus ? Écrivez-nous : on regarde votre cas plutôt que de vous laisser réessayer en boucle.

Le serveur MCP

MCP — Model Context Protocol — est la façon dont un agent IA découvre les outils d'un logiciel et s'en sert. Mailcheer expose un serveur MCP hébergé : rien à installer, une adresse et votre clé.

Claude Code

claude mcp add mailcheer \
  --transport http \
  --url https://mailcheer.com/api/mcp \
  --header "Authorization: Bearer mch_live_…"

ChatGPT, Cursor, Codex, Claude Desktop — tous lisent la même fiche de connecteur :

{
  "mcpServers": {
    "mailcheer": {
      "type": "http",
      "url": "https://mailcheer.com/api/mcp",
      "headers": { "Authorization": "Bearer mch_live_…" }
    }
  }
}

Puis, dans votre agent : « Quel espace Mailcheer vois-tu, et quels domaines d'envoi sont vérifiés ? ». Il appellera get_account, qui ne modifie rien — c'est la bonne façon de vérifier un branchement.

Les outils exposés

OutilCe qu'il fait
get_accountL'espace, les droits, le quota restant, les domaines vérifiés.
send_emailEnvoie un e-mail unitaire. Irréversible.
get_emailL'état d'un e-mail parti.
list_subscribersListe les abonnés, page par page.
add_subscriberAjoute ou met à jour un abonné.
remove_subscriberDésinscrit et écarte l'adresse. Irréversible.
list_suppressionLes adresses qui ne recevront plus rien.
add_suppressionÉcarte une adresse. Irréversible.
list_campaignsLes campagnes de l'espace.
create_campaignCrée un brouillon. Rien ne part.
preview_campaign_sendDit si la campagne partirait et à combien de personnes, adresse par adresse avec to. Rien ne part.
send_campaignEnvoie à tous les abonnés actifs, ou aux seuls abonnés actifs parmi les adresses de to. Irréversible.
get_campaign_statsChiffres et état d'une campagne.

Chaque outil est un appel à l'API ci-dessus, rien de plus : mêmes droits, même quota, même liste de suppression, mêmes refus. Un second chemin d'accès avec sa propre logique serait un second jeu de règles, et le jour où l'un des deux changerait, MCP deviendrait la porte de service.

Aucun outil ne retire une adresse de la liste de suppression. C'est le seul geste du produit qui fait suspendre une capacité d'envoi, et un agent à qui l'on dirait « nettoie la liste » le ferait sans hésiter. Il se fait à la main, dans votre espace.

La ressource mailcheer://docs donne à l'agent la référence complète : il n'a pas besoin de la connaître à l'avance.

Ce qui change si vous venez de Resend

Les champs de POST /v1/emails et la réponse { id } sont les mêmes. En pratique : l'adresse de base et la clé.

Deux façons de basculer.

Avec le client minimal — un fichier à copier, aucune dépendance, la même signature que le SDK de Resend. Récupérez-le : mailcheer.com/mailcheer-client.fr.ts — la version anglaise, canonique, est sur /mailcheer-client.ts.

// avant
const resend = new Resend(process.env.RESEND_API_KEY);

// après
const mailcheer = new Mailcheer(process.env.MAILCHEER_API_KEY);

// le reste de votre code ne bouge pas
const { data, error } = await mailcheer.emails.send({ from, to, subject, html, text });
if (error) throw new Error(`Email delivery failed: ${error.message}`);
return { providerId: data?.id ?? null };

Il ne lève jamais : une panne réseau devient elle aussi un error, avec name: "network_error". C'est voulu — une méthode qui lèverait là où l'ancienne rendait un objet transformerait « changer deux lignes » en « relire chaque appel », et les appels qu'on oublie de relire sont justement les chemins d'erreur.

Sans rien copier — un fetch nu suffit :

const res = await fetch("https://mailcheer.com/api/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MAILCHEER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ from, to, subject, html }),
});
const body = await res.json();
if (!res.ok) throw new Error(body.message); // le message est lisible par un humain
const id = body.id;

Trois points à connaître :

  • Le domaine de from doit être vérifié dans votre espace Mailcheer, pas chez Resend. Ajoutez-le dans Domaines et posez les enregistrements DNS.
  • La liste de suppression protège aussi les envois unitaires. Une adresse qui s'est désinscrite de votre newsletter ne recevra pas non plus vos e-mails transactionnels depuis le même espace — si ce n'est pas ce que vous voulez, séparez les deux dans deux espaces.
  • Le quota mensuel est partagé avec vos campagnes.

Où vivent ces e-mails

Les e-mails partis par l'API ne rejoignent pas vos campagnes : ils vivent à part, et ce n'est pas un détail technique.

Un destinataire de facture n'est pas un abonné. Le ranger avec vos abonnés l'aurait inscrit dans votre liste sans qu'il ait jamais consenti à recevoir votre newsletter — compté sur votre tableau de bord, et ciblé par votre prochaine campagne. Vos chiffres d'abonnés restent donc ceux de vos vrais abonnés.

Ce qui est partagé, en revanche : le quota mensuel, la liste de suppression, et la surveillance des rebonds. Ce sont les trois choses qui engagent votre réputation d'expéditeur, et elle est la même des deux côtés.

La description technique

Le fichier OpenAPI 3.1 est servi tel quel, en français : mailcheer.com/openapi.fr.json — la version anglaise, canonique, est sur /openapi.json. Il décrit chaque point d'entrée, chaque champ et chaque erreur — de quoi engendrer un client dans votre langage, ou le donner à un agent.