Saltar al contenido principal

API de campañas

Crea, lanza y supervisa campañas mediante programación.

Todos los endpoints de esta página son relativos a la URL base: https://api-eu1.joryio.com. Consulta el Resumen de la API.

Autenticación

Todas las solicitudes requieren autenticación mediante clave de API:

Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json

Tu clave debe incluir el permiso que requiere cada endpoint:

PermisoEndpoints
campaigns:readEnumerar, obtener, estadísticas, destinatarios, versiones e historial
campaigns:writeCrear, actualizar, duplicar, archivar, etiquetar y revertir versiones
campaigns:sendEnviar, pausar, reanudar, cancelar, reintentar, envíos de prueba y envío transaccional
campaigns:deleteEliminar y eliminar en bloque

Consulta Claves de API para gestionar los permisos.


Crear una campaña

Endpoint

POST /campaigns

Cuerpo de la solicitud

CampoTipoObligatorioDescripción
namecadenaNombre de campaña (máximo 255 caracteres)
descriptioncadenaNoDescripción (máximo 1000 caracteres)
channelcadenaemail, sms, viber, push, webhook, whatsapp, in_app o ai_optimized
variantsmatrizSí*Variantes de mensaje (obligatorias para todos los canales salvo in_app)
targetingobjetoNoAudiencia: userIds, filterGroups, excludeFilterGroups, filterOperator, subscriptionPreference
sendTypecadenaNoimmediate, scheduled, triggered, intelligent, recurring o ai_optimized
scheduledAtcadenaNoFecha ISO 8601 para sendType: "scheduled"
scheduledTimezonecadenaNoZona horaria IANA en la que se interpreta la hora programada
triggerConfigobjetoNoRegla de disparador para sendType: "triggered" (type, eventName, conditions, reEntry, cooldownHours)
recurringScheduleobjetoNoPara sendType: "recurring": frequency (daily/weekly/monthly/custom), cron, dayOfWeek, dayOfMonth, timeOfDay, timezone, endDate, maxOccurrences
conversionTrackingobjetoNoprimaryConversion / secondaryConversions (nombre de evento + condiciones de propiedad), conversionWindowHours, attributionModel (first_touch/last_touch/linear)
emailConfigIdcadenaNoIdentidad de remitente de email guardada (canal de email)
subscriptionCategoryIdcadenaNoCategoría de consentimiento (lista de suscripción) bajo la que se envía la campaña
sendVolumeLimitobjetoNoenabled, maxSends, cadence (lifetime/per_send)

Contenido del mensaje in-app

Cada variante in-app lleva su contenido en customContent. El mode decide qué campos se aplican:

modeCamposSe renderiza como
nativetitle, body, imageUrl, buttons, closeButton, backdropDismissible, styleLos propios componentes de la app, sin web view
html (por defecto)html, cssMarcado del autor dentro de un web view
drag_drophtml, css, grapejsDataComo html; grapejsData es el estado del editor visual

mode puede omitirse, lo que significa html.

{
"name": "Weekend offer",
"channel": "in_app",
"channelConfig": { "type": "modal", "triggers": [] },
"variants": [
{
"id": "v1",
"name": "Native",
"weight": 100,
"customContent": {
"mode": "native",
"title": "Weekend only",
"body": "Hi {{ firstName }}, members get 20% off through Sunday.",
"imageUrl": "https://cdn.example.com/weekend.png",
"buttons": [
{ "id": "cta", "text": "See offer", "action": "url", "url": "https://example.com/offer" },
{ "id": "later", "text": "Not now", "action": "dismiss" }
],
"closeButton": true,
"backdropDismissible": true,
"style": {
"backgroundColor": "#0A1240",
"textColor": "#FFFFFF",
"primaryButtonColor": "#00C8B7",
"cornerRadius": 18
}
}
}
]
}

Los campos nativos son texto, no marcado. Se entregan a la app sin escapar, porque la app los renderiza en vistas de texto, así que A & B llega como A & B y no como A & B. La personalización con Liquid funciona en title, body y en el text y la url de los botones.

buttons está limitado a 3. action es uno de dismiss, url o deep_link; para los dos últimos, url es obligatorio.

style - anulaciones de presentación opcionales

