Documentación
API pública y servidor MCP
Construye agentes, CLIs e integraciones de back-office sobre qmailing. PLUS y superiores desbloquean la autenticación con token Bearer en cada endpoint público, además de un servidor MCP listo para usar con Claude Desktop, Cursor o cualquier otro cliente compatible.
Primeros pasos
- 1. Pasa a PLUS — la API pública está disponible en
PLUS,PRO,PRO_PLUSoPRO_MAX. Las llamadas desde FREE reciben402 Payment Requiredindependientemente del token. - 2. Emite un token — ve a Configuración → Desarrolladores, haz clic en Nuevo token, elige los scopes que necesites y copia el valor
qm_live_…cuando se muestre. Los tokens aparecen una sola vez. - 3. Realiza tu primera llamada:
curl https://qmailing.com/api/v1/pub/mailboxes \ -H "Authorization: Bearer qm_live_your_token_here"
Autenticación
Toda llamada a la API pública lleva una cabecera Authorization: Bearer qm_live_<…>. El formato del token tiene un espacio de nombres (qm_live_ seguido de 32 caracteres aleatorios), así las fugas en logs y repositorios saltan a la vista.
Los tokens se almacenan en forma de hash (SHA-256). El texto en claro se muestra exactamente una vez al emitirlo y nunca más — si lo pierdes, genera uno nuevo. Cada solicitud activa una comparación de hash en tiempo constante; last_used_at se actualiza al tener éxito para que la UI de desarrolladores pueda señalar credenciales obsoletas.
apiAccess en cada solicitud firmada — no es necesario revocar tokens uno a uno al cambiar de plan.Scopes
Cada token lleva una o más cadenas de scope. Los endpoints declaran los scopes que necesitan; los scopes faltantes devuelven 403 InsufficientScope. El comodín (*) lo concede todo — actívalo solo si realmente lo necesitas.
| Scope | Permite |
|---|---|
| mailboxes:read | Listar e inspeccionar buzones |
| mailboxes:write | Crear, modificar y eliminar buzones |
| domains:read | Listar dominios y registros DNS |
| domains:write | Añadir, verificar y eliminar dominios personalizados (solo FE en v1) |
| email:read | Listar e inspeccionar mensajes de los buzones |
| email:send | Enviar correo saliente a través de la API |
| webhooks:read | Listar endpoints de webhook |
| webhooks:write | Registrar, listar y revocar endpoints de webhook |
| * | Comodín — lo concede todo. Úsalo con moderación. |
Buzones
Listar
GET /api/v1/pub/mailboxes
# scope: mailboxes:readObtener uno
GET /api/v1/pub/mailboxes/{id}
# scope: mailboxes:readCrear
POST /api/v1/pub/mailboxes
Content-Type: application/json
# scope: mailboxes:write
{
"localPart": "support",
"domain": "yourbrand.com",
"displayName": "Support team",
"forwardTo": "ops@yourbrand.com"
}El campo domain es opcional — omítelo y el buzón se creará en qmailing.com. En un dominio personalizado, el dominio debe estar tanto claimed COMO fullyVerified; un dominio aún no listo devuelve 400 en lugar de crear silenciosamente un buzón sin enrutamiento.
Dominios y DNS
Listar dominios personalizados
GET /api/v1/pub/domains
# scope: domains:readLista de registros DNS
GET /api/v1/pub/domains/{id}/dns-records
# scope: domains:readDevuelve la lista completa de 8 registros (TXT de challenge, MX, SPF, tres CNAME DKIM, DMARC, opcionalmente _amazonses) con un status por fila de PENDING / FOUND / MISMATCH / NOT_REQUIRED. El agente puede usarla para indicar al usuario exactamente qué publicar en su proveedor de DNS.
Correo
Lee el contenido de los buzones y envía correo saliente. La paginación por carpetas coincide con la API interna del frontend — los agentes que migran de cookie-JWT a Bearer no verán ningún cambio de forma.
Listar por carpeta
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 por defecto es INBOX; valores permitidos: INBOX, SENT, DRAFTS, TRASH, STARRED, SPAM. mailboxId es opcional — al omitirlo se listan todos los buzones del llamante. Paginación con offset + limit; limit está restringido al rango 1–100 (por defecto 25).
Obtener uno
GET /api/v1/pub/email/{id}
# scope: email:readDevuelve el EmailDetailDto completo: cuerpo HTML / texto, lista de etiquetas, metadatos de adjuntos. Los adjuntos NO se incluyen inline — descarga cada uno con GET /{emailId}/attachments/{index} más abajo, que transmite los bytes en bruto con el nombre original y el content-type.
Descargar adjunto
GET /api/v1/pub/email/{emailId}/attachments/{index}
# scope: email:read
# response: streamed bytes, Content-Type from the attachmentTransmite los bytes en bruto del adjunto (índice basado en cero dentro de la lista de adjuntos del correo). Mismo formato que el endpoint con sesión: Content-Type pass-through, Content-Disposition según RFC 6266 con el nombre original (UTF-8 percent-encoded para Unicode). Los agentes MCP que llaman vía @qmailing/mcp-server usan la herramienta qmailing_get_attachment, que devuelve los bytes codificados en base64 inline (límite 5 MiB).
Enviar
Endpoint de envío. Cuerpo multipart/form-data con una parte command en JSON (destinatarios / asunto / cuerpo) y cero o más partes-archivo 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--Forma del 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"
}Cada llamada cuenta contra el límite diario de envío del plan, la cuota de envío SES del buzón y la cuota de almacenamiento del usuario para la copia resultante en SENT. Los destinatarios se validan como direcciones RFC 5322; los inválidos devuelven 400 con la dirección ofensora. El buzón referenciado en mailboxId debe estar verificado en SES — si no, 409 MailboxNotVerified con una pista hacia la página de dominios.
multipart/form-data al salir, así la forma en cable permanece idéntica.filename en la parte command es opcional — Node fetch sella automáticamente filename="blob" en cada Blob, los clientes de navegador y Node pueden pasar command.json, y los clientes curl --form pueden omitirlo del todo. Las tres formas se parsean igual; solo importa Content-Type: application/json en los bytes de la parte.Scopes: email:read para listar y obtener uno, email:send para enviar.
Webhooks
Registra un endpoint HTTPS y QMailing hará POST de un envelope JSON firmado en él cada vez que se dispare uno de los eventos suscritos. La entrega está viva — el worker sondea una cola per-event cada 10s, firma cada solicitud con HMAC-SHA256 y reintenta los fallos con backoff exponencial (1m / 5m / 15m / 1h / 6h) antes de aparcar la fila en el DLQ para revisión manual.
Registrar 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 debe ser un endpoint HTTPS (HTTP solo se permite para localhost en dev). El host se comprueba contra una allow-list anti-SSRF — direcciones privadas / loopback / cloud-metadata se rechazan ya en el registro. La respuesta lleva plaintext exactamente una vez; guárdalo en el lado del servidor y úsalo para verificar firmas HMAC en las entregas entrantes.
Catálogo de eventos
| Evento | Se dispara cuando |
|---|---|
| email.received | Un correo entrante llega a uno de tus buzones. |
| email.sent | Un correo saliente fue aceptado por SES. |
| email.bounced | Un correo saliente rebotó (hard o soft). |
| domain.verified | Un dominio personalizado completó la verificación DNS de extremo a extremo. |
Listar endpoints
GET /api/v1/pub/webhooks
# scopes: webhooks:read OR webhooks:writeDevuelve endpoints activos y revocados, los más nuevos primero. El acceso de lectura funciona con webhooks:read o webhooks:write — un token inspector evita así el scope de escritura más amplio.
Forma de la entrega
Cada entrega es un único POST a tu endpoint con Content-Type: application/json. El cuerpo es el envelope del evento con los campos event (nombre del evento), occurred_at (timestamp ISO) y un bloque data cuya forma depende del evento (ver tabla arriba). Tres cabeceras llevan metadatos de routing + verificación:
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…"
}
}Esquema de firma
# 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]));
}Rechaza la solicitud si el timestamp tiene más de 5 minutos (ventana anti-replay) o si el HMAC recalculado no coincide con el v1 hex — nuestro firmador del BE imita exactamente las convenciones de Stripe / GitHub, por lo que cualquiera de las librerías estándar de verificación funcionará tal cual.
Política de reintentos
Respuestas no-2xx (o errores de transporte — DNS, TLS, conexión rechazada, timeout de 10s) incrementan attempt_count y empujan next_attempt_at al siguiente slot en 1m / 5m / 15m / 1h / 6h. Tras el quinto fallo la entrega pasa a FAILED (DLQ) — visible en /settings/developers, donde el desarrollador puede reintentarla manualmente. Idempotencia: cada reintento lleva el mismo UUID en X-Qmailing-Delivery — los receptores deduplican por esa cabecera.
Listar entregas recientes
GET /api/v1/pub/webhooks/{endpointId}/deliveries?limit=100
# scopes: webhooks:read OR webhooks:writeDevuelve las limit entregas más recientes de un endpoint (default 100, máx 100), las más nuevas primero. Cada fila incluye estado, contador de intentos, último código HTTP, último mensaje de error y el JSON original — alimenta la vista histórica del dashboard de desarrollador.
Enviar entrega de prueba
POST /api/v1/pub/webhooks/{endpointId}/test
# scope: webhooks:write
# Response 202: WebhookDeliveryDto (enqueued, picked up next worker tick)Dispara un evento sintético webhook.test al endpoint — mismo camino de dispatch que los eventos reales, útil para verificar firma + TLS antes de suscribirse a tráfico en vivo.
Reintentar entrega
POST /api/v1/pub/webhooks/deliveries/{deliveryId}/retry
# scope: webhooks:write
# Response 200: WebhookDeliveryDto (status=PENDING, attempt=MAX-1)Re-encola manualmente una fila de entrega. No se puede reintentar una ya entregada (409). El reintento da exactamente un intento adicional (contador a MAX-1), por lo que un endpoint persistentemente roto no puede bucle indefinido con clics repetidos.
Eliminar entrega del historial
DELETE /api/v1/pub/webhooks/deliveries/{deliveryId}
# scope: webhooks:write
# Response: 204 No ContentHard-delete de una fila de entrega de la tabla de historial. El log de auditoría de cumplimiento captura el propio evento de borrado, así que la trazabilidad no se pierde aunque desaparezca la fila.
Revocar endpoint
DELETE /api/v1/pub/webhooks/{id}
# scope: webhooks:write
# Response: 204 No ContentIdempotente — revocar un endpoint ya revocado tiene éxito de forma silenciosa. La fila se conserva (timestamp revoked-at fijado) para que los logs de auditoría mantengan sus referencias.
409 WebhookEndpointLimitExceeded / 400 InvalidWebhookEvent.Errores
Cada respuesta no-2xx es un ProblemDetail RFC-7807 con un campo code estable sobre el que tu agente puede ramificar:
| HTTP | code | Cuándo |
|---|---|---|
| 400 | InvalidApiTokenScope | Cadena de scope desconocida al crear. |
| 400 | InvalidEmailAddress | Uno de los destinatarios no superó la validación de sintaxis RFC 5322. |
| 400 | InvalidWebhookEvent | La lista de eventos registrada contiene un nombre desconocido. |
| 401 | — | Cabecera Bearer ausente, malformada, o token revocado / caducado. |
| 402 | PlanFeatureRequired | El llamante está en un plan que no incluye acceso a la API — actualiza. |
| 403 | InsufficientScope | El token no contiene ninguno de los scopes requeridos. |
| 404 | ApiTokenNotFound | El objetivo de revocación no existe o pertenece a otra cuenta. |
| 409 | MailboxNotVerified | El dominio del buzón aún no está verificado del todo — completa el DNS en /settings/domains. |
| 409 | ApiTokenLimitExceeded | La cuenta tiene el máximo (20) de tokens activos; revoca uno antes. |
| 409 | WebhookEndpointLimitExceeded | La cuenta ya tiene 10 endpoints de webhook activos; revoca uno primero. |
| 429 | RateLimitExceeded | El token superó las 300 solicitudes por minuto. Retry-After en segundos. |
Límites de tasa
Cada token tiene su propio cubo de 300 solicitudes por minuto. Los entornos multi-agente (CLI + cron + plugin de IDE) que usan tokens distintos para la misma cuenta funcionan de forma independiente — nunca compiten por un cubo compartido. 429 RateLimitExceeded incluye una cabecera Retry-After en segundos.
Servidor MCP
El paquete @qmailing/mcp-server se ejecuta localmente y expone la API pública como herramientas del Model Context Protocol. Conéctalo a Claude Desktop, Cursor, Continue o cualquier cliente compatible con MCP.
@qmailing/mcp-server@0.2.0) si no quieres actualizaciones automáticas.{
"mcpServers": {
"qmailing": {
"command": "npx",
"args": ["-y", "@qmailing/mcp-server"],
"env": {
"QMAILING_API_TOKEN": "qm_live_your_token_here"
}
}
}
}Herramientas incluidas: 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. El catálogo crece a medida que llegan nuevos endpoints.