Plumail
Ejecuta un espacio de trabajo de correo de Plumail desde un agente: suscriptores, segmentos, campañas, envíos transaccionales, estadísticas de entrega y lista de supresión. Servidor remoto alojado, alojamiento en la UE, claves API con alcance.
Servidor MCP alojado
npx add-mcp 'https://plumail.fr/api/mcp'Se instala en Claude Code, Codex, Cursor y más
Documentación
Envía correos electrónicos, gestiona tus suscriptores y tus campañas desde tu código o desde un agente de IA.
Plumail se controla desde el exterior: desde tu aplicación, desde un script, o desde un agente como Claude Code, ChatGPT o Codex. Dos puertas, la misma llave.
| Para quién | Dirección | |
|---|---|---|
| API REST | Para código — cualquier lenguaje sabe hacer una solicitud HTTP. | https://plumail.fr/api/v1 |
| Servidor MCP | Para agentes de IA, que descubren por sí solos las herramientas disponibles. | https://plumail.fr/api/mcp |
Tu primera llave
En tu espacio Plumail: Ajustes → API → Nueva llave. Le pones un nombre y marcas lo que tiene permitido hacer.
La llave completa se muestra una sola vez. Solo guardamos una huella: si la pierdes, nadie puede devolvértela — creas otra y cortas la anterior. Es el precio a pagar para que un robo de nuestra base no dé ninguna llave utilizable, y es el precio correcto.
Guárdala como una contraseña: en las variables de entorno de tu servicio, nunca en código compartido ni en una página pública.
Los permisos de una llave
| Permiso | Qué abre |
|---|---|
emails:send | Enviar correos individuales y leer su estado. |
subscribers:read | Leer los suscriptores y la lista de supresión. |
subscribers:write | Añadir, modificar y dar de baja a suscriptores. |
campaigns:read | Leer las campañas y sus estadísticas. |
campaigns:write | Crear y enviar campañas. |
Marca solo lo necesario. Una llamada fuera del alcance de la llave responde 403, y nada lo evita — es la única salvaguarda que se sostiene frente a un agente autónomo: no contamos con su prudencia, le quitamos el botón.
Los permisos se eligen en la creación y ya no cambian. Una llave cuyo alcance se pueda ampliar después no significa nada: quien la recibió cree tener acceso de lectura y se encuentra con el derecho de enviar, sin haber sido avisado.
Enviar un correo
Es el punto de entrada más utilizado: la factura, la alerta, la contraseña olvidada — todo lo que tu aplicación escribe a una persona a la vez.
curl -X POST https://plumail.fr/api/v1/emails \
-H "Authorization: Bearer plm_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 respuesta llega 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, y no 200: nuestro proveedor de envío ha aceptado el mensaje, aún no está en una bandeja de entrada. La entrega se comprueba unos segundos más tarde:
curl https://plumail.fr/api/v1/emails/cmu651xf200021n7nm68sikfw \
-H "Authorization: Bearer plm_live_…"
El campo status pasa de sent a delivered, o a bounced si la dirección no existe, o a complained si la persona ha marcado el mensaje como no deseado. En estos dos últimos casos, la dirección entra automáticamente en la lista de supresión: tu aplicación no tiene que ocuparse de ello.
Las cinco reglas de todo envío
No son negociables, y son las mismas que para una campaña enviada desde la interfaz.
1. from debe estar en un dominio verificado de tu espacio. Si no, 422 unverified_from_domain, con la lista de tus dominios verificados en el mensaje. GET /api/v1/me también te los da.
2. Una dirección en la lista de supresión es rechazada, con su motivo — baja, dirección muerta, queja. La llamada completa falla, incluidos los demás destinatarios: un envío parcial del que no supieras nada es el peor de los resultados posibles, porque creerías haber avisado a todos.
3. La cuota mensual de tu plan cuenta estos envíos igual que las campañas. Es la misma cuenta de envío, la misma factura.
4. Una tasa de rebotes o de quejas demasiado alta suspende el envío. Los umbrales son los de Amazon: 5 % de rebotes, 0,1 % de quejas. Una aplicación que escribe a direcciones inventadas causa los mismos daños que una campaña sobre lista comprada.
5. Nada evita el doble opt-in. Un suscriptor añadido por la API recibe una confirmación, salvo double_opt_in: false explícito — y entonces eres tú quien responde del consentimiento.
Nunca enviar dos veces
Una biblioteca HTTP que no ha recibido nuestra respuesta repite la llamada. Es su trabajo, y sin precaución tu cliente recibe dos veces la misma factura.
Añade la cabecera Idempotency-Key con un valor único por envío — el número de factura, el identificador del pedido, un UUID:
Idempotency-Key: facture-2026-0412
Repetir la misma llamada devuelve la misma respuesta, con el mismo id, sin segundo envío. La cabecera Idempotent-Replay: true te indica que es una repetición. La misma llave con un cuerpo diferente responde 409: no es un reintento, es un error de tu lado, y devolverte la respuesta de otro envío sería peor que decírtelo.
Los suscriptores
# Ajouter — la personne reçoit une confirmation et entre en « pending »
curl -X POST https://plumail.fr/api/v1/subscribers \
-H "Authorization: Bearer plm_live_…" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","firstName":"Marie","tags":["clients"]}'
# Lister, page par page
curl "https://plumail.fr/api/v1/subscribers?limit=50&status=subscribed" \
-H "Authorization: Bearer plm_live_…"
# Désinscrire
curl -X DELETE https://plumail.fr/api/v1/subscribers/marie%40exemple.fr \
-H "Authorization: Bearer plm_live_…"
DELETE no borra la ficha: la persona pasa a unsubscribed y su dirección entra en la lista de supresión. Borrar el rastro la haría volver en la próxima importación de archivo — habríamos respetado el verbo HTTP y traicionado a la persona.
Una dirección dada de baja no se vuelve a suscribir por la API. Solo la persona puede volver, mediante un formulario. Una baja que un programa pueda anular no vale nada.
La paginación
Las listas devuelven { data, has_more, next_cursor }. Pasa next_cursor en ?cursor= para la página siguiente.
Sin número de página, a propósito: en una lista donde se escribe mientras se lee — que es exactamente el caso de una API — page=2 salta líneas y muestra otras dos veces. El cursor, en cambio, no se mueve.
Las campañas
Crear y enviar son dos gestos separados. No es una molestia: es lo que permite hacer revisar una carta antes de que salga a tres mil personas.
# 1. Le brouillon — rien ne part
curl -X POST https://plumail.fr/api/v1/campaigns \
-H "Authorization: Bearer plm_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://plumail.fr/api/v1/campaigns/CAMP_ID/send \
-H "Authorization: Bearer plm_live_…"
# 3. Le suivi
curl https://plumail.fr/api/v1/campaigns/CAMP_ID \
-H "Authorization: Bearer plm_live_…"
En text, una línea vacía separa dos párrafos y # al inicio de línea hace un título. Para el maquetado completo (imágenes, botones, separadores), pasa content con los bloques del editor.
El envío responde 202 con queued: los destinatarios quedan fijados, los mensajes salen después al ritmo autorizado por nuestro proveedor. Un envío de cincuenta mil correos no cabe en una solicitud HTTP, y pretender lo contrario te daría un «enviado» para un trabajo que apenas comienza.
Las tasas de apertura y de clic se calculan sobre los mensajes entregados, nunca sobre el número de destinatarios: una dirección muerta no debe hacer bajar la tasa de quienes sí recibieron.
El informe completo
GET /api/v1/campaigns/CAMP_ID/stats devuelve todo lo que la ficha de campaña de Plumail muestra, para mostrarlo en tu propio software — un CRM, un panel de control:
{
"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>…"
}
Las proporciones (rates, share, proxiedShare) están entre 0 y 1, y null mientras no haya nada que dividir. timeline cuenta las aperturas y los clics por tramos de seis horas durante las 48 primeras horas tras el envío — vacío mientras la campaña no haya salido. audience solo conserva las cinco primeras líneas de cada desglose; proxiedShare es la proporción de aperturas provenientes de un relé de privacidad (Apple Mail, Gmail): recibidas, no necesariamente leídas. links va de lo más a lo menos clicado. html es el mensaje tal como salió, cadena vacía si no.
Los errores
Siempre la misma forma, con dos lecturas posibles del mismo contenido:
{
"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 es la forma de Plumail: estructurada, con en details lo necesario para corregir. Los tres campos planos — statusCode, message, name — son los de Resend, y están ahí para que el código escrito contra la antigua API muestre un mensaje correcto sin ser releído.
Escribe tu lógica contra code (o name, es el mismo valor), nunca contra message. El mensaje está hecho para ser leído por un humano, y nos permitimos reformularlo.
| Código | Estado | Qué significa |
|---|---|---|
missing_api_key | 401 | Sin cabecera Authorization. |
invalid_api_key | 401 | Llave desconocida. |
revoked_api_key | 401 | Llave cortada en los ajustes. |
insufficient_scope | 403 | La llave no tiene el permiso solicitado. |
reputation_blocked | 403 | Tus envíos están suspendidos: demasiados rebotes o quejas. |
quota_exceeded | 402 | La cuota mensual del plan está alcanzada. |
not_found | 404 | El objeto no existe en este espacio. |
conflict | 409 | Estado incompatible: campaña ya enviada, suscriptor salido. |
idempotency_key_reused | 409 | Mismo Idempotency-Key, cuerpo diferente. |
validation_error | 422 | Un campo falta o está mal formado. |
unverified_from_domain | 422 | El dominio de from no está verificado. |
suppressed_recipient | 422 | Un destinatario está en la lista de supresión. |
rate_limit_exceeded | 429 | Más de 600 solicitudes por minuto (ver Retry-After). |
send_failed | 502 | Nuestro proveedor de envío ha rechazado el mensaje. |
internal_error | 500 | Una avería de nuestro lado. |
Límite de velocidad
600 solicitudes por minuto y por llave. Cada respuesta lleva X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset; un rechazo lleva además Retry-After, en segundos.
¿Necesitas más? Escríbenos: miramos tu caso en lugar de dejarte reintentar en bucle.
El servidor MCP
MCP — Model Context Protocol — es la forma en que un agente de IA descubre las herramientas de un software y las usa. Plumail expone un servidor MCP alojado: nada que instalar, una dirección y tu llave.
Claude Code
claude mcp add plumail \
--transport http \
--url https://plumail.fr/api/mcp \
--header "Authorization: Bearer plm_live_…"
ChatGPT, Cursor, Codex, Claude Desktop — todos leen la misma ficha de conector:
{
"mcpServers": {
"plumail": {
"type": "http",
"url": "https://plumail.fr/api/mcp",
"headers": { "Authorization": "Bearer plm_live_…" }
}
}
}
Luego, en tu agente: «¿Qué espacio Plumail ves, y qué dominios de envío están verificados?». Llamará a get_account, que no modifica nada — es la forma correcta de verificar una conexión.
Las herramientas expuestas
| Herramienta | Qué hace |
|---|---|
get_account | El espacio, los permisos, la cuota restante, los dominios verificados. |
send_email | Envía un correo individual. Irreversible. |
get_email | El estado de un correo enviado. |
list_subscribers | Lista los suscriptores, página por página. |
add_subscriber | Añade o actualiza un suscriptor. |
remove_subscriber | Da de baja y descarta la dirección. Irreversible. |
list_suppression | Las direcciones que ya no recibirán nada. |
add_suppression | Descarta una dirección. Irreversible. |
list_campaigns | Las campañas del espacio. |
create_campaign | Crea un borrador. Nada sale. |
send_campaign | Envía a todos los suscriptores activos. Irreversible. |
get_campaign_stats | Cifras y estado de una campaña. |
Cada herramienta es una llamada a la API anterior, nada más: mismos permisos, misma cuota, misma lista de supresión, mismos rechazos. Un segundo camino de acceso con su propia lógica sería un segundo conjunto de reglas, y el día en que uno de los dos cambiara, MCP se convertiría en la puerta de servicio.
Ninguna herramienta retira una dirección de la lista de supresión. Es el único gesto del producto que suspende una capacidad de envío, y un agente al que se le dijera «limpia la lista» lo haría sin dudar. Se hace a mano, en tu espacio.
El recurso plumail://docs da al agente la referencia completa: no necesita conocerla de antemano.
Lo que cambia si vienes de Resend
Los campos de POST /v1/emails y la respuesta { id } son los mismos. En la práctica: la dirección base y la llave.
Dos formas de migrar.
Con el cliente mínimo — un archivo para copiar, sin dependencias, la misma firma que el SDK de Resend. Consíguelo: plumail.fr/plumail-client.ts.
// avant
const resend = new Resend(process.env.RESEND_API_KEY);
// après
const plumail = new Plumail(process.env.PLUMAIL_API_KEY);
// le reste de votre code ne bouge pas
const { data, error } = await plumail.emails.send({ from, to, subject, html, text });
if (error) throw new Error(\`Email delivery failed: ${error.message}\`);
return { providerId: data?.id ?? null };
Nunca lanza una excepción: una avería de red se convierte también en un error, con name: "network_error". Es intencionado — un método que lanzara excepción donde el anterior devolvía un objeto transformaría «cambiar dos líneas» en «releer cada llamada», y las llamadas que olvidamos releer son precisamente los caminos de error.
Sin copiar nada — un fetch desnudo es suficiente:
const res = await fetch("https://plumail.fr/api/v1/emails", {
method: "POST",
headers: {
Authorization: \`Bearer ${process.env.PLUMAIL_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 en français, lisible
const id = body.id;
Tres diferencias que debes conocer, todas asumidas:
- El dominio de
fromdebe verificarse en tu espacio Plumail, no en Resend. Añádelo en Dominios y configura los registros DNS. - La lista de exclusión se aplica a los envíos individuales. Resend no lo hace. Una dirección que se haya dado de baja de tu boletín no recibirá tampoco tus correos transaccionales desde el mismo espacio — si no es lo que quieres, separa ambos en dos espacios.
- La cuota mensual se comparte con tus campañas.
Dónde viven estos correos
Los correos enviados por la API no se unen a tus campañas: viven aparte, y esto no es un detalle técnico.
Un destinatario de factura no es un suscriptor. Clasificarlo con tus suscriptores lo habría inscrito en tu lista sin que jamás haya consentido recibir tu boletín — contado en tu panel de control, y objetivo de tu próxima campaña. Tus cifras de suscriptores siguen siendo, por tanto, las de tus suscriptores reales.
Lo que sí se comparte, en cambio: la cuota mensual, la lista de exclusión y el monitoreo de rebotes. Son las tres cosas que comprometen tu reputación como remitente, y es la misma en ambos lados.
La descripción técnica
El archivo OpenAPI 3.1 se sirve tal cual: plumail.fr/openapi.json. Describe cada punto de entrada, cada campo y cada error — suficiente para generar un cliente en tu lenguaje, o entregárselo a un agente.