Todos los campos son opcionales y uno ausente significa heredar: el color de superficie de la aplicación, su color de texto, su acento y su tipografía. Esa herencia es el sentido del contenido nativo, así que envía un campo solo cuando la campaña lo necesite.

CampoTipoSe aplica enSignificado
backgroundColorstringweb, iOS, AndroidFondo de la tarjeta
textColorstringweb, iOS, AndroidTitular y cuerpo (el cuerpo, algo más suave)
primaryButtonColorstringweb, iOS, AndroidRelleno del primer botón
primaryButtonTextColorstringweb, iOS, AndroidSu etiqueta. Si se omite, se elige negro o blanco según el contraste con el relleno
cornerRadiusnumberweb, iOS, Android0-48. Se ignora en fullscreen, donde las esquinas redondeadas dejarían ver la aplicación
fontSizenumberweb, iOS, Android10-32. Tamaño del cuerpo; el titular se escala a partir de él. En móvil se aplica encima el ajuste de tamaño de texto del usuario
titleWeightstringweb, iOS, Androidregular, medium, semibold, bold. Solo el TITULAR - el cuerpo se mantiene normal por legibilidad
textAlignstringweb, iOS, Androidauto (por defecto), start, center, end. auto sigue el idioma del propio mensaje, así que el hebreo y el árabe se leen de derecha a izquierda dentro de una app en inglés
fontFamilystringweb; en móvil, si se puedeEn móvil se aplica solo si la aplicación incluye esa fuente (iOS: registrada; Android: res/font o una familia del sistema). Si falta, conserva la suya
customCssstringsolo webCSS escrito a mano, hasta 20000 caracteres. El SDK web reescribe cada selector para que quede dentro del mensaje antes de inyectarlo, así que ninguna regla llega a la página anfitriona, y @import se descarta. Los móviles no tienen motor CSS

Los colores se pasan tal cual se escriben: hex, rgb() o una palabra clave CSS. Un valor que el renderizador no pueda interpretar vuelve al heredado en lugar de hacer fallar el mensaje.

En customCss puedes seleccionar la propia tarjeta, h2, p, button.primary y button.secondary, y los campos anteriores también se exponen como variables CSS: --joryio-inapp-bg, --joryio-inapp-fg, --joryio-inapp-primary, --joryio-inapp-primary-fg, --joryio-inapp-radius y --joryio-inapp-font.

El contenido HTML requiere que la app lo habilite. Los SDK móviles y web rechazan los mensajes in-app HTML salvo que la app defina allowHtmlJsInAppMessages en la inicialización, porque un mensaje así ejecuta JavaScript del autor dentro de la app. El contenido nativo siempre se muestra. Consulta las guías de Android, iOS y Web.

| sendRateLimit | objeto | No | enabled, maxPerMinute | | quietTimeOverride | objeto | No | Anulación de horas de silencio por campaña | | utmSettings | objeto | No | Anulación de UTM/etiquetado de enlaces por campaña | | channelConfig | objeto | No | Configuración específica del canal (su estructura depende del canal) | | stoConfig / abTestConfig | objeto | No | Configuración de optimización de hora de envío / prueba A-B | | resendPolicy | cadena | No | Política de reenvío para «Enviar de nuevo» de una campaña puntual: only_new (predeterminado), everyone o cooldown | | resendCooldownDays | número | No | Ventana de antigüedad (días) para resendPolicy: "cooldown" | | tags | cadena[] | No | Etiquetas | | status | cadena | No | draft, scheduled, active, paused, completed o cancelled |

Estructura de variante

Cada entrada de variants:

CampoTipoObligatorioDescripción
idcadenaID de variante
namecadenaNombre de variante
weightnúmeroProporción de tráfico, 0-100 (los pesos deben sumar correctamente según las reglas del canal)
messageobjetoNoMensaje del canal. Email: subject, preheader, from, fromName, html o templateId, text. SMS: body, from, shortenLinks. Push: title, body, icon, image, data. WhatsApp: messageType (template/reply), templateId, wabaId, variableMapping, replyText. Webhook: url, method, headers, body, auth, bodyType
isControlGroupbooleanoNoMarca esta variante como grupo de control retenido (permite medir la mejora incremental)

