Marketing
Los métodos de sdk.mcp.marketing en JavaScript y Python: comprobación previa al envío, historial por destinatario, consentimiento, rebotes y rutas de envío.
Actualizado el . 11 min de lectura
Incluido en los planes Profesional y Avanzado. Hasta 10.000 correos de campaña por ciclo en Profesional y 25.000 en Avanzado. Ver precios
sdk.mcp.marketing lleva a tu integración lo mismo que la pantalla de Marketing, necesitas entrar con tu cuenta: preguntar si una campaña está lista sin intentar enviarla, explicar qué pasó con un correo concreto, saber quién ha contestado, dejar constancia de en qué bandeja cayó la prueba y, sobre todo, saber qué se está midiendo de verdad. Los métodos se reparten en los sub-espacios campaigns, lists, deliverability, sendingRoutes y bounceSettings, entre otros.
Todo viaja por el carril MCP (POST /api/mcp/marketing), que actúa en nombre de una persona: el SDK tiene que estar configurado con OAuth, y la organización necesita el aplicativo Marketing en su plan (salvo sendingRoutes, que se explica más abajo). Los ejemplos de Python usan la clase asíncrona.
Cambio con efecto sobre integraciones vivas: «marketing:write» ya no envía
Desde el 5 de septiembre de 2026, marketing:campaigns:send y
marketing:campaigns:schedule han salido del conjunto OAuth
marketing:write. El texto de consentimiento decía que ese conjunto no incluía
el envío, y ahora es verdad.
Si tu integración enviaba o programaba campañas apoyándose en marketing:write,
tiene que volver a pedir autorización con los dos scopes por su nombre (o
marketing:full). Hasta que lo haga, campaigns.send, campaigns.schedule,
campaigns.retryFailed, campaigns.inboxCheckStart y
campaigns.inboxCheckConfirm llegan denegadas por falta de scope, no por
falta de permiso del usuario.
Así encajan los métodos de esta página en el envío de una campaña:
Una campaña enviada con garantías
campaigns.preflight
Comprobar
Las mismas comprobaciones que bloquean el envío, sin enviar nada.inboxCheckStart y inboxCheckConfirm · Opcional
Probar en buzones
Hasta tres buzones propios; una persona declara dónde cayó.campaigns.send
Enviar
Exige el scope marketing:campaigns:send.recipientTimeline y listReplies
Seguir cada correo
El historial de un correo y quién ha contestado.measurementCoverage
Medir sin inventar
Qué señales se miden de verdad antes de dar cifras.
La regla que gobierna todo este bloque
La ausencia de medición no es un cero. Cuando el servidor no puede saber algo lo dice, y presentarlo como «0» convierte una limitación nuestra en una afirmación sobre el cliente. Cuatro casos concretos que aparecen en los tipos:
| Lo que llega | Lo que significa | Lo que no puede decir |
|---|---|---|
reputationScore: null | No hay muestra suficiente para puntuar | «Tu puntuación es 0» |
Una señal en NOT_MEASURED | No tenemos forma de enterarnos | «No has tenido rebotes» |
consent ausente | Quien pregunta no tiene permiso para verlo | «Esta persona no está suscrita» |
automatic: true | La respuesta la generó una máquina | «Esta persona está interesada» |
Antes de enviar: campaigns.preflight
Ejecuta en modo diagnóstico las mismas comprobaciones que bloquean al programar y al arrancar. Sin ella, la única forma de saber si una campaña está lista sería intentar enviarla y leer el rechazo, algo inaceptable en una integración: el intento que falla puede haber reservado cupo, y el que no falla ya ha escrito a la lista entera.
const { data } = await sdk.mcp.marketing.campaigns.preflight('cmp_123');
if (!data.preflight.canSend) {
const bloqueantes = data.preflight.checks.filter((c) => c.blocking);
// NO llames a send: el envío se rechazaría igual, después de tocar el cupo.
throw new Error(`Bloquean: ${bloqueantes.map((c) => c.code).join(', ')}`);
}
// «unknown» significa que esa comprobación NO SE PUDO HACER, no que esté bien.
const sinComprobar = data.preflight.checks.filter((c) => c.status === 'unknown');Es de solo lectura: no reserva cupo, no confirma ningún aviso y no autoriza
nada. Las puertas reales se vuelven a aplicar al enviar, así que un preflight
en verde no es un salvoconducto. Lo abren los tres permisos del envío en OR:
quien solo puede mirar la campaña también necesita entender por qué no sale.
Durante y después: historial, respuestas y bandejas
campaigns.recipientTimeline(campaignId, sendId)
El relato de un correo: encolado, intentos, aceptación o rechazo con su causa, exclusión deliberada, rebote, apertura, clic, queja, baja y respuesta.
El sendId sale del campo id que viaja dentro de sends[] en cada fila de
listRecipients, y también de listReplies. No es el identificador del
destinatario ni el del contacto.
ACCEPTED no significa que llegara a nadie
Un hito ACCEPTED acredita que el servidor de destino se hizo cargo del
mensaje. No dice que llegara a la bandeja de entrada, ni que la persona lo
viera. Un hito con metadata.derived se ha reconstruido desde las columnas
del envío porque es anterior a esta versión.
La clave tech (aparato, programa de correo, país y enlace pulsado) se omite
sin el permiso EMAIL_MARKETING_VIEW_RECIPIENT_DETAIL y el scope
marketing:recipients:detail:view. El servidor nunca devuelve un error por eso.
campaigns.listReplies(campaignId, filters)
Quién ha contestado, con la marca automatic. Una respuesta con
automatic: true la generó una máquina (un aviso de ausencia): no la cuentes
como conversación ni como interés, y un aviso de no entrega no es una respuesta.
Ver que alguien contestó lo gobierna EMAIL_MARKETING_VIEW. Leer lo que dijo
es otra cosa: el adelanto del texto exige el permiso de interacciones del CRM
y el aplicativo CRM en el plan. Cuando no viaja, snippetIncluded: false lo
dice, y eso no significa que no haya texto.
El pack de comprobación en buzones
// Manda la prueba a hasta TRES buzones propios por el camino real.
const { data: pack } = await sdk.mcp.marketing.campaigns.inboxCheckStart(
'cmp_123',
[{ email: 'yo@gmail.com', provider: 'gmail' }],
);
// Después, una PERSONA abre su buzón y declara lo que ve.
await sdk.mcp.marketing.campaigns.inboxCheckConfirm(pack.inboxChecks[0].id, {
placement: 'INBOX',
imagesBlockedOk: true,
// Lo que no se declara queda SIN COMPROBAR: ni aprobado ni suspenso.
});
const { data: hechas } =
await sdk.mcp.marketing.campaigns.inboxChecksList('cmp_123');
const caducadas = hechas.inboxChecks.filter((c) => c.expired);No concede ningún estado. Que un correo llegue a la bandeja principal solo
lo puede confirmar una persona abriendo su buzón, y esa confirmación caduca a
los 90 días (expired: true) porque las reglas de los buzones cambian.
Consume el mismo freno que el envío de prueba y no gasta correos del plan.
Qué se está midiendo de verdad
deliverability.measurementCoverage() devuelve las cinco señales (rebotes,
quejas, aperturas, clics y respuestas) con su estado, su fuente y el motivo
cuando no hay medición. Es la operación que hay que leer antes de presentar
cualquier cifra de este espacio.
| Estado | Qué significa |
|---|---|
MEASURED | Hay fuente y la cifra se puede afirmar |
PARTIAL | La cifra es un suelo y puede quedarse corta |
NOT_MEASURED | No tenemos forma de enterarnos. Un cero aquí no significa nada |
NOT_APPLICABLE | Esa señal no aplica a esta configuración |
byRoute («qué mide cada vía de salida») viaja solo con permiso de gestión: su
ausencia es falta de permiso, no ausencia de rutas.
La puntuación puede no existir
reputationSummary y dashboardSummary admiten periodDays (7, 30 o 90; por
defecto 30) y el período efectivo viaja siempre en la respuesta.
const { data } = await sdk.mcp.marketing.deliverability.reputationSummary({
periodDays: 30,
});
if (data.reputationScoreAvailable === false) {
// NO des ninguna cifra: sin muestra el cálculo devolvía siempre 39 por
// construcción, y no significaba nada.
const base = data.metrics.reputationBasis;
console.log(`Hacen falta ${base?.minAccepted} aceptados y hay ${base?.accepted}.`);
} else {
console.log(`Salud de tus envíos: ${data.metrics.reputationScore} / 100`);
}Las recomendaciones del panel dejaron de ser frases sueltas: llegan como objetos
con su código, la métrica, el valor, el umbral y el tamaño de la muestra, más un
message con la redacción única del servidor. Con cero intentos en el período
la respuesta trae exactamente NO_DATA, y ALL_HEALTHY solo aparece con
muestra por encima del suelo.
Comprobar una dirección
deliverability.addressCheck(email) o addressCheck([...]) (hasta 500)
devuelve tres bloques independientes: lo que dice nuestro histórico, la
comprobación técnica del dominio y la evidencia de suscripción.
No comprueba que el buzón exista ni que haya autorización
El veredicto NO_BLOCKS significa «no consta ningún impedimento nuestro». No
dice que el buzón exista, ni que haya autorización para escribirle.
Y hay dos ausencias que no significan lo mismo: consentIncluded: false
quiere decir que quien pregunta no tiene permiso para ver la evidencia
(hacen falta EMAIL_MARKETING_LISTS_MANAGE y el scope marketing:lists:manage)
y entonces la clave consent no viaja en ninguna fila; consent: null sí viaja
y quiere decir que no consta ninguna suscripción. Una ficha del CRM no es
una suscripción.
El DNS se consulta una vez por dominio distinto, así que comprobar cien direcciones del mismo dominio cuesta una sola consulta.
Control de frecuencia
deliverability.frequencyCapGet() y frequencyCapSet(input) gobiernan cuántos
correos de campaña puede recibir la misma dirección en una ventana de días.
Nace apagado en todas las organizaciones: enabled: false significa que hoy
no se excluye a nadie por frecuencia, no que la regla sea infinita.
A quien supere el tope se le excluye al construir la audiencia con el motivo
FREQUENCY_CAP, y eso no es un fallo de envío. Una campaña concreta puede
saltárselo desde campaigns.update con frequencyCapOverride, explicando por
qué en frequencyCapOverrideReason.
Leerlo vale con el permiso de ver Marketing; cambiarlo exige el de gestión de entregabilidad, porque decide a cuánta gente deja de llegarle el correo.
Consentimiento, readmisión e importación
| Método | Qué hace | Permiso |
|---|---|---|
lists.consentHistory(listId, subscriberId) | Todos los hechos de consentimiento, del más antiguo al más reciente. Sobrevive a la baja. | EMAIL_MARKETING_LISTS_MANAGE |
lists.resendConfirmation(listId, subscriberId) | Reenvía el correo de doble confirmación a quien sigue pendiente. | EMAIL_MARKETING_EDIT |
lists.reactivateContact(contactId) | Borra la baja global de un contacto del CRM. | EMAIL_MARKETING_LISTS_MANAGE |
lists.importPreview / importStart / importStatus / importStop / importRejected | Supervisar una importación de suscriptores. | EMAIL_MARKETING_LISTS_IMPORT |
reactivateContactno esreactivateSubscriber. Aquella devuelve a alguien que se apartó por inactividad dentro de una lista; esta borra la baja global y afecta a todas las comunicaciones. Ninguna de las dos crea consentimiento: si la persona no lo ha dado de nuevo, no le escribas.- El archivo de una importación no se sube por esta vía. Se sube en la
pantalla, que devuelve el testigo
previewDataKey(caduca a los 30 minutos), y por integración se recalculan las cifras y se pone en marcha el trabajo con ese testigo. Para dar de alta direcciones que ya tienes a mano estálists.addSubscribers, que sigue exigiendoimportDeclaration. importStopno deshace nada: quien ya entró se queda en la lista y solo se deja de procesar el resto del archivo.importRejectedexige los dos permisos, importar y exportar: son correos de personas que no están en la lista.- La dirección de red y el navegador del historial de consentimiento solo viajan
con
CLIENTS_VIEW_SENSITIVE: son dato personal del interesado, mismo criterio que el NIF y el IBAN.
Rutas de envío: sendingRoutes
sdk.mcp.marketing.sendingRoutes cubre entera la herramienta transversal
email_sending_routes (12 operaciones). Es por dónde sale el correo, que no
es lo mismo que quién puede firmarlo: email_sender_domains responde a quién
puede firmar (los registros del DNS) y esta herramienta a quién expide el correo.
Un dominio con SPF, DKIM y DMARC en verde y una campaña entera rechazada es
exactamente esa confusión.
// Inspecciona el correo de prueba SIN enviarlo: no sale ni un byte.
const { data: informe } = await sdk.mcp.marketing.sendingRoutes.headerReport(
'rt_1',
'marketing@miempresa.com',
);
console.log(informe.headerReport.dkim.signed, informe.headerReport.notes);
// La prueba REAL. `ok` solo significa que el servidor de salida aceptó.
const { data: prueba } = await sdk.mcp.marketing.sendingRoutes.test(
'rt_1',
'marketing@miempresa.com',
);
console.log(prueba.testResult.verdicts?.inboxPlacement); // siempre 'UNKNOWN'headerReportes una lectura: compone el mismo correo de prueba y lo inspecciona en lugar de entregarlo al transporte, así que no consume el freno ni gasta reputación. Nunca devuelve claves privadas ni el secreto de la ruta. Mientras el dominio no está verificado, la prueba de una ruta sale sin firma y sin cabeceras de baja a propósito, y las notas lo dicen: ahídkim.notAttemptedestrueysigned: falseno es una avería.managedRetrysaca la cuenta gestionada por UTILIA de un estado terminal y dispara una sonda de envío real. Si la organización ya envía por su propia ruta, respondesupersededy no toca el transporte de nadie. No devuelve ninguna credencial.- El secreto nunca se devuelve, ni enmascarado: solo
secretSet. Se escribe consetCredentials, que lleva su propio permiso y confirmación.
Esta herramienta está exenta de la puerta del aplicativo Marketing, porque la identidad de envío gobierna también facturas, presupuestos y portal; pero exige la capacidad de plan «Servidor de envío propio».
Avisos de rebote: bounceSettings
sdk.mcp.marketing.bounceSettings cubre entera marketing_bounce_settings
(5 operaciones): los dos canales por los que nos enteramos de un correo
devuelto. Sin ninguno de los dos activo, los rebotes no se miden:
delivered vale lo mismo que accepted y un cero en rebotes solo quiere decir
que no estamos mirando.
get()trae la configuración, la telemetría ywebhookUrl, la dirección que hay que pegar en el panel del proveedor.lastWebhookReceivedAt: nullcon el webhook encendido significa que todavía no ha llegado ningún aviso verificado: es lo que distingue «está configurado» de «funciona».testWebhook()da el veredicto sin mandar nada fuera.- Solo Mailgun y Resend son verificables. De SendGrid y Postmark no sabemos
comprobar la firma, así que sus avisos se rechazan; llegan declarados con
verifiable: falsey su motivo escrito. regenerateWebhookSecret()devuelve el secreto una sola vez.
Cifras que cambian de significado
excludedyexcludedByReasonen las estadísticas de campaña: los correos que no salieron a propósito (baja respetada, rebote definitivo, correo desactivado en su ficha, apartado por inactividad, tope de frecuencia) ya no cuentan como fallidos. La cifra de fallidos de las campañas ya enviadas bajó por este cambio, y no porque nada se haya arreglado.repliescuenta personas distintas que han contestado y valenull—nunca cero— sin buzón sincronizado, con el motivoNO_MAILBOX_SYNCenavailability.replies.listRecipientsadmitefailureCategory(conEXCLUDEDpara ese grupo) ysendStatusIn, y el segmentorepliedenengagement. Cada fila trae dentro desends[]el ciclo de vida del envío y suid.campaigns.listya no devuelve el contenido de cada campaña salvo que pidasincludeContent: true. Engetel valor por defecto sigue siendotrue.
Lo que el SDK no cubre
La opción stopOnReply de marketing_automations —encendida por defecto, para
que una secuencia deje de escribir a quien ya ha contestado— no tiene método
en el SDK. Esa herramienta sirve 23 operaciones y la regla del contrato es
cubrirla entera o no cubrirla: media herramienta obliga a mezclar métodos
tipados con llamadas a pelo. Se usa con el cliente MCP de bajo nivel o desde la
pantalla de automatizaciones.
Necesita algún buzón de correo sincronizado en la organización: sin él no se registran las respuestas y la opción, aunque quede guardada, no puede surtir efecto.