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:
| Permiso | Endpoints |
|---|---|
campaigns:read | Enumerar, obtener, estadísticas, destinatarios, versiones e historial |
campaigns:write | Crear, actualizar, duplicar, archivar, etiquetar y revertir versiones |
campaigns:send | Enviar, pausar, reanudar, cancelar, reintentar, envíos de prueba y envío transaccional |
campaigns:delete | Eliminar y eliminar en bloque |
Consulta Claves de API para gestionar los permisos.
Crear una campaña
Endpoint
POST /campaigns
Cuerpo de la solicitud
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
name | cadena | Sí | Nombre de campaña (máximo 255 caracteres) |
description | cadena | No | Descripción (máximo 1000 caracteres) |
channel | cadena | Sí | email, sms, viber, push, webhook, whatsapp, in_app o ai_optimized |
variants | matriz | Sí* | Variantes de mensaje (obligatorias para todos los canales salvo in_app) |
targeting | objeto | No | Audiencia: userIds, filterGroups, excludeFilterGroups, filterOperator, subscriptionPreference |
sendType | cadena | No | immediate, scheduled, triggered, intelligent, recurring o ai_optimized |
scheduledAt | cadena | No | Fecha ISO 8601 para sendType: "scheduled" |
scheduledTimezone | cadena | No | Zona horaria IANA en la que se interpreta la hora programada |
triggerConfig | objeto | No | Regla de disparador para sendType: "triggered" (type, eventName, conditions, reEntry, cooldownHours) |
recurringSchedule | objeto | No | Para sendType: "recurring": frequency (daily/weekly/monthly/custom), cron, dayOfWeek, dayOfMonth, timeOfDay, timezone, endDate, maxOccurrences |
conversionTracking | objeto | No | primaryConversion / secondaryConversions (nombre de evento + condiciones de propiedad), conversionWindowHours, attributionModel (first_touch/last_touch/linear) |
emailConfigId | cadena | No | Identidad de remitente de email guardada (canal de email) |
subscriptionCategoryId | cadena | No | Categoría de consentimiento (lista de suscripción) bajo la que se envía la campaña |
sendVolumeLimit | objeto | No | enabled, 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:
mode | Campos | Se renderiza como |
|---|---|---|
native | title, body, imageUrl, buttons, closeButton, backdropDismissible, style | Los propios componentes de la app, sin web view |
html (por defecto) | html, css | Marcado del autor dentro de un web view |
drag_drop | html, css, grapejsData | Como 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.
| Campo | Tipo | Se aplica en | Significado |
|---|---|---|---|
backgroundColor | string | web, iOS, Android | Fondo de la tarjeta |
textColor | string | web, iOS, Android | Titular y cuerpo (el cuerpo, algo más suave) |
primaryButtonColor | string | web, iOS, Android | Relleno del primer botón |
primaryButtonTextColor | string | web, iOS, Android | Su etiqueta. Si se omite, se elige negro o blanco según el contraste con el relleno |
cornerRadius | number | web, iOS, Android | 0-48. Se ignora en fullscreen, donde las esquinas redondeadas dejarían ver la aplicación |
fontSize | number | web, iOS, Android | 10-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 |
titleWeight | string | web, iOS, Android | regular, medium, semibold, bold. Solo el TITULAR - el cuerpo se mantiene normal por legibilidad |
textAlign | string | web, iOS, Android | auto (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 |
fontFamily | string | web; en móvil, si se puede | En 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 |
customCss | string | solo web | CSS 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:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | cadena | Sí | ID de variante |
name | cadena | Sí | Nombre de variante |
weight | número | Sí | Proporción de tráfico, 0-100 (los pesos deben sumar correctamente según las reglas del canal) |
message | objeto | No | Mensaje 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 |
isControlGroup | booleano | No | Marca 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ámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
status | cadena | - | Filtra por estado (separados por comas si hay varios) |
channel | cadena | - | Filtra por canal (separados por comas si hay varios) |
tags | cadena | - | Filtra por etiquetas (separadas por comas) |
q | cadena | - | Búsqueda de texto libre |
createdBy / editedBy | cadena | - | Filtra por creador / último editor (ID de usuario separados por comas) |
createdFrom | cadena | - | Solo campañas creadas en o después de esta fecha ISO |
page | número | 1 | Número de página |
limit | número | 20 | Resultados 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" }'
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étodo | Ruta | Descripción |
|---|---|---|
POST | /campaigns/:campaignId/send | Lanza la campaña (inicia el envío / activa una campaña activada) |
POST | /campaigns/:campaignId/pause | Pausa una campaña en ejecución |
POST | /campaigns/:campaignId/resume | Reanuda una campaña en pausa |
POST | /campaigns/:campaignId/publish | Aplica 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-draft | Descarta las ediciones almacenadas en búfer de una campaña activa (sin efecto si no hay ninguna) |
POST | /campaigns/:campaignId/cancel | Cancela una campaña |
POST | /campaigns/:campaignId/resend | Reenvía («Enviar de nuevo») una campaña puntual completada según su resendPolicy |
POST | /campaigns/:campaignId/retry | Reintenta una campaña failed (la devuelve a scheduled) |
POST | /campaigns/:campaignId/preview-launch | Resumen previo al lanzamiento (tamaño de audiencia y comprobaciones) sin enviar |
POST | /campaigns/:campaignId/duplicate | Duplica una campaña |
POST | /campaigns/:campaignId/archive | Archiva (detiene los envíos y conserva el historial) |
POST | /campaigns/:campaignId/unarchive | Restaura a un estado sin envío (reanuda explícitamente para volver a enviar) |
POST | /campaigns/:campaignId/stop-recurring | Detiene 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"
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 últimosresendCooldownDaysdí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étodo | Ruta | Descripción |
|---|---|---|
GET | /campaigns/:campaignId/variant-stats | Estadísticas A/B por variante con significación estadística (startDate/endDate) |
GET | /campaigns/:campaignId/links | Estadísticas de clics en enlaces (startDate/endDate) |
GET | /campaigns/:campaignId/failure-reasons | Errores de entrega agrupados por código de error DLR ([{ code, reason, count }]) |
GET | /campaigns/:campaignId/recipients | Destinatarios paginados con el último estado de mensaje (status, limit máximo 200, offset) |
GET | /campaigns/:campaignId/recipients/:userId | Cronología de eventos de mensaje por usuario para esta campaña |
GET | /campaigns/:campaignId/in-app-stats | Estadí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/impressions | Lista 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
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
userId | cadena | Sí | ID de usuario objetivo |
channel | cadena | Sí | email, sms, push o viber |
message | objeto | Sí | subject (email), body (texto simple), html (email). Puede ser {} para viber: el cuerpo de plantilla aprobado es el mensaje |
viberTemplateId | cadena | Solo Viber | Plantilla 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 |
variables | objeto | No | Solo Viber: valores para los campos dinámicos de la plantilla (se combinan en el contexto de personalización) |
triggerData | objeto | No | Contexto disponible para la personalización (type, name, properties, metadata) |
idempotencyKey | cadena | No | Clave 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étodo | Ruta | Descripción |
|---|---|---|
POST | /campaigns/preview | Previsualiza usuarios que cumplen los criterios de audiencia (cuerpo: targeting, limit opcional máx. 500, channel, webAppId) |
POST | /campaigns/spam-check | Comprueba el contenido de email en busca de spam antes de enviarlo |
GET | /campaigns/:campaignId/versions | Enumera instantáneas de versiones guardadas |
GET | /campaigns/:campaignId/versions/compare?v1=&v2= | Compara dos versiones |
GET | /campaigns/:campaignId/versions/:versionId | Obtiene una instantánea de versión |
POST | /campaigns/:campaignId/versions/:versionId/rollback | Revierte a una versión |
GET | /campaigns/:campaignId/history | Historial de registro de auditoría (limit, máximo 200) |
POST | /campaigns/bulk-delete / bulk-duplicate / bulk-archive / bulk-unarchive | Acciones masivas; cuerpo { "ids": [...] }, devuelve { succeeded, failed } |
POST | /campaigns/bulk-tag | Etiquetado masivo; cuerpo { "ids": [...], "tags": [...] } |
POST | /campaigns/:campaignId/send-test-whatsapp / send-test-sms / send-test-push / send-test-in-app | Envía pruebas a un teléfono/usuario antes del lanzamiento |
GET | /campaigns/:campaignId/sto-coverage | Cobertura de optimización de hora de envío para la audiencia |
GET | /campaigns/:campaignId/recurring-status | Estado de la campaña recurrente |
POST | /campaigns/:campaignId/retest-ab | Restablece la ganadora A/B de una campaña recurrente de variante ganadora |
POST | /campaigns/:campaignId/launch-rl | Lanza en modo optimizado por IA (RL) |
GET | /campaigns/:campaignId/rl-stats | Estadísticas del dashboard de campaña optimizada por IA |
POST | /campaigns/:campaignId/pause-rl / resume-rl | Pausa / reanuda una campaña optimizada por IA |
POST | /campaigns/ml-path-warmth | Estado 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"
}
| Estado | Cuándo |
|---|---|
400 | Error 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 |
401 | Falta la clave de API o no es válida |
403 | La clave de API no tiene el permiso campaigns:* necesario |
404 | No se encontró la campaña en este espacio de trabajo |
409 | Conflicto 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
- Crear campañas
- Analítica de campañas
- API de segmentos: crea las audiencias a las que se dirigen las campañas.
- API de Canvas: recorridos de varios pasos.