Solicitud de ejemplo

curl -X POST https://api-eu1.joryio.com/campaigns \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "July Newsletter",
"channel": "email",
"sendType": "scheduled",
"scheduledAt": "2026-07-20T10:00:00.000Z",
"scheduledTimezone": "America/New_York",
"variants": [
{
"id": "variant-a",
"name": "Variant A",
"weight": 100,
"message": {
"subject": "Your July update",
"from": "news@example.com",
"fromName": "Example",
"html": "<h1>Hello {{ user.firstName }}</h1>"
}
}
],
"targeting": {
"filterGroups": [
{
"filters": [
{ "type": "attribute", "field": "plan", "operator": "equals", "value": "premium" }
],
"operator": "AND"
}
]
}
}'

Respuesta

Devuelve el objeto de campaña creado:

{
"id": "8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b",
"name": "July Newsletter",
"channel": "email",
"status": "scheduled",
"sendType": "scheduled",
"scheduledAt": "2026-07-20T14:00:00.000Z",
"variants": [ ... ],
"targeting": { ... },
"tags": [],
"createdAt": "2026-07-12T09:00:00.000Z",
"updatedAt": "2026-07-12T09:00:00.000Z"
}

Enumerar campañas

Endpoint

GET /campaigns

Parámetros de consulta

ParámetroTipoPredeterminadoDescripción
statuscadena-Filtra por estado (separados por comas si hay varios)
channelcadena-Filtra por canal (separados por comas si hay varios)
tagscadena-Filtra por etiquetas (separadas por comas)
qcadena-Búsqueda de texto libre
createdBy / editedBycadena-Filtra por creador / último editor (ID de usuario separados por comas)
createdFromcadena-Solo campañas creadas en o después de esta fecha ISO
pagenúmero1Número de página
limitnúmero20Resultados por página (máximo 100)

Solicitud de ejemplo

curl -X GET "https://api-eu1.joryio.com/campaigns?status=active&channel=email&limit=50" \
-H "Authorization: Bearer jry_live_your_api_key"

Respuesta

{
"data": [
{ "id": "8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b", "name": "July Newsletter", "channel": "email", "status": "active" }
],
"pagination": {
"total": 23,
"page": 1,
"limit": 50,
"offset": 0,
"totalPages": 1,
"hasMore": false
}
}

Obtener una campaña

GET /campaigns/:campaignId
curl -X GET https://api-eu1.joryio.com/campaigns/8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b \
-H "Authorization: Bearer jry_live_your_api_key"

Devuelve el objeto de campaña completo. El estado de la campaña es uno de estos: draft, scheduled, active, paused, completed, cancelled, archived o failed (error de envío permanente; consulta failureReason).


Actualizar una campaña

PUT /campaigns/:campaignId

El cuerpo acepta los mismos campos que Crear una campaña. Todos son opcionales y solo se actualizan los campos proporcionados.

curl -X PUT https://api-eu1.joryio.com/campaigns/8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "name": "July Newsletter v2" }'
Editar una campaña activa

Las campañas activadas y recurrentes permanecen active durante toda su vida. Editar una campaña active no cambia lo que se está enviando en ese momento: las ediciones se almacenan en búfer como borrador pendiente. Llama a POST /campaigns/:campaignId/publish para aplicarlas de forma atómica a la campaña en curso o a POST /campaigns/:campaignId/discard-draft para descartarlas. Las campañas en borrador se actualizan directamente (sin paso de publicación).


Eliminar una campaña

DELETE /campaigns/:campaignId

Devuelve 204 No Content. Solo se eliminan definitivamente los borradores que nunca se enviaron; las campañas con historial de envío deben archivarse (POST /campaigns/:campaignId/archive), lo que detiene los envíos y conserva la analítica.


Ciclo de vida de una campaña

