Documentation
API publique et serveur MCP
Construisez des agents, des CLI et des intégrations back-office sur qmailing. PLUS et au-dessus débloquent l'authentification par token Bearer sur chaque endpoint public ainsi qu'un serveur MCP prêt à brancher dans Claude Desktop, Cursor ou tout autre client compatible.
Démarrer
- 1. Passez à PLUS — l'API publique est disponible sur
PLUS,PRO,PRO_PLUSouPRO_MAX. Les appelants en FREE reçoivent402 Payment Requiredquel que soit le token. - 2. Émettez un token — rendez-vous dans Paramètres → Développeurs, cliquez sur Nouveau token, choisissez les scopes nécessaires et copiez la valeur
qm_live_…dès qu'elle s'affiche. Les tokens n'apparaissent qu'une seule fois. - 3. Effectuez votre premier appel :
curl https://qmailing.com/api/v1/pub/mailboxes \ -H "Authorization: Bearer qm_live_your_token_here"
Authentification
Chaque appel à l'API publique embarque un en-tête Authorization: Bearer qm_live_<…>. Le format du token utilise un préfixe (qm_live_ suivi de 32 caractères aléatoires) afin que les fuites dans les logs et les dépôts soient évidentes.
Les tokens sont stockés sous forme de hash (SHA-256). Le texte en clair est affiché une seule fois lors de l'émission et plus jamais — si vous le perdez, créez-en un nouveau. Chaque requête déclenche une comparaison de hash en temps constant ; last_used_at est mis à jour en cas de succès afin que l'UI développeurs puisse mettre en évidence les identifiants obsolètes.
apiAccess à chaque requête signée — pas besoin de révoquer les tokens un par un lors d'un changement de plan.Scopes
Chaque token porte une ou plusieurs chaînes de scope. Les endpoints déclarent les scopes dont ils ont besoin ; un scope manquant renvoie 403 InsufficientScope. Le joker (*) accorde tout — ne l'activez que si c'est vraiment nécessaire.
| Scope | Permet |
|---|---|
| mailboxes:read | Lister et inspecter les boîtes mail |
| mailboxes:write | Créer, modifier, supprimer des boîtes mail |
| domains:read | Lister les domaines et les enregistrements DNS |
| domains:write | Ajouter, vérifier, supprimer des domaines personnalisés (FE uniquement en v1) |
| email:read | Lister et inspecter les messages des boîtes |
| email:send | Envoyer du courrier sortant via l'API |
| webhooks:read | Lister les endpoints de webhook |
| webhooks:write | Enregistrer, lister et révoquer les endpoints de webhook |
| * | Joker — accorde tout. À utiliser avec parcimonie. |
Boîtes mail
Lister
GET /api/v1/pub/mailboxes
# scope: mailboxes:readRécupérer une
GET /api/v1/pub/mailboxes/{id}
# scope: mailboxes:readCréer
POST /api/v1/pub/mailboxes
Content-Type: application/json
# scope: mailboxes:write
{
"localPart": "support",
"domain": "yourbrand.com",
"displayName": "Support team",
"forwardTo": "ops@yourbrand.com"
}Le champ domain est optionnel — omettez-le et la boîte est créée sur qmailing.com. Sur un domaine personnalisé, celui-ci doit être à la fois claimed ET fullyVerified ; un domaine pas encore prêt renvoie 400 au lieu de créer silencieusement une boîte non routable.
Domaines & DNS
Lister les domaines personnalisés
GET /api/v1/pub/domains
# scope: domains:readListe des enregistrements DNS
GET /api/v1/pub/domains/{id}/dns-records
# scope: domains:readRenvoie la liste complète des 8 enregistrements (TXT de challenge, MX, SPF, trois CNAME DKIM, DMARC, optionnellement _amazonses) avec un status par ligne parmi PENDING / FOUND / MISMATCH / NOT_REQUIRED. L'agent peut s'en servir pour indiquer à l'utilisateur exactement ce qu'il doit publier chez son fournisseur DNS.
Lisez le contenu des boîtes mail et envoyez du courrier sortant. La pagination par dossier correspond à l'API interne du front — un agent qui passe de l'auth cookie-JWT à Bearer ne voit aucun changement de forme.
Lister par dossier
GET /api/v1/pub/email?folder=INBOX&offset=0&limit=25
# scope: email:read
# Optional query params:
# mailboxId filter to one mailbox (omit for all-mailboxes view)
# folder INBOX | SENT | DRAFTS | TRASH | STARRED | SPAM
# offset 0-based index, default 0
# limit 1..100, default 25folder vaut par défaut INBOX ; valeurs autorisées : INBOX, SENT, DRAFTS, TRASH, STARRED, SPAM. mailboxId est optionnel — l'omettre liste toutes les boîtes de l'appelant. Pagination via offset + limit ; limit est borné à la plage 1–100 (par défaut 25).
Récupérer un
GET /api/v1/pub/email/{id}
# scope: email:readRenvoie le EmailDetailDto complet : corps HTML / texte, liste de labels, métadonnées des pièces jointes. Les pièces jointes ne sont pas inline — récupérez chacune via GET /{emailId}/attachments/{index} ci-dessous, qui diffuse les octets bruts avec le nom de fichier d'origine et le content-type.
Télécharger une pièce jointe
GET /api/v1/pub/email/{emailId}/attachments/{index}
# scope: email:read
# response: streamed bytes, Content-Type from the attachmentDiffuse les octets bruts de la pièce jointe (index zéro-basé dans la liste des pièces jointes du message). Même forme que l'endpoint basé session : Content-Type en pass-through, Content-Disposition selon RFC 6266 avec le nom d'origine (UTF-8 percent-encoded pour l'Unicode). Les agents MCP appelant via @qmailing/mcp-server utilisent l'outil qmailing_get_attachment, qui renvoie les octets encodés en base64 inline (limite 5 Mio).
Envoyer
Endpoint d'envoi. Corps multipart/form-data avec une partie JSON command (destinataires / sujet / corps) et zéro ou plusieurs parties-fichiers attachments :
POST /api/v1/pub/email/send
Content-Type: multipart/form-data
# scope: email:send
--boundary
Content-Disposition: form-data; name="command"
Content-Type: application/json
{
"mailboxId": "11111111-2222-3333-4444-555555555555",
"to": ["alice@example.com"],
"cc": [],
"bcc": [],
"subject": "Order #1428 confirmed",
"bodyText": "Plain-text body",
"bodyHtml": "<p>Rich body</p>",
"replyToId": null
}
--boundary
Content-Disposition: form-data; name="attachments"; filename="receipt.pdf"
Content-Type: application/pdf
<binary bytes>
--boundary--Forme du JSON command :
{
"mailboxId": "uuid (required) — must be SES-verified",
"to": "string[] (required, max 50)",
"cc": "string[] (optional, max 50)",
"bcc": "string[] (optional, max 50)",
"subject": "string (max 998 chars)",
"bodyText": "string — plain-text body",
"bodyHtml": "string — HTML body",
"replyToId": "uuid | null — set for in-thread replies"
}Chaque appel décompte de la limite quotidienne d'envoi du plan, du quota d'envoi SES de la boîte et du quota de stockage de l'utilisateur pour la copie résultante dans SENT. Les destinataires sont validés comme adresses RFC 5322 ; ceux qui échouent renvoient 400 avec l'adresse fautive. La boîte référencée par mailboxId doit être vérifiée par SES — sinon, 409 MailboxNotVerified avec un indice pointant vers la page domaines.
multipart/form-data à la sortie, le format sur le fil reste donc identique.filename sur la partie command est facultatif — Node fetch tamponne automatiquement filename="blob" sur chaque Blob, les clients navigateur et Node peuvent transmettre command.json, et les clients curl --form conformes peuvent l'omettre. Les trois formes sont parsées de la même manière ; seul Content-Type: application/json sur les octets de la partie compte.Scopes : email:read pour lister + récupérer un, email:send pour l'envoi.
Webhooks
Enregistrez un endpoint HTTPS et QMailing POSTe un envelope JSON signé dès qu'un des événements souscrits se déclenche. La livraison est en service — le worker interroge une file par événement toutes les 10s, signe chaque requête en HMAC-SHA256 et réessaie les échecs en backoff exponentiel (1m / 5m / 15m / 1h / 6h) avant de parquer la ligne en DLQ pour revue manuelle.
Enregistrer un endpoint
POST /api/v1/pub/webhooks
Content-Type: application/json
# scope: webhooks:write
{
"url": "https://your-server.example.com/qmailing-events",
"label": "Production listener",
"events": ["email.received", "email.bounced", "domain.verified"]
}
# Response (201):
{
"endpoint": {
"id": "uuid",
"url": "https://your-server.example.com/qmailing-events",
"label": "Production listener",
"events": ["email.received", "email.bounced", "domain.verified"],
"secretPrefix": "whk_AbCd",
"createdAt": "2026-05-04T20:30:00Z",
"active": true
},
"plaintext": "whk_AbCd…<32 random chars>" // shown ONCE
}url doit être un endpoint HTTPS (HTTP n'est autorisé que pour localhost en dev). L'hôte est vérifié contre une allow-list anti-SSRF — les adresses privées / loopback / cloud-metadata sont rejetées dès l'enregistrement. La réponse contient plaintext exactement une fois ; conservez-le côté serveur et utilisez-le pour vérifier les signatures HMAC des livraisons entrantes.
Catalogue d'événements
| Événement | Se déclenche quand |
|---|---|
| email.received | Un courrier entrant arrive dans l'une de vos boîtes. |
| email.sent | Un courrier sortant a été accepté par SES. |
| email.bounced | Un courrier sortant a rebondi (hard ou soft). |
| domain.verified | Un domaine personnalisé a terminé la vérification DNS de bout en bout. |
Lister les endpoints
GET /api/v1/pub/webhooks
# scopes: webhooks:read OR webhooks:writeRenvoie les endpoints actifs et révoqués, les plus récents d'abord. L'accès en lecture fonctionne avec webhooks:read ou webhooks:write — un token inspecteur évite ainsi le scope d'écriture plus large.
Forme de la livraison
Chaque livraison est un unique POST vers votre endpoint avec Content-Type: application/json. Le corps est l envelope d événement avec les champs event (nom de l événement), occurred_at (timestamp ISO) et un bloc data dont la forme dépend de l événement (voir la table ci-dessus). Trois en-têtes portent les métadonnées de routage + vérification :
POST https://your-server.example.com/qmailing-events
Content-Type: application/json
User-Agent: qmailing-webhook/1 (+https://qmailing.com)
X-Qmailing-Event: email.received
X-Qmailing-Delivery: 5f3b2a91-7c4d-4d52-9c3e-aa1bcd8a4f12
X-Qmailing-Signature: t=1685120800,v1=abc123def…
{
"event": "email.received",
"occurred_at": "2026-05-23T19:46:40Z",
"data": {
"email_id": "uuid",
"mailbox_id": "uuid",
"from": "alice@example.com",
"subject": "Hello",
"preview_text": "Just checking in…"
}
}Schéma de signature
# Receiver side (Node example)
const crypto = require("node:crypto");
function verify(headers, body, secret) {
const sig = headers["x-qmailing-signature"]; // "t=1685120800,v1=abc123…"
const m = /t=(\d+),v1=([0-9a-f]+)/.exec(sig);
if (!m) return false;
const ts = Number(m[1]);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5-min replay window
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}Rejetez la requête si le timestamp a plus de 5 minutes (fenêtre anti-replay) ou si le HMAC recalculé ne correspond pas au v1 hex — notre signataire BE imite exactement les conventions Stripe / GitHub, donc n'importe quelle bibliothèque de vérification standard fonctionne telle quelle.
Politique de retry
Les réponses non-2xx (ou erreurs de transport — DNS, TLS, connexion refusée, timeout de 10s) incrémentent attempt_count et poussent next_attempt_at au prochain créneau dans 1m / 5m / 15m / 1h / 6h. Après le cinquième échec, la livraison passe en FAILED (DLQ) — visible sur /settings/developers, où le développeur peut la rejouer manuellement. Idempotence : chaque retry porte le même UUID dans X-Qmailing-Delivery — les receveurs dédupliquent sur cet en-tête.
Lister les livraisons récentes
GET /api/v1/pub/webhooks/{endpointId}/deliveries?limit=100
# scopes: webhooks:read OR webhooks:writeRenvoie les limit livraisons les plus récentes d'un endpoint (par défaut 100, max 100), les plus récentes d'abord. Chaque ligne porte statut, compteur de tentatives, dernier code HTTP, dernière erreur et le JSON original — alimente la vue d'historique sur le dashboard développeur.
Envoyer une livraison de test
POST /api/v1/pub/webhooks/{endpointId}/test
# scope: webhooks:write
# Response 202: WebhookDeliveryDto (enqueued, picked up next worker tick)Déclenche un événement synthétique webhook.test sur l'endpoint — même chemin de dispatch que les vrais événements, utile pour vérifier la signature + TLS avant de souscrire au trafic en direct.
Réessayer une livraison
POST /api/v1/pub/webhooks/deliveries/{deliveryId}/retry
# scope: webhooks:write
# Response 200: WebhookDeliveryDto (status=PENDING, attempt=MAX-1)Remet manuellement en file une ligne de livraison. Impossible de réessayer une livraison déjà DELIVERED (409). Le retry donne exactement une tentative supplémentaire (compteur à MAX-1), donc un endpoint cassé en permanence ne peut être bouclé indéfiniment par des clics répétés.
Supprimer une livraison de l'historique
DELETE /api/v1/pub/webhooks/deliveries/{deliveryId}
# scope: webhooks:write
# Response: 204 No ContentHard-delete d'une ligne de livraison de la table d'historique. Le log d'audit de conformité capture le fait de la suppression, donc la traçabilité n'est pas perdue lorsque la ligne disparaît.
Révoquer un endpoint
DELETE /api/v1/pub/webhooks/{id}
# scope: webhooks:write
# Response: 204 No ContentIdempotent — révoquer un endpoint déjà révoqué réussit silencieusement. La ligne est conservée (timestamp revoked-at posé) pour que les logs d'audit gardent leurs références.
409 WebhookEndpointLimitExceeded / 400 InvalidWebhookEvent.Erreurs
Chaque réponse non-2xx est un ProblemDetail RFC-7807 avec un champ code stable sur lequel votre agent peut s'aiguiller :
| HTTP | code | Quand |
|---|---|---|
| 400 | InvalidApiTokenScope | Chaîne de scope inconnue à la création. |
| 400 | InvalidEmailAddress | Un destinataire n'a pas passé la validation de syntaxe RFC 5322. |
| 400 | InvalidWebhookEvent | La liste d'événements enregistrée contient un nom inconnu. |
| 401 | — | En-tête Bearer manquant, mal formé, ou token révoqué / expiré. |
| 402 | PlanFeatureRequired | L'appelant est sur un plan sans accès API — passez à la version supérieure. |
| 403 | InsufficientScope | Le token ne porte aucun des scopes requis. |
| 404 | ApiTokenNotFound | La cible de révocation n'existe pas ou appartient à un autre compte. |
| 409 | MailboxNotVerified | Le domaine de la boîte d'envoi n'est pas encore entièrement vérifié — terminez le DNS dans /settings/domains. |
| 409 | ApiTokenLimitExceeded | Le compte a atteint le maximum (20) de tokens actifs ; révoquez-en un d'abord. |
| 409 | WebhookEndpointLimitExceeded | Le compte détient déjà 10 endpoints de webhook actifs ; révoquez-en un d'abord. |
| 429 | RateLimitExceeded | Le token a dépassé 300 requêtes par minute. Retry-After en secondes. |
Limites de débit
Chaque token dispose de son propre seau de 300 requêtes par minute. Les configurations multi-agents (CLI + cron + plugin IDE) utilisant des tokens différents pour le même compte fonctionnent indépendamment — elles ne se disputent jamais un seau commun. 429 RateLimitExceeded inclut un en-tête Retry-After en secondes.
Serveur MCP
Le paquet @qmailing/mcp-server s'exécute localement et expose l'API publique sous forme d'outils Model Context Protocol. Branchez-le dans Claude Desktop, Cursor, Continue ou tout client compatible MCP.
@qmailing/mcp-server@0.2.0) si vous ne voulez pas de mises à jour automatiques.{
"mcpServers": {
"qmailing": {
"command": "npx",
"args": ["-y", "@qmailing/mcp-server"],
"env": {
"QMAILING_API_TOKEN": "qm_live_your_token_here"
}
}
}
}Outils fournis : qmailing_list_mailboxes, qmailing_get_mailbox, qmailing_create_mailbox, qmailing_list_domains, qmailing_get_dns_records, qmailing_list_emails, qmailing_get_email, qmailing_get_attachment, qmailing_send_email, qmailing_register_webhook, qmailing_list_webhooks, qmailing_delete_webhook. Le catalogue s'enrichit à mesure que de nouveaux endpoints arrivent.