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énDirección
API RESTPara código — cualquier lenguaje sabe hacer una solicitud HTTP.https://plumail.fr/api/v1
Servidor MCPPara 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

PermisoQué abre
emails:sendEnviar correos individuales y leer su estado.
subscribers:readLeer los suscriptores y la lista de supresión.
subscribers:writeAñadir, modificar y dar de baja a suscriptores.
campaigns:readLeer las campañas y sus estadísticas.
campaigns:writeCrear 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ódigoEstadoQué significa
missing_api_key401Sin cabecera Authorization.
invalid_api_key401Llave desconocida.
revoked_api_key401Llave cortada en los ajustes.
insufficient_scope403La llave no tiene el permiso solicitado.
reputation_blocked403Tus envíos están suspendidos: demasiados rebotes o quejas.
quota_exceeded402La cuota mensual del plan está alcanzada.
not_found404El objeto no existe en este espacio.
conflict409Estado incompatible: campaña ya enviada, suscriptor salido.
idempotency_key_reused409Mismo Idempotency-Key, cuerpo diferente.
validation_error422Un campo falta o está mal formado.
unverified_from_domain422El dominio de from no está verificado.
suppressed_recipient422Un destinatario está en la lista de supresión.
rate_limit_exceeded429Más de 600 solicitudes por minuto (ver Retry-After).
send_failed502Nuestro proveedor de envío ha rechazado el mensaje.
internal_error500Una 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

HerramientaQué hace
get_accountEl espacio, los permisos, la cuota restante, los dominios verificados.
send_emailEnvía un correo individual. Irreversible.
get_emailEl estado de un correo enviado.
list_subscribersLista los suscriptores, página por página.
add_subscriberAñade o actualiza un suscriptor.
remove_subscriberDa de baja y descarta la dirección. Irreversible.
list_suppressionLas direcciones que ya no recibirán nada.
add_suppressionDescarta una dirección. Irreversible.
list_campaignsLas campañas del espacio.
create_campaignCrea un borrador. Nada sale.
send_campaignEnvía a todos los suscriptores activos. Irreversible.
get_campaign_statsCifras 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 from debe 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.