MétodoRutaDescripción
POST/campaigns/:campaignId/sendLanza la campaña (inicia el envío / activa una campaña activada)
POST/campaigns/:campaignId/pausePausa una campaña en ejecución
POST/campaigns/:campaignId/resumeReanuda una campaña en pausa
POST/campaigns/:campaignId/publishAplica de forma atómica las ediciones almacenadas en búfer a una campaña activa (400 si no hay ninguna pendiente)
POST/campaigns/:campaignId/discard-draftDescarta las ediciones almacenadas en búfer de una campaña activa (sin efecto si no hay ninguna)
POST/campaigns/:campaignId/cancelCancela una campaña
POST/campaigns/:campaignId/resendReenvía («Enviar de nuevo») una campaña puntual completada según su resendPolicy
POST/campaigns/:campaignId/retryReintenta una campaña failed (la devuelve a scheduled)
POST/campaigns/:campaignId/preview-launchResumen previo al lanzamiento (tamaño de audiencia y comprobaciones) sin enviar
POST/campaigns/:campaignId/duplicateDuplica una campaña
POST/campaigns/:campaignId/archiveArchiva (detiene los envíos y conserva el historial)
POST/campaigns/:campaignId/unarchiveRestaura a un estado sin envío (reanuda explícitamente para volver a enviar)
POST/campaigns/:campaignId/stop-recurringDetiene futuras apariciones de una campaña recurrente
curl -X POST https://api-eu1.joryio.com/campaigns/8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b/send \
-H "Authorization: Bearer jry_live_your_api_key"
Enviar de nuevo (política de reenvío)

Una campaña puntual completada puede reenviarse mediante POST /campaigns/:campaignId/resend. Quién la recibe depende de resendPolicy de la campaña:

  • only_new (predeterminado): omite a quien ya la recibió (solo la reciben usuarios nunca alcanzados).
  • everyone: reenvía a toda la audiencia, incluidos los destinatarios anteriores.
  • cooldown: reenvía a todos excepto a quienes recibieron un mensaje durante los últimos resendCooldownDays días.

El consentimiento y las supresiones se aplican siempre. Los límites de frecuencia siguen la marca ignoreTouchingRules de la campaña. Las campañas activadas usan triggerConfig.reEntry en su lugar y las recurrentes reenvían según su programación. Devuelve 400 para esos tipos, para in-app o para una campaña que aún no ha terminado de enviarse.


Estadísticas de campaña

GET /campaigns/:campaignId/stats

Parámetros de consulta: startDate, endDate (ISO 8601, opcionales).

Respuesta

{
"campaignId": "8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b",
"name": "July Newsletter",
"channel": "email",
"status": "completed",
"stats": {
"queued": 1200,
"sent": 1180,
"delivered": 1150,
"failed": 30,
"opened": 640,
"clicked": 210
},
"conversionStats": { ... },
"revenue": { ... },
"uplift": null,
"conversionTracking": { ... },
"startedAt": "2026-07-20T14:00:00.000Z",
"completedAt": "2026-07-20T14:12:00.000Z",
"createdAt": "2026-07-12T09:00:00.000Z"
}

conversionStats y revenue se completan cuando se configura el seguimiento de conversiones; uplift se completa solo cuando una variante está marcada con isControlGroup.

Endpoints de estadísticas relacionados

MétodoRutaDescripción
GET/campaigns/:campaignId/variant-statsEstadísticas A/B por variante con significación estadística (startDate/endDate)
GET/campaigns/:campaignId/linksEstadísticas de clics en enlaces (startDate/endDate)
GET/campaigns/:campaignId/failure-reasonsErrores de entrega agrupados por código de error DLR ([{ code, reason, count }])
GET/campaigns/:campaignId/recipientsDestinatarios paginados con el último estado de mensaje (status, limit máximo 200, offset)
GET/campaigns/:campaignId/recipients/:userIdCronología de eventos de mensaje por usuario para esta campaña
GET/campaigns/:campaignId/in-app-statsEstadísticas de visualización in-app (campañas in-app). Incluye displayFrequency - la distribución de impresiones por usuario como { tailBucket, buckets: [{ displays, users, impressions }], maxPerUser }. Los recuentos iguales o superiores a tailBucket se agrupan en un solo bucket, así que displays === tailBucket significa "esa cantidad o más"; para calcular una media usa el campo impressions de cada bucket (no displays * users), y maxPerUser para el usuario más expuesto
GET/campaigns/:campaignId/impressionsLista de impresiones in-app (limit, offset, startDate, endDate)

