Mailcheer
Envía correos transaccionales y campañas de newsletter desde un agente: suscriptores, segmentos, lista de supresión, envío y estadísticas — con una sola clave de API. Funciona en Amazon SES, alojado en Europa.
Servidor MCP alojado
npx add-mcp 'https://mailcheer.com/api/mcp'Se instala en Claude Code, Codex, Cursor y más
Documentación
API de correo y servidor MCP
El servidor REST API y MCP de Mailcheer: envía correos transaccionales, gestiona suscriptores, crea y envía campañas, todo desde tu aplicación o agente de IA.
Mailcheer es controlable desde el exterior: desde tu aplicación, desde un script, o desde un agente como Claude Code, ChatGPT o Codex. Dos puntos de entrada, una clave.
| Para quién | Dirección | |
|---|---|---|
| REST API | Código — cualquier lenguaje que pueda hacer una solicitud HTTP. | https://mailcheer.com/api/v1 |
| Servidor MCP | Agentes de IA, que descubren las herramientas disponibles por sí mismos. | https://mailcheer.com/api/mcp |
Tu primera clave
En tu espacio de trabajo de Mailcheer: Cuenta → API y agentes de IA → Nueva clave. Ponle un nombre y marca lo que se le permite hacer.
La clave completa se muestra una sola vez. Solo conservamos una huella digital: si la pierdes, nadie puede recuperarla por ti — crea una nueva y revoca la antigua. Este es el precio de garantizar que una copia robada de nuestra base de datos no produzca ninguna clave 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.
Permisos de la clave
| Permiso | Qué desbloquea |
|---|---|
emails:send | Enviar correos transaccionales y leer su estado. |
subscribers:read | Leer suscriptores y la lista de supresión. |
subscribers:write | Añadir, actualizar y dar de baja suscriptores. |
campaigns:read | Leer campañas y sus estadísticas. |
campaigns:write | Crear y enviar campañas, eliminar un borrador. |
webhooks:read | Leer suscripciones a eventos y su registro. |
webhooks:write | Crear, editar y eliminar suscripciones a eventos. |
Marca solo lo que necesites. Una llamada fuera del ámbito de la clave devuelve 403, y nada lo elude — es la única protección que se mantiene frente a un agente autónomo: no cuentas con su cautela, le quitas el botón.
Los permisos se eligen en la creación y nunca cambian. Una clave cuyo ámbito pueda ampliarse después no significa nada: la persona que la recibió cree que tiene acceso de solo lectura y termina con derechos de envío, sin que se le informe.
Enviar un correo
El punto de entrada más común: la factura, la alerta, el restablecimiento de contraseña — todo lo que tu aplicación escribe a una persona a la vez.
curl -X POST https://mailcheer.com/api/v1/emails \
-H "Authorization: Bearer mch_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-2026-0412" \
-d '{
"from": "Your brand <hello@yourdomain.com>",
"to": "customer@example.com",
"subject": "Your September invoice",
"html": "<p>Here it is.</p>"
}'
La respuesta vuelve como 202:
{
"id": "cmu651xf200021n7nm68sikfw",
"object": "email",
"from": "hello@yourdomain.com",
"to": ["customer@example.com"],
"subject": "Your September invoice",
"created_at": "2026-09-18T07:12:44.102Z"
}
202, no 200: nuestro proveedor de envío ha aceptado el mensaje; aún no está en una bandeja de entrada. La entrega se confirma unos segundos después:
curl https://mailcheer.com/api/v1/emails/cmu651xf200021n7nm68sikfw \
-H "Authorization: Bearer mch_live_…"
El campo status pasa de sent a delivered, o a bounced si la dirección no existe, o a complained si la persona marcó el mensaje como spam. En ambos últimos casos, la dirección se añade automáticamente a la lista de supresión — tu aplicación no necesita gestionar eso.
Cancelación de suscripción con un clic
Si escribes a personas que no te escribieron primero — un boletín, una alerta a la que alguien se suscribió, una página de estado — Gmail y Yahoo esperan un enlace de cancelación de suscripción en las cabeceras del mensaje, no solo al pie de la página. Lo exigen desde febrero de 2024.
Pasa la dirección en unsubscribe_url, y Mailcheer establece ambas cabeceras, que siempre van juntas:
curl -X POST https://mailcheer.com/api/v1/emails \
-H "Authorization: Bearer mch_live_…" \
-H "Content-Type: application/json" \
-d '{
"from": "Your brand <alerts@yourdomain.com>",
"to": "customer@example.com",
"subject": "Your monthly report",
"html": "<p>Here it is.</p>",
"unsubscribe_url": "https://yourdomain.com/unsubscribe/abc123"
}'
Tu endpoint debe aceptar un POST y cancelar la suscripción sin pedir confirmación — eso es lo que significa un clic. Un GET en la misma dirección puede llevar a una página legible, para clientes de correo que hagan cualquiera de las dos cosas.
Sin esta cabecera, la única salida que ofreces es el botón de Spam — y es la reputación de tu dominio de envío la que lo paga, no la del mensaje.
⚠️ List-Unsubscribe sigue siendo rechazado dentro de headers: Mailcheer lo escribe, tú solo proporcionas el destino. Eso garantiza que List-Unsubscribe-Post siempre venga con él — sin esa segunda cabecera, Gmail no muestra ningún botón.
Archivos adjuntos
Una cotización, una factura, un folleto: pásalos en attachments, en el formato de Resend — filename y content, el archivo codificado en base64.
curl -X POST https://mailcheer.com/api/v1/emails \
-H "Authorization: Bearer mch_live_…" \
-H "Content-Type: application/json" \
-d '{
"from": "Your brand <hello@yourdomain.com>",
"to": "client@example.com",
"subject": "Your quote",
"text": "The quote is attached.",
"attachments": [
{
"filename": "quote-2026-09.pdf",
"content": "JVBERi0xLjQKJcfsj6IK…",
"content_type": "application/pdf"
}
]
}'
En Node, el contenido ocupa una línea:
import { readFileSync } from "node:fs";
const quote = {
filename: "quote-2026-09.pdf",
content: readFileSync("./quote-2026-09.pdf").toString("base64"),
};
content_type es opcional: se infiere de la extensión (.pdf → application/pdf), con application/octet-stream como alternativa. Veinte archivos adjuntos por mensaje como máximo.
El límite es el de nuestro proveedor de envío: 40 MB por mensaje una vez codificado, que son aproximadamente 30 MB de archivos reales — base64 añade un tercio. Más allá de eso, la llamada se rechaza con un 422, nombrando el archivo, su tamaño y el tamaño alcanzado. Esto es deliberado: una negativa que explica algo es mejor que un mensaje que sale sin su archivo adjunto.
Rechazados de la misma manera, y siempre en voz alta:
- extensiones que los proveedores de bandeja de entrada rechazan —
.exe,.bat,.js,.vbs,.scr… Pon el archivo en un.zip, o envía un enlace de descarga; - un
contentque no sea base64 válido; - un campo
pathque apunte a una URL para obtener: nuestro servidor no sigue una dirección que tú elijas. Codifica el archivo.
Cuando un mensaje lleva un archivo adjunto, sale como un mensaje MIME completo en lugar de uno simple. Todo lo demás no cambia: cc, bcc, reply_to, tus cabeceras, la cancelación de suscripción con un clic y tus etiquetas se comportan de manera idéntica — y bcc sigue sin aparecer en ninguna cabecera del mensaje recibido.
Copias
cc y bcc aceptan una dirección o un array de 1 a 50, igual que to. Ambas cuentan para tu cuota y pasan por las mismas comprobaciones — una dirección suprimida rechaza toda la llamada, ya sea destinatario o copia.
Seguimiento de aperturas
Un correo HTML lleva una imagen invisible de un píxel que cuenta las aperturas. Sin track_opens en la solicitud, el ajuste Seguimiento de aperturas de tu espacio de trabajo decide (pantalla Espacio de trabajo de la aplicación); track_opens: true o false lo anula para ese correo. Un correo de texto plano no lleva píxel. GET /api/v1/emails/{id} devuelve track_opens: cuando es false, opened_at permanece null porque las aperturas no se rastrean, no porque nadie abrió.
En Francia, la recomendación de la CNIL del 14 de abril de 2026 hace que medir aperturas con un píxel, para rastrear el rendimiento de una campaña, esté sujeto al consentimiento previo del destinatario. Un código de inicio de sesión o un restablecimiento de contraseña no tiene razón para ser rastreado: envía track_opens: false.
Las cinco reglas de cada envío
No se pueden eludir, y son las mismas que para una campaña enviada a través de la interfaz.
1. from debe estar en un dominio verificado en tu espacio de trabajo. De lo contrario, 422 unverified_from_domain, con una lista de tus dominios verificados en el mensaje. GET /api/v1/me también los devuelve.
2. Una dirección en la lista de supresión es rechazada, con su motivo — baja, dirección muerta, queja. Toda la llamada completa falla, incluidos otros destinatarios: un envío parcial del que no sabes es el peor resultado posible, porque pensarías que has notificado a todos.
3. La cuota mensual de tu plan cuenta estos envíos igual que las campañas — y lo que ya está esperando en la cola. Es el mismo recuento de envíos, la misma factura. Un envío que no cabe devuelve 402 quota_exceeded (ver más abajo). En el plan gratuito, los correos gratuitos de un dominio de envío sirven a un espacio de trabajo al mes: en otros lugares, es 402 free_plan_domain_used.
4. Una tasa de rebote o queja 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 el mismo daño que una campaña en una lista comprada.
5. Nada elude el doble opt-in. Un suscriptor añadido a través de la API recibe una confirmación, a menos que double_opt_in: false sea explícito — y entonces tú asumes la responsabilidad del consentimiento. La misma dirección recibe como máximo una confirmación cada 2 minutos y 3 por 24 horas, todos los canales combinados (formulario, API, MCP): una nueva llamada actualiza el registro sin enviar otro correo, y la respuesta dice por qué (confirmation_sent: false, confirmation_not_sent_reason, confirmation_retry_at). Más allá de eso, cada recordatorio es una posible queja contra tu dominio.
Dónde está tu espacio de trabajo
GET /api/v1/me es la primera llamada que hacer, y el reflejo correcto antes de un envío importante: dice a quién pertenece la clave, desde qué direcciones escribir — y si el envío cabrá. No se requiere ningún permiso particular.
curl https://mailcheer.com/api/v1/me -H "Authorization: Bearer mch_live_…"
{
"object": "account",
"organization": { "id": "org_3f9", "name": "Your brand", "slug": "your-brand" },
"key": { "name": "Production", "scopes": ["emails:send", "subscribers:read"] },
"plan": { "id": "free", "name": "Découverte", "emails_per_month": 3000 },
"usage": {
"period": "2026-09",
"emails_sent": 2410,
"emails_in_flight": 120,
"emails_remaining": 470,
"resets_at": "2026-10-01T00:00:00.000Z"
},
"billing": {
"status": "none",
"subscribed_plan": null,
"current_period_end": null,
"cancel_at_period_end": false,
"scheduled_change": null,
"manage_url": "https://mailcheer.com/reglages/facturation"
},
"limits": {
"members": { "used": 1, "pending_invitations": 0, "max": 1 },
"sending_domains": { "used": 1, "max": 1 },
"daily": null
},
"subscribers": 551,
"sending_domains": [{ "domain": "yourdomain.com", "verified": true }],
"senders": [{ "id": "snd_71a", "from": "hello@yourdomain.com", "name": "Your brand", "default": true }]
}
usage—emails_sent: lo que salió este mes, todos los canales juntos.emails_in_flight: lo que espera en la cola (una campaña en curso, envíos de automatización reservados) — ya prometido.emails_remaining: lo que aún se puede enviar, cola deducida, nunca negativo (nullen un plan ilimitado).resets_at: cuándo se reinicia el contador, el 1 del próximo mes a las 00:00 UTC.billing—statusesnonesin una suscripción de pago; de lo contrario, el estado del pago:active,past_due(un cargo falló, el plan sigue abierto mientras reintenta),unpaid(reintentos abandonados: el espacio de trabajo funciona con los límites de Discovery),canceled…subscribed_plannombra el plan que se factura — puede diferir deplan.iddespués de un pago fallido.scheduled_changeanuncia una degradación o cancelación programada ({ "plan": "free", "effective_at": "…" }).manage_urles la pantalla donde el propietario o un administrador cambia de plan.limits— miembros (una invitación pendiente ocupa un asiento), dominios de envío, ydaily: el límite de un nuevo espacio de trabajo en un período móvil de 24 horas (100 correos durante los primeros tres días, 500 hasta el séptimo),nullcuando no aplica.
El software conectado a Mailcheer — un CRM que envía por sus usuarios, por ejemplo — puede entonces mostrar "Quedan 470 correos hasta el 1 de octubre" en lugar de descubrir la negativa. El propietario del espacio de trabajo y los administradores reciben un correo al 50%, 80% y 95% de la cuota, una vez al mes cada uno — sin correo al 100%: la negativa lo dice.
La cuota en cada respuesta
No es necesario llamar a GET /api/v1/me antes de cada envío: cada respuesta autenticada de la API — éxito o error, 402 incluido — y del servidor MCP lleva el estado de la cuota de este mes.
| Cabecera | Valor |
|---|---|
Mailcheer-Quota-Limit | Correos al mes en tu plan, o unlimited. |
Mailcheer-Quota-Used | Enviados este mes más lo que espera en la cola (emails_sent + emails_in_flight). |
Mailcheer-Quota-Remaining | Lo que aún se puede enviar, cola deducida, nunca negativo — o unlimited. |
Mailcheer-Quota-Reset | Cuándo se reinicia el contador, en ISO 8601: el 1 del próximo mes, 00:00 UTC. |
curl -i https://mailcheer.com/api/v1/emails -H "Authorization: Bearer mch_live_…" …
# HTTP/1.1 202 Accepted
# Mailcheer-Quota-Limit: 3000
# Mailcheer-Quota-Used: 2531
# Mailcheer-Quota-Remaining: 469
# Mailcheer-Quota-Reset: 2026-10-01T00:00:00.000Z
La respuesta a un envío aceptado ya cuenta ese envío. Una respuesta sin clave válida (401) no lleva ninguna: no conoce tu espacio de trabajo. Si la cuota no se puede leer en ese momento, la respuesta sale igualmente, sin estas cabeceras.
Recibir notificaciones: el webhook quota.threshold_reached
Suscribe una dirección al evento quota.threshold_reached (POST /api/v1/webhooks, o Configuración → API): Mailcheer lo llama cuando la cuota de este mes alcanza 50, 80, 95 y 100%, una vez al mes por umbral, en el momento en que se acepta el correo que cruza el umbral. Si un solo envío cruza varios, solo se envía el más alto. El cuerpo está firmado y se reintenta como cada evento:
{
"id": "evt_3kT9xQ2mV7aB1cD4",
"type": "quota.threshold_reached",
"created_at": "2026-09-24T16:02:11.000Z",
"data": {
"threshold": 80,
"plan": "free",
"quota": 3000,
"sent": 2400,
"in_flight": 35,
"remaining": 565,
"resets_at": "2026-10-01T00:00:00.000Z",
"period": "2026-09"
}
}
threshold es el umbral alcanzado, en porcentaje; los otros campos significan lo que significan en el details de una negativa 402. Al 100%, solo se dispara el webhook (sin correo): a partir de entonces, los envíos devuelven 402 quota_exceeded hasta resets_at.
Cuando la cuota no cubre un envío
Un envío que no cabe en lo que queda este mes es rechazado por completo, antes de que salga nada: nada se envía, nada se pone en cola. La respuesta es un 402 con código quota_exceeded, en POST /api/v1/emails como en POST /api/v1/campaigns/CAMP_ID/send, y la herramienta MCP que hace el mismo movimiento devuelve el mismo error:
{
"error": {
"code": "quota_exceeded",
"message": "…",
"details": {
"plan": "free",
"quota": 3000,
"sent": 2940,
"in_flight": 20,
"remaining": 40,
"requested": 250,
"resets_at": "2026-10-01T00:00:00.000Z"
}
},
"statusCode": 402,
"message": "…",
"name": "quota_exceeded"
}
remaining es quota − sent − in_flight; requested es lo que la llamada solicitó (destinatarios, copias incluidas). Hay dos caminos: cambiar el plan (billing.manage_url), o esperar a resets_at. Reintentar la misma llamada antes de cualquiera de los dos obtendrá la misma negativa.
El plan gratuito: un dominio, un espacio de trabajo por mes
En el plan gratuito, los 3.000 correos del mes están vinculados al dominio de envío, no a la cuenta. Un dominio registrado (acme.com, subdominios incluidos) sirve el plan gratuito a un único espacio de trabajo por mes UTC: el primero que envíe con él. Otro espacio de trabajo gratuito que envíe desde ese dominio, o desde uno de sus subdominios, en el mismo mes recibe un 402 diferente, rechazado también por completo:
{
"error": {
"code": "free_plan_domain_used",
"message": "The free plan is per sending domain, not per account: this domain (news.acme.com, part of acme.com) has already used it this month in another workspace. Upgrade to a paid plan to send from several workspaces, or wait until October 1 at 00:00 UTC.",
"details": {
"domain": "news.acme.com",
"root_domain": "acme.com",
"period": "2026-09",
"resets_at": "2026-10-01T00:00:00.000Z"
}
},
"statusCode": 402,
"message": "…",
"name": "free_plan_domain_used"
}
Distinga los dos 402 mediante error.code. Actualizar a un plan de pago levanta esta restricción de inmediato; los planes de pago no se ven afectados.
Nunca envíe dos veces
Una biblioteca HTTP que no recibió nuestra respuesta reproducirá la llamada. Ese es su trabajo, y sin una precaución su cliente recibe la misma factura dos veces.
Añada el encabezado Idempotency-Key con un valor único por envío — el número de factura, el ID de pedido, un UUID:
Idempotency-Key: invoice-2026-0412
Reproducir la misma llamada devuelve la misma respuesta, con el mismo id, sin un segundo envío. El encabezado Idempotent-Replay: true le indica que fue una reproducción. La misma clave con un cuerpo diferente devuelve 409: eso no es un reintento, es un error de su lado, y devolver la respuesta del otro envío sería peor que decirlo.
Suscriptores
# Add — the person receives a confirmation and enters "pending"
curl -X POST https://mailcheer.com/api/v1/subscribers \
-H "Authorization: Bearer mch_live_…" \
-H "Content-Type: application/json" \
-d '{"email":"marie@example.com","firstName":"Marie","tags":["customers"]}'
# List, page by page
curl "https://mailcheer.com/api/v1/subscribers?limit=50&status=subscribed" \
-H "Authorization: Bearer mch_live_…"
# Unsubscribe
curl -X DELETE https://mailcheer.com/api/v1/subscribers/marie%40example.com \
-H "Authorization: Bearer mch_live_…"
DELETE no borra el registro: la persona pasa a unsubscribed y su dirección entra en la lista de supresión. Eliminar el registro permitiría que reapareciera en la próxima importación de archivo — habría respetado el verbo HTTP y traicionado a la persona.
Una dirección dada de baja no puede volver a suscribirse mediante la API. Solo la persona puede regresar, a través de un formulario. Una baja que un programa pueda deshacer no vale nada.
Paginación
Las listas devuelven { data, has_more, next_cursor }. Pase next_cursor como ?cursor= para la siguiente página.
Sin número de página, por diseño: en una lista donde las escrituras ocurren al mismo tiempo que las lecturas — que es exactamente el caso de una API — page=2 omite filas y muestra otras dos veces. Un cursor no se mueve.
Campañas
Crear y enviar son dos acciones separadas. No es burocracia: es lo que le permite revisar una carta antes de que llegue a tres mil personas.
# 1. The draft — nothing is sent
curl -X POST https://mailcheer.com/api/v1/campaigns \
-H "Authorization: Bearer mch_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "September newsletter",
"subject": "What we learned this summer",
"text": "# Hello\n\nHere is this month'\''s news."
}'
# 2. Send — irreversible
curl -X POST https://mailcheer.com/api/v1/campaigns/CAMP_ID/send \
-H "Authorization: Bearer mch_live_…"
# 3. Track
curl https://mailcheer.com/api/v1/campaigns/CAMP_ID \
-H "Authorization: Bearer mch_live_…"
En text, una línea en blanco separa dos párrafos y # al inicio de una línea crea un encabezado. Para un diseño completo (imágenes, botones, divisores), pase content con los bloques del editor.
El envío devuelve 202 con queued: los destinatarios quedan bloqueados, los mensajes se envían luego a la velocidad que permite nuestro proveedor. Un envío de cincuenta mil correos no cabe en una sola solicitud HTTP, y afirmar lo contrario le daría un "enviado" para un trabajo que apenas comienza.
Las tasas de apertura y clics se calculan sobre los mensajes entregados, nunca sobre el número total de destinatarios: una dirección muerta no debe arrastrar hacia abajo la tasa de quienes sí recibieron el mensaje.
Las aperturas solo cuentan en mensajes que llevaban el píxel de seguimiento. Cuando la opción Seguimiento de aperturas del espacio de trabajo estuvo desactivada durante todo el envío, opens_tracked es false y open_rate y human_open_rate son null — nunca un engañoso 0%.
Cada tasa viene dos veces: open_rate y click_rate incluyen bots, como los cuentan la mayoría de herramientas; human_open_rate y human_click_rate los apartan. Un bot es una apertura o un clic dentro de los dos minutos posteriores a la entrega (las pasarelas de seguridad de los buzones empresariales visitan cada enlace cuando llega el mensaje, los relés de privacidad precargan las imágenes), o uno de un robot que se declara en su agente de usuario, o de un rango de direcciones que su operador publica (Google, Bing). Los relés de privacidad (Apple Mail, Gmail, Yahoo) no son bots en sí mismos. La regla es deliberadamente estricta: una persona que abre dentro del minuto se cuenta como bot — una tasa ligeramente baja en lugar de una inflada.
Eliminar un borrador
curl -X DELETE https://mailcheer.com/api/v1/campaigns/CAMP_ID \
-H "Authorization: Bearer mch_live_…"
Devuelve { "object": "campaign", "id": "…", "deleted": true }. Solo un borrador (draft) se puede eliminar, y no se puede deshacer. Una campaña programada, en envío, enviada o archivada responde 409 conflict, con su estado en details.status: lo que ya salió, o saldrá, conserva sus envíos y sus estadísticas.
Escribir a un grupo en lugar de a toda la lista
Sin un cuerpo, el envío va a toda la audiencia de la campaña: cada suscriptor activo del espacio de trabajo (o del segmento elegido en la interfaz), menos la lista de supresión. Para escribir solo a una parte — personas que no han respondido, sus clientes, quienes se apuntaron a un taller — pase sus direcciones en 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": ["claire@example.com", "marc@example.com", "former@example.com"] }'
to solo puede reducir. La carta va a las direcciones solicitadas que también sean suscriptores activos de la audiencia, y nunca a una dirección en la lista de supresión: alguien que se dio de baja, rebotó o se quejó no recibe nada, incluso si su dirección está en to. La respuesta dice quién se conserva, quién se excluye y por qué:
{
"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": "former@example.com", "reason": "unsubscribed" } ]
},
"note": "2 recipient(s) queued. …"
}
El recuento siempre cuadra: requested = duplicates + retained + excluded. Se ignoran mayúsculas y espacios circundantes (Claire@Example.com es la misma persona). Las razones: invalid (no es una dirección), not_in_list (ningún suscriptor del espacio de trabajo la tiene), pending (registro aún no confirmado), unsubscribed, bounced, complained, suppressed (suscrito, pero en la lista de supresión) y outside_segment (fuera del segmento que la campaña apunta).
Tres reglas que vale la pena conocer:
- Una lista vacía no es "sin lista".
"to": []escribe a nadie: el envío se rechaza (422). Para escribir a toda la lista, no envíe ningún campoto. - Sin
toen una campaña con prueba A/B. La versión ganadora sale horas después, calculada sobre toda la audiencia de la campaña; la lista de direcciones no sobreviviría a eso. El envío se rechaza en lugar de ir a todos. - Los campos desconocidos se ignoran, excepto los que se parecen a
dry_runyto.dryRun,dry-run,DRY_RUN,test,simulate,preview… yTo,TO,recipients,emails,to_emails,destinataires,adresses,audience… se rechazan (422), nombrando el campo correcto: ignorados, los primeros enviarían la campaña de verdad, los segundos la enviarían a toda la lista.POST /api/v1/audiencerechaza cualquier campo desconocido.
Hasta 50.000 direcciones por llamada.
Vista previa antes de enviar
"dry_run": true ejecuta todas las comprobaciones de un envío real — remitente, contenido, reputación, cuota, destinatarios — y no pone nada en cola. La respuesta es 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 el envío real sería rechazado, would_send es false y blocked_reason da el mensaje exacto que devolvería. Ese es el número que debe mostrar a la persona antes de que confirme.
Para hacer la misma pregunta antes de que la campaña exista — mientras alguien elige a quién escribir, en su propio software — POST /api/v1/audience devuelve el mismo desglose sin crear nada (alcance subscribers:read):
curl -X POST https://mailcheer.com/api/v1/audience \
-H "Authorization: Bearer mch_live_…" \
-H "Content-Type: application/json" \
-d '{ "to": ["claire@example.com", "marc@example.com"] }'
# → { "object": "audience", "recipients": 2, "audience": { … } }
Sin to, devuelve a cuántos suscriptores llegaría una campaña enviada a toda la lista. Las comprobaciones específicas de campaña (asunto, contenido, cuota) siguen siendo las de dry_run.
El informe completo
GET /api/v1/campaigns/CAMP_ID/stats devuelve todo lo que muestra la vista de campaña de Mailcheer, para que pueda mostrarlo en su propio software — un CRM, un panel:
{
"campaign": { "id": "…", "name": "…", "subject": "…", "status": "sent", "kind": "newsletter",
"fromName": "…", "fromEmail": "…", "sentAt": "…", "scheduledAt": null, "updatedAt": "…" },
"counts": { "recipients": 66, "delivered": 60, "opened": 30, "clicked": 10,
"humanOpened": 22, "humanClicked": 7,
"bounced": 6, "complained": 1, "unsubscribed": 0 },
"rates": { "delivered": 0.9091, "opened": 0.5, "clicked": 0.1667,
"humanOpened": 0.3667, "humanClicked": 0.1167, "bounced": 0.0909 },
"bots": { "opened": 9, "clicked": 4, "delay": 12, "scanner": 1 },
"timeline": [ { "label": "+0h", "opened": 12, "clicked": 5, "humanOpened": 4, "humanClicked": 1 }, … ],
"audience": { "total": 30, "proxiedShare": 0.4,
"device": [ { "label": "Phone", "count": 15, "share": 0.5 }, … ],
"os": [ … ], "client": [ … ] },
"links": [ { "url": "https://…", "clicks": 6 }, … ],
"html": "<!doctype html>…"
}
Las tasas (rates, share, proxiedShare) están entre 0 y 1, y null mientras no haya nada que dividir. opened y clicked incluyen bots, humanOpened y humanClicked los apartan; untracked cuenta mensajes entregados que no llevaban píxel de seguimiento de aperturas — las cifras y tasas de apertura cubren solo los demás, y rates.opened es null cuando ninguno lo llevaba; bots dice cuántas aperturas y clics se apartaron, contados como eventos, y por qué (delay: dentro de dos minutos de la entrega; scanner: un robot que se declara o una dirección que su operador publica). timeline cuenta aperturas y clics en ventanas de seis horas durante las primeras 48 horas tras el envío, con y sin bots — vacío hasta que la campaña haya salido. audience cuenta solo aperturas de personas y conserva las cinco primeras filas de cada desglose; proxiedShare es la proporción de esas aperturas provenientes de un relé de privacidad (Apple Mail, Gmail): recibido, no necesariamente leído. links está ordenado de más a menos clics, por personas. html es el mensaje tal como se envió, cadena vacía en caso contrario.
Quién abrió: los destinatarios
GET /api/v1/campaigns/CAMP_ID/recipients devuelve una fila por persona a la que fue la campaña, con paginación por cursor como /subscribers (?limit= hasta 100, ?cursor= = el next_cursor de la página anterior):
curl "https://mailcheer.com/api/v1/campaigns/CAMP_ID/recipients?status=opened" \
-H "Authorization: Bearer mch_live_…"
{
"object": "list",
"data": [
{ "object": "recipient", "id": "…", "email": "claire@example.com",
"first_name": "Claire", "last_name": "Martin", "status": "clicked",
"sent_at": "…", "delivered_at": "…", "opened_at": "…", "clicked_at": "…",
"human_opened": true, "human_clicked": true, "unsubscribed": false },
{ "object": "recipient", "id": "…", "email": null, … }
],
"has_more": true,
"next_cursor": "…"
}
?status= filtra por opened, clicked, not_opened (enviado, no rebotado, nunca abierto), bounced, complained o unsubscribed. opened_at y clicked_at incluyen bots, como los opened y clicked del informe; human_opened y human_clicked indican si una persona abrió o hizo clic. email es null cuando el contacto se eliminó después del envío: la fila permanece, sigue contando en las cifras de la campaña. unsubscribed es true cuando la persona se dio de baja mediante el enlace de este correo: el enlace de baja identifica el correo que lo lleva, y una baja hecha en otro lugar (mediante la API, en la aplicación, desde una automatización) no cuenta en ninguna campaña. Para un correo enviado antes del 24 de septiembre de 2026, cuyo enlace solo identificaba a la persona, la baja se atribuye aún al último correo recibido antes de ella. El informe (/stats) cuenta a las mismas personas en counts.unsubscribed.
El idioma de las respuestas
Los mensajes de error siguen su encabezado Accept-Language: inglés por defecto, francés si lo solicita.
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 …"}}
Esto cubre todo lo que un programa lee: mensajes de error de la API, las herramientas del servidor MCP (sus nombres, qué hacen, sus parámetros) y la referencia servida en mailcheer://docs.
Solo se lee la primera preferencia: fr-FR,fr;q=0.9,en;q=0.8 solicita francés, aunque el inglés esté listado — y en-US,fr;q=0.9 solicita inglés.
⚠️ El code nunca cambia de idioma — escriba su lógica contra él, nunca contra el mensaje.
Errores
Siempre la misma forma, legible de dos maneras desde el mismo contenido. El message sigue el encabezado Accept-Language, inglés por defecto — su lógica debería leer code (o name), nunca message.
{
"error": {
"code": "unverified_from_domain",
"message": "Domain “example.com” is not verified in this workspace. Verified domains: yourdomain.com.",
"details": { "from": "hello@example.com", "verifiedDomains": ["yourdomain.com"] }
},
"statusCode": 422,
"message": "Domain “example.com” is not verified in this workspace. Verified domains: yourdomain.com.",
"name": "unverified_from_domain"
}
error es el formato de Mailcheer: estructurado, con details conteniendo lo que necesita para arreglar el problema. Los tres campos planos — statusCode, message, name — coinciden con el formato de Resend, de modo que el código escrito contra la API antigua muestra un mensaje correcto sin reescribirse.
Escriba su lógica contra code (o name — son el mismo valor), nunca contra message. El mensaje es para que lo lea un humano, y nos reservamos el derecho de reformularlo.
| Código | Estado | Qué significa |
|---|---|---|
missing_api_key | 401 | No hay cabecera Authorization. |
invalid_api_key | 401 | Clave desconocida. |
revoked_api_key | 401 | Clave revocada en los ajustes. |
insufficient_scope | 403 | La clave no tiene el permiso solicitado. |
reputation_blocked | 403 | Tus envíos están bloqueados: demasiados rebotes o quejas. |
sending_blocked | 403 | El envío está detenido para el espacio de trabajo (suspendido, bloqueado o prohibido): no sale nada, por ningún canal. details.reason indica cuál. |
commitment_required | 403 | El compromiso antispam no está aceptado: se muestra la próxima vez que inicies sesión en el espacio de trabajo. |
sending_paused | 423 | El espacio de trabajo está en pausa por una revisión de seguridad. No hay nada malo en la llamada: envíala de nuevo, sin cambios, después de la decisión. |
daily_quota_exceeded | 429 | Un espacio de trabajo nuevo alcanzó su límite diario: 100 correos por cada 24 horas móviles durante sus primeros tres días, 500 hasta el séptimo. Retry-After y details indican cuándo reintentar. |
quota_exceeded | 402 | La cuota mensual del plan no cubre el envío: no salió nada. details da plan, quota, sent, in_flight, remaining, requested y resets_at. |
free_plan_domain_used | 402 | Espacio de trabajo gratuito: el dominio de envío ya ha servido el plan gratuito este mes en otro espacio de trabajo. No salió nada. details da domain, root_domain, period y resets_at. |
not_found | 404 | El objeto no existe en este espacio de trabajo. |
conflict | 409 | Estado incompatible: campaña ya enviada, suscriptor ya eliminado. |
idempotency_key_reused | 409 | Mismo Idempotency-Key, cuerpo diferente. |
validation_error | 422 | Falta un campo o está mal formado. |
unverified_from_domain | 422 | El dominio 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 rechazó el mensaje. |
internal_error | 500 | Un fallo de nuestro lado. |
Límite de velocidad
600 solicitudes por minuto por clave. Cada respuesta incluye X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset; un rechazo también incluye Retry-After, en segundos. No confundas estas cabeceras con las de cuota mensual (Mailcheer-Quota-*, arriba): la velocidad se cuenta por minuto, la cuota por mes.
¿Necesitas más? Escríbenos: analizamos tu caso en lugar de dejarte reintentar en un bucle.
El servidor MCP
MCP — Model Context Protocol — es cómo un agente de IA descubre las herramientas de un producto y las usa. Mailcheer expone un servidor MCP alojado: nada que instalar, una dirección y tu clave.
Claude Code
claude mcp add mailcheer \
--transport http \
--url https://mailcheer.com/api/mcp \
--header "Authorization: Bearer mch_live_…"
ChatGPT, Cursor, Codex, Claude Desktop — todos leen la misma configuración de conector:
{
"mcpServers": {
"mailcheer": {
"type": "http",
"url": "https://mailcheer.com/api/mcp",
"headers": { "Authorization": "Bearer mch_live_…" }
}
}
}
Luego, en tu agente: "¿Qué espacio de trabajo de Mailcheer ves y qué dominios de envío están verificados?" Llamará a get_account, que no modifica nada — la forma correcta de confirmar una conexión.
Herramientas expuestas
| Herramienta | Qué hace |
|---|---|
get_account | El espacio de trabajo, permisos, uso de este mes (cola y fecha de reinicio incluidas), facturación, límites, dominios verificados. |
send_email | Envía un correo transaccional. Irreversible. |
get_email | El estado de un correo enviado. |
list_subscribers | Lista suscriptores, página por página. |
add_subscriber | Añade o actualiza un suscriptor. |
remove_subscriber | Da de baja y bloquea la dirección. Irreversible. |
list_suppression | Direcciones que no recibirán nada más. |
add_suppression | Bloquea una dirección. Irreversible. |
list_campaigns | Las campañas del espacio de trabajo. |
create_campaign | Crea un borrador. No se envía nada. |
preview_campaign_send | Indica si la campaña saldría y a cuántas personas, dirección por dirección con to. No se envía nada. |
send_campaign | Envía a todos los suscriptores activos, o solo a los suscriptores activos entre las direcciones en to. Irreversible. |
get_campaign_stats | Números 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 que una cambiara, MCP se convertiría en la puerta trasera.
Ninguna herramienta elimina una dirección de la lista de supresión. Es la única acción del producto que suspende una capacidad de envío, y un agente al que se le diga "limpia la lista" lo haría sin dudar. Se hace a mano, en tu espacio de trabajo.
El recurso mailcheer://docs da al agente la referencia completa: no necesita conocerla de antemano.
Si migras desde Resend
Los campos de POST /v1/emails y la respuesta { id } son los mismos. En la práctica: la URL base y la clave.
Dos formas de cambiar.
Con el cliente mínimo — un archivo para copiar, sin dependencias, la misma firma que el SDK de Resend. Consíguelo: mailcheer.com/mailcheer-client.ts.
// before
const resend = new Resend(process.env.RESEND_API_KEY);
// after
const mailcheer = new Mailcheer(process.env.MAILCHEER_API_KEY);
// the rest of your code stays the same
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 };
Nunca lanza excepciones: un fallo de red también se convierte en un error, con name: "network_error". Esto es intencional — un método que lanza donde el anterior devolvía un objeto convertiría "cambiar dos líneas" en "revisar cada punto de llamada", y los puntos de llamada que olvides revisar son exactamente las rutas de error.
Sin copiar nada — un fetch simple es suficiente:
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); // the message is human-readable
const id = body.id;
Tres cosas que saber:
- El dominio
fromdebe estar verificado en tu espacio de trabajo de Mailcheer, no en Resend. Añádelo en Dominios y publica los registros DNS. - La lista de supresión también protege los envíos transaccionales. Una dirección que se dio de baja de tu boletín no recibirá tus correos transaccionales del mismo espacio de trabajo tampoco — si eso no es lo que quieres, separa ambos en dos espacios de trabajo.
- La cuota mensual se comparte con tus campañas.
Dónde viven estos correos
Los correos enviados a través de la API no se unen a tus campañas: viven por separado, y esto no es un detalle técnico.
Un destinatario de factura no es un suscriptor. Agruparlos con tus suscriptores los habría inscrito en tu lista sin que jamás consintieran recibir tu boletín — contados en tu panel y objetivo de tu próxima campaña. Tus números de suscriptores siguen siendo los de tus suscriptores reales.
Lo que se comparte: la cuota mensual, la lista de supresión y el monitoreo de rebotes. Estas son las tres cosas que comprometen tu reputación de remitente, y esa reputación es la misma en ambos lados.
La especificación técnica
El archivo OpenAPI 3.1 se sirve tal cual: mailcheer.com/openapi.json. Describe cada endpoint, cada campo y cada error — suficiente para generar un cliente en tu idioma o para entregarlo a un agente.