Enviar un mensaje transaccional

Envía un mensaje puntual a un único usuario sin crear una campaña.

POST /campaigns/transactional/send

Cuerpo de la solicitud

CampoTipoObligatorioDescripción
userIdcadenaID de usuario objetivo
channelcadenaemail, sms, push o viber
messageobjetosubject (email), body (texto simple), html (email). Puede ser {} para viber: el cuerpo de plantilla aprobado es el mensaje
viberTemplateIdcadenaSolo ViberPlantilla aprobada del registro de plantillas de Viber. Rakuten Viber exige plantillas preaprobadas para mensajes transaccionales/OTP (desde julio de 2026); el contenido no aprobado se factura a la tarifa promocional, por lo que la API se niega a enviar sin una
variablesobjetoNoSolo Viber: valores para los campos dinámicos de la plantilla (se combinan en el contexto de personalización)
triggerDataobjetoNoContexto disponible para la personalización (type, name, properties, metadata)
idempotencyKeycadenaNoClave de deduplicación proporcionada por quien llama: un reintento con la misma clave no se entrega dos veces
curl -X POST https://api-eu1.joryio.com/campaigns/transactional/send \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"channel": "email",
"message": {
"subject": "Your receipt",
"html": "<p>Thanks for your order, {{ user.firstName }}.</p>"
},
"idempotencyKey": "order-98421-receipt"
}'

Endpoints adicionales

MétodoRutaDescripción
POST/campaigns/previewPrevisualiza usuarios que cumplen los criterios de audiencia (cuerpo: targeting, limit opcional máx. 500, channel, webAppId)
POST/campaigns/spam-checkComprueba el contenido de email en busca de spam antes de enviarlo
GET/campaigns/:campaignId/versionsEnumera instantáneas de versiones guardadas
GET/campaigns/:campaignId/versions/compare?v1=&v2=Compara dos versiones
GET/campaigns/:campaignId/versions/:versionIdObtiene una instantánea de versión
POST/campaigns/:campaignId/versions/:versionId/rollbackRevierte a una versión
GET/campaigns/:campaignId/historyHistorial de registro de auditoría (limit, máximo 200)
POST/campaigns/bulk-delete / bulk-duplicate / bulk-archive / bulk-unarchiveAcciones masivas; cuerpo { "ids": [...] }, devuelve { succeeded, failed }
POST/campaigns/bulk-tagEtiquetado masivo; cuerpo { "ids": [...], "tags": [...] }
POST/campaigns/:campaignId/send-test-whatsapp / send-test-sms / send-test-push / send-test-in-appEnvía pruebas a un teléfono/usuario antes del lanzamiento
GET/campaigns/:campaignId/sto-coverageCobertura de optimización de hora de envío para la audiencia
GET/campaigns/:campaignId/recurring-statusEstado de la campaña recurrente
POST/campaigns/:campaignId/retest-abRestablece la ganadora A/B de una campaña recurrente de variante ganadora
POST/campaigns/:campaignId/launch-rlLanza en modo optimizado por IA (RL)
GET/campaigns/:campaignId/rl-statsEstadísticas del dashboard de campaña optimizada por IA
POST/campaigns/:campaignId/pause-rl / resume-rlPausa / reanuda una campaña optimizada por IA
POST/campaigns/ml-path-warmthEstado de calentamiento de personalización para ID de rutas/variantes

Respuestas de error

Todos los errores comparten la estructura estándar. Consulta Respuesta de error en el Resumen de la API para ver el formato y la lista completa de códigos de estado.

{
"statusCode": 404,
"message": "Campaign not found",
"timestamp": "2026-07-12T09:00:00.000Z",
"path": "/campaigns/8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b"
}
EstadoCuándo
400Error de validación (por ejemplo, channel no válido o falta weight de una variante); el cuerpo añade una matriz errors con un mensaje por campo no válido
401Falta la clave de API o no es válida
403La clave de API no tiene el permiso campaigns:* necesario
404No se encontró la campaña en este espacio de trabajo
409Conflicto de ciclo de vida; por ejemplo, reanudar una campaña que ya no está en pausa ("Campaign is no longer paused.") o enviar una que ya no se puede enviar

Relacionado