Mini-juegos in-app
Configura la ruleta de premios, el raspa y gana, la caja sorpresa y la piñata que aparecen como notificaciones in-app.
Los mini-juegos in-app son notificaciones interactivas: en vez de un mensaje estatico, el cliente ve un mini-juego cuyo premio se sortea en el servidor. En el portal se gestionan en su propia seccion — Engagement > Mini-juegos —, separada del area de Notificaciones; por debajo siguen siendo notificaciones in-app con contentType: "game", asi que llegan a las apps por el mismo canal de entrega de siempre.
Las cuatro mecanicas
| Mecanica | Que hace el cliente | Cuando se sortea |
|---|---|---|
| Ruleta de premios | Toca "Girar" y la ruleta gira hasta su gajo | Al tocar el boton |
| Raspa y Gana | Raspa una capa con el dedo y descubre el premio | Al primer trazo de raspado |
| Caja Sorpresa | Elige una de 2 a 4 cajas y la abre | Al tocar la caja |
| Rompe la Piñata | Golpea la piñata hasta romperla (3 a 10 toques) | Al primer toque |
Las cuatro comparten el mismo modelo por debajo: la misma lista de premios (pesos, cupos, limite semanal, "Es premio", identificador externo), el mismo sorteo server-side via el endpoint spin, los mismos eventos (result:/finished:, webhooks) y las mismas cadencias de giro y de cupos. Todo lo que esta guia explica con la ruleta como referencia aplica igual a las otras tres mecanicas — donde dice "girar", lease "jugar" (el gesto inicial de cada mecanica); las diferencias concretas estan en la seccion "Raspa y Gana, Caja Sorpresa y Rompe la Piñata" mas abajo.
El rail sigue pensado para agregar mas plantillas a futuro sin cambios de plataforma ni releases de SDK: cada plantilla es una carpeta con su propio configSchema, y los SDKs ya deployados las reciben como HTML compilado igual que siempre.
Crear un mini-juego
En el portal, anda a Engagement > Mini-juegos y toca "Crear mini-juego". Se abre un asistente que pide dos cosas: el nombre interno y la mecanica — Ruleta de premios, Raspa y Gana, Caja Sorpresa o Rompe la Piñata (el ejemplo de nombre del campo sigue a la mecanica elegida). Al confirmar, el mini-juego queda creado en borrador —aparece en la lista con su badge— con una configuracion de ejemplo editable (las cuatro mecanicas parten igual: 4 opciones con pesos que suman 100, dos premios reales, uno menor y uno de consuelo) y te deja en el Studio, listo para cambiarle los premios y el diseño. La mecanica es la decision irreversible del asistente (define la plantilla que se compila); todo lo demas —premios, diseño, audiencia, publicacion— se edita despues.
Los mini-juegos ya no se crean desde el editor de notificaciones in-app — el selector de tipo de contenido de esa area ofrece solo "Estandar" y "HTML" — y los links viejos a un mini-juego dentro de Notificaciones redirigen solos a la seccion nueva.
Donde se edita cada cosa (el reparto importa, y es el mismo criterio en todo el rail):
| Que | Donde |
|---|---|
| Premios: texto y colores de la opcion, probabilidad, cupos, "Es premio", identificador externo y codigo del premio | Studio → Premios (clic en el gajo sobre la ruleta; en las otras mecanicas, clic en la fila de la lista de premios) |
| Apariencia del juego y pantallas de resultado | Studio (pantalla del juego, Premio, Sin premio, por premio) |
| A quien se le muestra, cuando, y cada cuanto se puede jugar | Formulario del mini-juego (Audiencia, Publicacion, Mecanica) |
Que se puede configurar
El formulario y el panel de premios se generan a partir del schema de la plantilla:
- Titulo, texto del boton ("Girar" por defecto) y prefijo del resultado ("¡Ganaste:" por defecto). El titulo y el prefijo del resultado admiten quedar vacios: borralos y la ruleta se muestra sin titulo, o el resultado muestra solo el nombre del premio. El texto del boton no — vaciarlo dejaria un boton sin etiqueta, asi que vuelve a su default.
- Paleta —
oscuro(default),claro,arenaopersonalizado. Elegir una paleta reescribe de una vez el color de fondo, el de la tarjeta y el del texto. Si despues editas cualquiera de esos tres a mano, la paleta salta sola apersonalizado: el selector nunca miente sobre lo que hay configurado. - Color principal — boton de girar y puntero. Es el color de marca y ninguna paleta lo sobrescribe, para que cambiar de fondo no te borre el color corporativo.
- Color de fondo, color de la tarjeta y color del texto — ajuste fino sobre lo que dejo la paleta. Los bordes y el anillo de la ruleta se derivan del color del texto, asi que una paleta clara no deja elementos invisibles.
- Opacidad de la tarjeta (0–100, default 100) — bajala para que el fondo se vea a traves del panel que contiene la ruleta. En 0 la tarjeta desaparece del todo y el titulo queda directamente sobre el fondo, asi que conviene revisar que se siga leyendo. La tarjeta del resultado nunca se transparenta: tiene que ser legible por encima de la ruleta si o si.
- Imagen de fondo (opcional) — se sube desde el editor (arrastrar o pegar URL) y se guarda en Firebase Storage. Se dibuja sobre el color de fondo, recortada para cubrir la pantalla (
cover, centrada). Elige un color de fondo que combine: la imagen es el unico elemento del mini-juego que se carga por red, asi que si el dispositivo esta sin conexion o la imagen tarda, lo que se ve es el color. Solo se aceptan URLshttp(s); cualquier otra cosa se descarta al guardar. - Opacidad de la imagen (0–100, default 100) — atenua la imagen mezclandola con el color de fondo. En 0 la imagen queda totalmente tapada por el color. Util cuando una foto con mucho detalle le compite a la ruleta.
- Frecuencia de giro «Cada minuto» (
spinRenewal: "minute") — un giro nuevo por cliente por minuto de reloj (UTC). Pensada para QA y campanas de alta frecuencia; los CUPOS no tienen cadencia por minuto (siguen en sin renovacion/mensual/etc.). - Gate de aparicion por giro — con la carta de resultado APAGADA, la entrega no sirve el mini-juego a un cliente que ya giro el periodo actual (ni en el poll ni al pedirlo directo por id: la app recibe null y decide que mostrar). Al abrirse el periodo siguiente, vuelve a aparecer. Con la carta ENCENDIDA se mantiene el replay visible ("mira tu resultado").
- Los mini-juegos no usan impresiones ni cooldown — desde SDK 0.14.0, la aparicion de una in-app de mini-juego se regula SOLO por la frecuencia de giro (el replay idempotente gobierna lo que ve el cliente); los campos de impresiones/cooldown se ocultan en el editor para juegos. Las in-apps normales conservan sus caps.
- Mostrar resultado en la ruleta (
showResultCard, encendido por defecto) — apagalo cuando tu app muestre su propio mensaje de premio cononGameResult: la ruleta gira, marca el gajo un instante (~1,2 s) y la in-app se cierra sola (cuenta como consumida y respeta el frequency cap). Aplica SOLO a giros confirmados por el servidor — el sorteo local de respaldo (sin conexion) y la vista previa del editor siempre muestran el mensaje de la plantilla, porque en esos casos la app no recibe callback. - Fondo transparente (
transparentBackground, apagado por defecto) — con el switch activo, la ruleta deja de dibujar su color de fondo (la app se ve detras) pero la IMAGEN de fondo se mantiene: un PNG con transparencia flota sobre la app (ideal para mascotas o marcos decorativos). Color y opacidad-velo quedan deshabilitados en el editor (aviso "Se ignora con fondo transparente") aunque su valor guardado se conserva por si se apaga el switch; el velo no aplica en modo transparente porque tenniria la app detras. Ver la seccion Fondo transparente mas abajo para el detalle completo y sus requisitos. - Opciones de la ruleta (2 a 12 gajos) — se editan en el Studio, tocando el gajo sobre la ruleta. Cada una con:
- label — el texto que ve el cliente en el gajo y en el resultado. En el gajo se acomoda hasta en 2 lineas, partiendo por palabras. La segunda linea solo aparece si el gajo tiene alto suficiente: con pocos gajos entra, con 10 o 12 el texto vuelve a una sola linea para que no se pisen los vecinos. Una palabra sola mas ancha que el gajo se corta con "…" — no hay donde partirla, asi que conviene evitar palabras muy largas ("participando") si el cupo de texto importa.
- color — color de fondo del gajo.
- textColor (color del texto) — color del label de ESE gajo (default blanco). Las configuraciones guardadas antes de este campo lo heredan automaticamente al editarlas. El titulo, el resultado y los botones siguen el "color del texto" global de la seccion de colores.
- weight (peso) — pondera la probabilidad del gajo en el sorteo. La probabilidad efectiva de cada gajo es
peso / suma de los pesos de los gajos elegibles. No hace falta que los pesos sumen 100; son relativos entre si. Acepta decimales (minimo0.001). Tip: si haces que los pesos sumen 100, cada peso es directamente el porcentaje del gajo (ej.weight: 0.036= 0.036% de probabilidad). - maxWinners (cupo de ganadores) — cuantos clientes pueden ganar ese premio en total.
0(o vacio) significa ilimitado, el uso tipico para gajos como "Sigue participando". Cuando un gajo agota su cupo, sale del sorteo automaticamente: si solo quedan gajos ilimitados, el sorteo sigue entre esos. - weeklyLimit (limite semanal) — cupo maximo de ganadores de ese premio dentro de la semana ISO en curso (lunes a domingo, corte UTC).
0(o vacio) = sin limite semanal. Es un tope secundario que convive conmaxWinners: un gajo puede tener stock total disponible y aun asi quedar fuera del sorteo esta semana porque ya alcanzo su cuota semanal; al cruzar a la semana siguiente, el contador semanal nace en cero y el gajo vuelve a estar disponible (hasta agotar el stock total). UnweeklyLimitmayor quemaxWinnerses un error de configuracion —el stock total se agotaria antes de alcanzar el tope semanal— y el Studio/servidor lo rechazan al guardar. - externalId (identificador externo, opcional) — tu propio codigo para esa opcion (SKU, id de campana, id de cupon…), pensado para mapear el resultado contra TU plataforma sin mantener una tabla de ids internos. Viaja tal cual como
externalIden la respuesta del endpoint spin y en los eventosresult:/finished:(onGameResult/onGameFinished), y comosegmentExternalIden los webhooksinapp_game.played/inapp_game.won(en los eventos de webhookexternalIdya significa "id externo del cliente"); si lo dejas vacio, en todos esos canales vanull. Los valores no vacios deben ser unicos entre las opciones de la ruleta (el servidor rechaza duplicados al guardar); opciones sin codigo puede haber cuantas quieras. No confundir con elsegmentIdinterno (seg_0,seg_1, …), que lo asigna la plataforma, no es editable y sigue siendo el id estable que amarra premios y contadores —externalIdes un alias tuyo que puede cambiar sin afectar nada de eso.
- Frecuencia de giro (
spinRenewal) — cada cuanto un mismo cliente puede volver a girar:none(default) — una sola vez por cliente, de por vida.daily— un giro por dia (corte UTC).weekly— un giro por semana ISO (corte UTC).monthly— un giro por mes calendario (corte UTC).
- Renovacion de cupos (
renewal) — gobierna solo los cupos de cada premio, no cuando puede volver a girar el cliente:none(default) — los cupos son de por vida; no se reponen.monthly— los cupos se reinician cada mes calendario, con corte en UTC. Al cruzar el mes, los contadores del nuevo periodo nacen vacios; el historial de periodos anteriores se conserva.
La vista previa del editor sortea en modo local (preview): girar ahi nunca consume cupos reales.
Los tres primeros bloques de esta lista (textos, colores, imagen de fondo, transparencia) viven en el Studio; frecuencia de giro y renovacion de cupos viven en la pestaña Mecanica del formulario; las opciones de la ruleta, en Studio → Premios.
Raspa y Gana, Caja Sorpresa y Rompe la Piñata
Las tres mecanicas nuevas comparten con la ruleta todo el modelo de premios y de sorteo: la misma lista de opciones (texto, colores, weight, maxWinners, weeklyLimit, "Es premio", externalId, codigo del premio — 2 a 12 opciones), la misma paleta y colores (palette, primaryColor, fondo, tarjeta, texto, opacidades), la misma imagen de fondo y el mismo transparentBackground, las mismas cadencias (spinRenewal — rotulada "Frecuencia de juego" — y renewal), el mismo endpoint spin con su replay idempotente, los mismos eventos result:/finished: y los mismos webhooks. Lo que cambia es como el cliente descubre el premio:
Raspa y Gana (scratch-card)
El premio esta impreso en la tarjeta, cubierto por una capa de raspado que el cliente borra con el dedo (o el mouse). El sorteo se dispara al primer trazo, no al cargar la pagina — mientras el cliente raspa, la respuesta del servidor ya viene en camino. Campos propios:
- Texto de la capa de raspado (
scratchHint, "Raspa aqui" por defecto) — se pinta sobre la capa; admite quedar vacio. - Color de la capa (
scratchColor) y, opcional, una imagen de la capa (scratchImage) — mismo tratamiento que la imagen de fondo (URL https, cae al color si no carga). - Umbral de revelado (
revealThreshold, 20–95, default 60) — porcentaje de la capa que hay que raspar para que el resto se disuelva solo y se muestre el premio. Mas bajo = revelado mas facil.
El rotulo revelado bajo la capa y la carta de resultado no duplican el premio: al abrirse la carta clasica, el rotulo grande ya cumplio su papel y se esconde.
Caja Sorpresa (mystery-box)
Se muestran 2 a 4 cajas (boxCount, default 3) y el cliente elige una. La eleccion es cosmetica a proposito: el sorteo se dispara al tocar la caja y el premio se decide por los pesos de la lista de premios, exactamente igual que un giro de ruleta — la caja tocada es la que lo revela, y las cajas señuelo se abren vacias (nunca muestran un premio real que "se perdio"). Campos propios: texto bajo las cajas (tapHint) y color de las cajas (boxColor).
Rompe la Piñata (pinata)
La piñata cuelga y se agrieta con cada toque; hacen falta 3 a 10 toques (tapsToBreak, default 5) para que explote y suelte el premio. El sorteo se dispara al primer toque: los golpes siguientes solo revelan progresivamente el resultado que el servidor ya confirmo. Campos propios: texto bajo la piñata (tapHint) y color de la piñata (pinataColor).
Diferencias transversales
- No hay boton "Girar": las tres juegan con un gesto directo, asi que no tienen
buttonLabel. El resto de los textos (titulo, prefijo del resultado,showResultCard) funcionan igual que en la ruleta. - En el Studio, el lienzo de Premios es una lista, no una rueda: cada plantilla declara en su manifiesto como se presentan sus premios (
prizePresenter: "wheel"solo para la ruleta;"list"para el resto — y el default seguro para cualquier plantilla futura). Se edita clickeando la fila del premio, con el mismo panel (probabilidad, cupos, "Es premio", Avanzado) y las mismas validaciones; el copy del Studio habla de "premios" donde en la ruleta dice "gajos". - Las pantallas diseñadas, los codigos de premio y los botones con enlace funcionan identico en las cuatro mecanicas: es la misma libreria de pantallas compilada en cada plantilla.
- El respaldo sin conexion es el mismo: si el spin falla tras un reintento, la plantilla sortea localmente solo entre opciones ilimitadas (nunca inventa un premio con cupo) y ese resultado no emite
result:nifinished:. - El mensaje de "ya jugaste" se adapta a la cadencia de
spinRenewaligual que en la ruleta ("vuelve mañana", "la proxima semana", "el proximo mes").
Fondo transparente
Activar Fondo transparente (transparentBackground) hace que la ruleta deje de pintar cualquier fondo propio: ni el color de fondo, ni la imagen de fondo, ni su opacidad se aplican. Detras de la ruleta queda visible la pantalla de la app del cliente, como si el mini-juego flotara sobre ella. La tarjeta que contiene la ruleta y el resultado nunca se transparenta: cardOpacity sigue siendo el unico control de eso, exactamente igual que sin este flag.
Lo que se ve detras del mini-juego no es la app "a pelo": el velo del modal (backdropOpacity, la "Opacidad del velo" del resto del editor de notificaciones in-app) se sigue aplicando por encima de la app y por debajo del mini-juego, igual que en cualquier otra notificacion in-app — en 0% la app queda completamente visible sin oscurecer, valores mas altos la atenuan con un velo negro. Para notificaciones de mini-juego este control todavia no esta expuesto en el editor: el velo queda fijo en su default (54%), asi que hoy la app se ve siempre con ese nivel de atenuacion detras del juego.
Dos requisitos para que se note:
- SDK Flutter 0.12.0 o superior. Un SDK anterior no sabe interpretar
transparentBackground: el mini-juego se sigue mostrando dentro de su contenedor opaco de siempre, sin ningun error — activar el switch simplemente no tiene ningun efecto visual en esos clientes. - Volver a guardar las ruletas publicadas antes de este flag. El fondo transparente depende de la plantilla compilada (manifiesto de la ruleta 1.4.0+): una notificacion cuyo
htmlContentya estaba compilado con una version anterior de la plantilla no lo tiene, aunque el editor te deje activar el switch (el schema que arma el formulario es siempre el vigente). Abrila en el editor, activa Fondo transparente y guarda — el guardado siempre recompila con la plantilla vigente (mismo mecanismo que describe «Limitacion conocida» al final de esta guia).
Dos cadencias desacopladas: giro y cupos
spinRenewal (frecuencia de giro) y renewal (renovacion de cupos) son independientes a proposito. Antes una sola opcion gobernaba las dos cosas a la vez, y combinaciones utiles como "giro diario pero stock unico del mes" no se podian expresar. Ahora:
spinRenewaldecide cuando un cliente puede volver a girar (idempotencia del giro: dentro de su periodo, re-girar devuelve el mismo resultado).renewaldecide cuando se reponen los cupos (maxWinners) de cada premio.
Y hay una tercera capa, aparte de estas dos: el cooldown de aparicion de la in-app (cooldownMinutes del trigger) gobierna cada cuanto se muestra la notificacion. No confundir:
- Cooldown de la in-app = aparicion (cada cuanto se ve la notificacion).
spinRenewal= giro (cada cuanto el cliente puede volver a jugar).
Una in-app puede aparecer varias veces sin que el cliente pueda volver a girar (vera el mismo resultado), o al reves. La ruleta no opina sobre cuando aparece; solo sobre el premio que entrega en cada periodo de giro.
Compatibilidad: las ruletas guardadas antes de que existiera spinRenewal (todo lo compilado hasta hoy) siguen funcionando exactamente igual — en runtime el giro cae a la cadencia de renewal, el comportamiento acoplado de siempre. Y al abrir en el editor una de esas ruletas, el campo «Frecuencia de giro» no aparece en blanco: el editor lo precarga con esa misma cadencia acoplada (la de renewal), asi que re-guardarla sin tocarlo preserva el comportamiento que ya tenia. Para desacoplar el giro de los cupos, cambia «Frecuencia de giro» a mano.
Receta: campana de aniversario (giro diario + stock de campana + limites semanales)
Caso real que motivo esta funcionalidad: una campana de un mes en la que cada cliente gira una vez al dia, con premios de stock unico del mes (la campana no se repite) y un limite semanal por premio para que los premios grandes no se agoten el primer dia.
Configuracion exacta:
-
Frecuencia de giro (
spinRenewal):daily— un giro por cliente por dia. -
Renovacion de cupos (
renewal):none— stock unico, no se repone; la vigencia la acotan las fechas de la in-app (startsAt/expiresAt). -
Cooldown de la in-app (
cooldownMinutesdel trigger):1440(24 h) — la notificacion reaparece una vez al dia, en linea con el giro diario. -
Gajos — probabilidades fijas que suman 100 (pesos relativos
1/1/3/10/15/15/55):Premio weight maxWinners (stock) weeklyLimit 1 Ano Gratis 1 4 1 Cena para dos 1 4 1 40% Descuento 3 500 125 20% Descuento 10 2000 500 Bebida gratis 15 5000 1250 Postre gratis 15 2000 500 Sigue intentando 55 0 (ilimitado) 0 (sin limite) El gajo "Sigue intentando" es la red de seguridad ilimitada: cuando todos los premios con cupo se agotan (por stock total o por su tope semanal), el sorteo sigue entre los ilimitados y ningun cliente ve un error al girar.
Con esto, "1 Ano Gratis" solo se puede ganar 1 vez por semana (4 en todo el mes), mientras el cliente puede seguir girando cada dia y ganando premios menores; y el stock total nunca se repone porque la campana es unica.
Un premio por cliente por periodo de giro
La ruleta entrega un premio por cliente por notificacion, por periodo de giro (spinRenewal). El sorteo ocurre en el servidor la primera vez que el cliente gira en ese periodo; si la misma in-app vuelve a aparecerle (porque el cliente volvio a cumplir el trigger) dentro del mismo periodo de giro, la ruleta no vuelve a sortear — muestra el mismo resultado que ya obtuvo. El mensaje de "ya jugaste" se adapta a la cadencia ("vuelve manana" con daily, "la proxima semana" con weekly, "el proximo mes" con monthly).
Esto es una separacion de responsabilidades deliberada: cuantas veces se muestra la in-app la siguen gobernando sus controles habituales (tipo de trigger, maxImpressions, cooldownMinutes) — la ruleta no opina sobre cuando aparece, solo sobre el premio que entrega la primera vez que el cliente juega en cada periodo de giro.
Solo usuarios logeados
Un premio ganado por un cliente anonimo (que nunca paso por identify, asi que no tiene externalId) no tiene contra quien canjearse en tu plataforma. Para evitarlo, la seccion Segmentacion del editor (el mismo formulario en mini-juegos y en notificaciones in-app) trae el switch "Solo usuarios logeados": con el activo, el servidor descarta la notificacion para los clientes sin identidad en todas las vias por las que puede llegar — el poll del SDK, el fetch directo por id, los triggers por evento (la respuesta de /events/track) y la accion in-app de un journey — sin cambios de SDK (el cliente anonimo simplemente no la recibe). Ademas, el endpoint de spin rechaza el giro de un cliente sin identidad cuando el mini-juego tiene el switch activo (403 IDENTITY_REQUIRED): aunque el HTML ya estuviera entregado o precargado, el premio nunca se materializa para un anonimo — no se crea award, no se consumen cupos y no se emiten webhooks; la ruleta muestra su mensaje generico de giro fallido. El gate es fail-closed: si la identidad del cliente no se puede determinar, se trata como anonimo. Aplica a cualquier notificacion in-app (no solo mini-juegos) y viene apagado por defecto: las notificaciones existentes se siguen sirviendo a todos, igual que hasta ahora.
Version minima del app
Una campana puede depender de una funcion del app host que solo existe desde cierta version — el caso tipico: el app registra el premio del mini-juego en su propia plataforma via onGameResult, algo que sus versiones viejas no saben hacer, asi que un cliente con app vieja podia girar, ganar en el servidor y quedar con un premio imposible de acreditar. Para evitarlo, la seccion Segmentacion del editor trae el campo "Version minima del app" (ej: 8.7.2): con un valor configurado, el servidor descarta la notificacion para los clientes cuyo appVersion (reportado automaticamente por el SDK en cada register/sync) sea menor al minimo — en todas las vias de entrega (poll, fetch por id, triggers por evento y journeys) — y el endpoint de spin rechaza el giro (403 APP_VERSION_TOO_OLD) aunque el HTML ya estuviera entregado: no se crea award, no se consumen cupos, no se emiten webhooks. La comparacion es numerica por segmento (8.7.10 ≥ 8.7.2) y la metadata de build se ignora (8.7.4+51247 cuenta como 8.7.4). El gate es fail-closed: un cliente sin appVersion conocido (SDKs muy viejos o docs incompletos) no la recibe. Vacio = deshabilitado; las notificaciones existentes se siguen sirviendo a todos. Combinable con "Solo usuarios logeados" (deben cumplirse ambos).
Como llega a las apps
Al guardar la notificacion, el servidor compila la plantilla elegida junto con la configuracion en un htmlContent autocontenido (HTML + JS inline, sin dependencias externas). La notificacion se guarda con contentType: "game" pero viaja a los SDKs igual que una notificacion "html" normal: mismo campo htmlContent, mismo renderer.
Unica excepcion al "sin dependencias externas": si configuras una imagen de fondo, el HTML compilado referencia esa URL de Storage. El mini-juego sigue siendo jugable sin ella —la ruleta, el giro y el resultado no dependen de la imagen— pero el fondo cae al color solido cuando no carga.
Consecuencia practica: los mini-juegos funcionan hoy en todos los SDKs ya deployados, sin necesidad de ningun release. El SDK de Flutter (0.10.0+) ademas precarga el HTML completo antes de mostrarlo.
El endpoint spin
El sorteo real ocurre server-side, en una transaccion de Firestore, via:
POST /api/v1/notifications/in-app/spin
Headers — los mismos de autenticacion SDK que el resto de la API REST:
Authorization: Bearer <api_key>
X-Org-Id: <org_slug>
Content-Type: application/json
Body:
{
"clientId": "abc123",
"notificationId": "notif_abc123"
}
Respuestas:
| Status | Cuerpo | Cuando |
|---|---|---|
200 | { "segmentId": "...", "label": "...", "alreadyPlayed": false, "isPrize": true, "awardId": "...", "externalId": "..." } | Sorteo nuevo para este cliente en este periodo. |
200 | { "segmentId": "...", "label": "...", "alreadyPlayed": true, "isPrize": true, "awardId": "...", "externalId": "..." } | El cliente ya habia jugado este periodo: devuelve el mismo resultado guardado (idempotente). |
404 | { "error": "...", "code": "NOT_FOUND" } | La notificacion no existe, no esta activa, o no es de tipo mini-juego. |
409 | { "error": "...", "code": "NO_ELIGIBLE_SEGMENTS" } | Todos los gajos con cupo estan agotados y no queda ninguno ilimitado. |
isPrize, awardId y externalId son campos aditivos sobre la forma base {segmentId, label, alreadyPlayed} (congelada para siempre — un HTML de mini-juego ya compilado la sigue consumiendo igual). isPrize refleja si el gajo ganador esta marcado como "Es premio" en el panel de premios del Studio; awardId ("{spinPeriod}_{clientId}") identifica de forma unica el premio de ese cliente en ese periodo de giro; externalId es el identificador externo que definiste para ese gajo en el Studio — o null si la opcion no lo declara (incluidas todas las configs guardadas antes de que existiera el campo). En el replay idempotente, tanto isPrize como externalId se resuelven contra la config vigente del gajo persistido, no contra un snapshot del giro original.
Las cuatro plantillas llaman a este endpoint en su gesto inicial — la ruleta al tocar "Girar", el raspa al primer trazo, la caja sorpresa al tocar una caja, la piñata al primer toque —; ante un error de red hacen un reintento y, si sigue fallando, caen a un sorteo local solo entre opciones ilimitadas (nunca inventan un premio con cupo).
El Studio: premios, diseño y pantallas
El Studio es una pantalla completa aparte del formulario: entra desde el boton "Studio" en el detalle del mini-juego, o desde "Abrir Studio" en el encabezado del formulario (rotulo de ayuda: Diseño y premios). Al crear un mini-juego, el asistente te deja directamente ahi.
El reparto: el Studio tiene los premios y todo lo visual; el formulario se queda con a quien, cuando y cada cuanto (audiencia, publicacion, mecanica).
Dos canales de guardado, no uno. Por dentro, el Studio guarda por dos mutaciones distintas y separadas a proposito:
- el canal visual solo acepta campos de apariencia — el servidor descarta cualquier otra clave, asi que ni un bug del lienzo ni un cliente manipulado pueden mover un peso o un cupo desde ahi;
- el canal de premios manda la lista completa y corre la misma validacion que el formulario (rangos, colores,
weeklyLimit≤maxWinners, unicidad deexternalId, codigos de premio) antes de recompilar. Preserva las pantallas diseñadas y los codigos que no viajan en la fila.
Guardar manda solo el canal que cambiaste: tocar un color no reescribe los premios.
Si alguien mas guardo mientras trabajabas, ninguna de las dos superficies pisa nada en silencio: el guardado se rechaza y aparece una barra de conflicto con la opcion de Recargar (lo local se descarta) — en el Studio y tambien en el formulario, que es el caso tipico si dejaste las dos pestañas abiertas. Mientras la barra este arriba, el boton de guardar queda deshabilitado. Si tu borrador del Studio esta limpio y el mini-juego cambio por fuera, se rehidrata solo.
El formulario no puede tocar los premios. No es solo que ya no los muestre: al guardar, el servidor toma la lista de premios, los codigos y las pantallas del mini-juego guardado y descarta lo que venga en el formulario. Una pestaña vieja del formulario, entonces, no puede revertir un peso, un cupo ni un diseño aunque lleve horas abierta.
A la izquierda estan las pantallas del juego, hermanas y conectadas (comparten el tema):
- Premios: la ruleta con sus gajos. Toca un gajo para editarlo — arriba lo que se ve (texto y colores), despues probabilidad y cupo (con el porcentaje efectivo en vivo: "5% inicial" y, si conviven cupos finitos e ilimitados, "100% si se agotan los cupos") y el switch "Es premio", y plegado en Avanzado el identificador externo y el codigo del premio. Desde ahi tambien se agrega, duplica, quita y reordena (respetando el minimo y maximo de la plantilla).
- Los ids internos de los gajos (
seg_0,seg_1, …) no cambian nunca: reordenar mueve posiciones, duplicar crea uno nuevo, y quitar no re-keyea a los demas. Es lo que mantiene amarrados los cupos consumidos y los premios ya entregados. - El lienzo de premios es una vista de edicion: los gajos son todos del mismo tamaño, igual que en el juego real (el peso pondera el sorteo, no el tamaño en pantalla). Para ver el juego tal cual queda, usa Pantalla del juego o Vista real.
- Los ids internos de los gajos (
- Pantalla del juego: la ruleta en si — titulo, textos, paleta, colores y fondo. Se muestra compilada de verdad, asi que puedes girarla.
- Premio (generica): la que comparten todos los gajos premiados. Usa los placeholders
{{premio}}(nombre del gajo ganador) y{{codigo}}(el codigo cumplido) en titulos y textos, y el bloque "Codigo del premio" donde aterriza el codigo con su boton de copiar. - Sin premio: la pantalla del gajo consuelo.
- Por premio: un diseño propio para un premio especifico (parte del generico clonado).
Como se diseña: arrastra cualquier elemento a donde quieras sobre el telefono (guias azules lo imantan al centro), estiralo por los tiradores laterales, y haz doble clic sobre un titulo, texto o boton para escribirlo ahi mismo. Desde el panel derecho agregas elementos — Titulo, Texto, Imagen, Boton, Codigo del premio y Linea — y ajustas tamaño, alineacion, colores, esquinas, orden de superposicion y el fondo de la pantalla. Maximo 20 elementos por pantalla.
Las posiciones se guardan en porcentaje de la pantalla, asi que la composicion se adapta a cualquier telefono: usa el selector de modelo de la barra superior para comprobarlo.
Comportamiento:
- Sin diseño configurado, nada cambia: la carta clasica de resultado sigue tal cual (las notificaciones ya publicadas no se ven afectadas). "Quitar diseño" vuelve a ella.
- La pantalla diseñada ocupa todo el viewport (no es un modal) y aparece cuando la rueda se detiene.
- El codigo nunca viaja en el HTML: el bloque de codigo se rellena en runtime desde la respuesta del giro. Si el codigo quedo pendiente, muestra su texto de espera y se completa solo al llegar.
- El bloque de codigo tiene cuatro estados, no dos: con codigo lo muestra; con un cumplimiento realmente pendiente muestra tu texto de espera; cuando la plataforma agoto los reintentos muestra un aviso fijo que NO promete entrega ("reclamalo en el local") en vez de repetir tu promesa para siempre; y cuando no hay codigo posible —el gajo no tiene "Codigo del premio" configurado (una moto, un "1 año gratis"), o el giro cayo al sorteo local sin red— no se pinta. Prometer "tu codigo esta en camino" a quien nunca va a recibir uno era peor que no decir nada. Por eso el Studio avisa si agregas ese bloque a una pantalla que jamas mostrara un codigo, y te ofrece quitarlo si ya estaba.
- El lienzo pinta con el mismo criterio que el dispositivo (incluido el damero de transparencia y el bloque de codigo que no corresponde), y el interruptor "Vista real" muestra el juego compilado de verdad. El codigo de muestra
DEMO-1234aparece solo donde el telefono pintaria un codigo: en una pantalla cuyo premio no emite ninguno, ni el lienzo ni la Vista real lo inventan. - Podes ver los tres estados sin esperar a que pasen: seleccionando el bloque de codigo, el panel derecho ofrece "Con codigo / Esperando / Sin codigo" para que revises como queda tu texto de espera antes de publicar.
- En una ruleta mixta el panel te lo dice: si la pantalla generica tiene bloque de codigo pero algunos de tus premios no emiten ninguno, aparece la lista de esos premios con un atajo para darles su propia pantalla — sin eso, sus ganadores caen en la generica y ven el hueco.
- Con "Fondo transparente" activo, la pantalla diseñada SI tiene fondo propio: usa el color de fondo configurado como respaldo opaco, para que el texto y el codigo se lean. Si el juego fuera transparente tambien ahi, la pantalla quedaria superpuesta a la rueda y a la app. Podes darle su propio color desde Fondo de la pantalla.
- Las pantallas se validan al guardar con la misma dureza que los gajos: colores hex, imagenes solo por URL https y enlaces de boton que pasan la guarda de abajo. Si algo no pasa, no se guarda nada a medias.
- El Studio necesita pantalla de computador; en movil avisa en vez de mostrar un lienzo apretado.
Botones que llevan a algun lado
Cada boton de una pantalla diseñada tiene un selector "Al tocar" con dos formas:
- Cerrar el juego (lo que hacen todos por defecto, y lo que hacian siempre hasta ahora).
- Abrir un enlace: el juego se cierra y tu app recibe el enlace. Es la salida natural de un premio — "Ver mi cupon", "Ir a la tienda", "Canjear ahora" — en vez de dejar al cliente con un codigo y ninguna puerta.
Viaja por el mismo canal que los CTA de las in-apps. No hay una tuberia nueva: el boton emite el mismo mensaje cta: que un boton de in-app estandar, asi que del otro lado el SDK hace exactamente lo de siempre — trackea el clic como cta_click, entrega el enlace a tu handler unificado onDeeplink (con source: inApp) y cierra el mini-juego solo. Tu app no necesita ninguna rama nueva: el enlace cae en el mismo router de applinks que ya resuelve tus deeplinks de push y de in-app.
No hay que actualizar la app. El hub onDeeplink existe desde el SDK 0.8.0: cualquier app con esa version o superior ya recibe estos enlaces sin tocar una linea de codigo ni publicar en las tiendas. Es un cambio del portal — se activa apenas guardas la pantalla y se recompila el mini-juego.
Que acepta el enlace (misma guarda en el editor y al guardar, asi que el error aparece mientras escribes, no al final):
https://…— una URL normal.miapp://…— el esquema propio de tu app, el mismo que ya registras para tus applinks (doggis://coupon/ABC-123,mitienda://producto/42). Es la forma recomendada: lleva a una pantalla de tu app en vez de salir al navegador.- Tambien puedes elegir una campaña de juego de la lista, igual que en el formulario de in-app: se guarda como
southgames://campaign/{id}.
Que rechaza, porque este enlace queda embebido en el HTML que corre en el WebView del cliente y ademas cruza al router nativo:
- Cualquier cosa ejecutable:
javascript:,data:,vbscript:,file:,blob:— incluido el disfraz clasicojavascript://…, que exigir://no basta para atajar. - Esquemas reservados por el sistema operativo o el navegador, que no son "el esquema propio de tu app" porque no son de nadie:
intent:yandroid-app:,content:,market:,package:(Android);itms-services:,itms-apps:,prefs:(iOS);ms-settings:,ms-msdt:,search-ms:,shell:(Windows);chrome:,chrome-extension:,moz-extension:,devtools:,resource:(navegador);ftp:,smb:,ws:,telnet:(transporte no-web).intent://es el mas importante de la lista: en Android nombra paquete y componente explicitos, asi que un enlace asi puede arrancar cualquier app del telefono en cuanto la tuya reenvia lo que no reconoce. - Cualquier enlace con un
#Intent;adentro, sea cual sea el esquema: Android lee ese marcador aunque el enlace empiece con el esquema de tu app. http://plano (usahttps://).- Espacios, comillas, parentesis, backslashes, acentos y caracteres de control: escribe el enlace tal como lo abre tu app, con los acentos percent-encoded (
%C3%A9). - Mas de 500 caracteres.
Los espacios al principio y al final se recortan solos: se guarda exactamente el enlace que el editor valido.
En el portal el boton no navega. El lienzo es una superficie de edicion: un boton con enlace se marca con un distintivo azul de cadena y el destino aparece en el tooltip — y si el enlace no pasa la guarda, el distintivo se pone rojo con un icono de aviso para que se vea sin abrir el bloque. En Vista real —el juego compilado de verdad— tocarlo tampoco navega: aparece un aviso al pie con lo que ese toque haria en el telefono.
Codigos de premio generados por SouthGames
Ademas del flujo de acreditacion en tu backend (seccion siguiente), cada gajo marcado "Es premio" puede configurar "Codigo del premio" → Generar con SouthGames en el Studio → Premios → Avanzado: al ganar, la plataforma emite un promo code unico para ese cliente (prefijo, largo, tipo de descuento, expiracion y usos configurables por gajo) y lo entrega en el mismo giro.
- En la carta de resultado: el cliente ve el codigo con un boton de copiar, sin que tu app haga nada.
- En
onGameResult/onGameFinished(SDK Flutter 0.17.0+): el payload gana el campo aditivoprize({code, fulfillment}) por si tu app quiere guardarlo o mostrar su propia pantalla. - En tus webhooks:
code.created(la emision del codigo, con su detalle) einapp_game.prize_fulfilled(el premio de ese giro quedo cumplido, conawardId+code). - El canje es el de siempre:
POST /api/v1/codes/redeemvalida y consume el codigo como cualquier promo code de la plataforma.
Detalles de comportamiento:
- Un codigo por premio por periodo de giro: el replay idempotente del spin devuelve SIEMPRE el mismo codigo (persistido en el award). Con cadencia de giro renovable, cada periodo nuevo emite codigo nuevo.
- Activarlo NO es retroactivo: solo reciben codigo los giros ocurridos DESPUES de configurar el gajo. Los ganadores anteriores siguen viendo su premio sin codigo, aunque vuelvan a abrir el juego.
- Si la emision falla (transitorio), la respuesta llega con
prize.fulfillment: "pending": la carta avisa y reintenta sola a los 5 segundos; el codigo tambien queda recuperable reabriendo la ruleta (el server reintenta el cumplimiento en cada replay). Un fallo de emision jamas rompe el giro. - Y si el cliente no vuelve, lo recuperamos igual: un barrido corre cada pocos minutos, toma los premios que quedaron sin codigo y reintenta la emision (o la llamada a tu proveedor) por su cuenta, con espera creciente entre intentos — 5 minutos el primero, hasta 1 hora. Es la via que no depende de que nadie reabra nada, y la unica que cubre las ruletas con "Mostrar tarjeta de resultado" apagada, que se cierran solas apenas termina la animacion.
- Cuando ya no da para mas: tras 5 intentos fallidos el premio se marca como sin codigo y el barrido no lo vuelve a tomar — ni aunque arregles el proveedor (un proveedor caido para siempre no puede generar trafico eterno). Le queda una sola via: que el cliente reabra la ruleta. Un premio en ese estado necesita tu atencion de verdad: revisa el proveedor y entrega el premio a mano si hace falta. La cuenta la ves en el detalle del mini-juego y en "Necesita tu atencion" de la Vision general.
- Si apagas el codigo de un gajo, sus premios pendientes NO se pierden: quedan esperando configuracion (no gastan reintentos, no se marcan como fallidos) y el barrido los retoma solo el dia que ese gajo vuelva a emitir codigo. Vale igual si apagas el codigo de TODOS los gajos: la cuenta sigue apareciendo en el detalle del mini-juego.
- El codigo nunca viaja en el HTML compilado — solo en la respuesta del spin del cliente que gano.
- Clientes anonimos tambien reciben codigo (el sujeto es el
clientId); si tu operacion exige identidad, usa el flag "Solo usuarios logeados" de la notificacion.
Codigos desde tu propio proveedor
Si los codigos viven en OTRA plataforma (tu cuponera, tu ERP, un tercero), configura un proveedor de codigos en Configuracion > Integraciones > Proveedores de codigos y elige "Codigo del premio" → Proveedor externo en el gajo: al ganar, SouthGames le pide el codigo a tu URL y lo entrega igual que un codigo propio (carta, onGameResult, webhook inapp_game.prize_fulfilled).
El contrato que tu endpoint debe cumplir:
- Request:
POST(oPUT) HTTPS con el body JSON que definas en la plantilla — los placeholders{{clientId}},{{awardId}},{{segmentId}},{{segmentLabel}}y{{notificationId}}se sustituyen por giro. Cada peticion llevaIdempotency-Key: {notificationId}:{awardId}— deduplica por esa clave: un reintento nuestro (o un replay concurrente) debe devolver el MISMO codigo, no generar otro. - Firma (si activaste "Firmar peticiones"): header
X-Webhook-Signature: sha256=<HMAC-SHA256 hex del body>con el secret que se te mostro al crear el proveedor — verificala antes de emitir. - Respuesta: JSON con el codigo en el campo que declaraste (dot-path, ej.
data.coupon.code). El codigo debe ser ASCII imprimible de hasta 64 caracteres. Cualquier otra cosa se descarta. - Tiempo: el presupuesto es de 2 segundos y UN SOLO intento (sin reintentos) — la peticion ocurre mientras el cliente mira girar la ruleta, y el juego que ya corre en los telefonos deja de esperar a los 8 s. Si no llegas, el premio queda
pending: el cliente ve el aviso en la carta, la plantilla reintenta a los 5 s, cada reapertura de la ruleta vuelve a intentar, y el barrido de pendientes te vuelve a llamar por su cuenta (conIdempotency-Keyigual) hasta 5 veces con espera creciente. Un proveedor caido jamas rompe el giro. - Seguridad del lado de SouthGames: solo URLs HTTPS publicas (nada de redes privadas), sin seguir redirects, y tu respuesta se trata como datos — nunca se interpreta como HTML.
- Prueba antes de publicar: el boton "Probar" del proveedor hace una llamada real con payload sintetico (
notificationId: "test") por el mismo camino del cumplimiento.
Acreditar premios en tu backend
Cada gajo de la ruleta puede marcarse como "Es premio" (isPrize) en Studio → Premios. Cuando un cliente gira y le toca un gajo marcado como premio, el servidor emite el evento de webhook inapp_game.won a los endpoints de tu organizacion suscritos a el; en todo giro genuino (gane o no) tambien emite inapp_game.played. Es la senal que conviene usar para acreditar valor real (puntos, cupones, stock) en tu backend — ver Webhooks > Eventos para el payload completo de ambos.
- Suscribite al evento desde Configuracion > Integraciones > Webhooks, marcando
inapp_game.won(o*para todos los eventos). - Verifica la firma antes de procesar cualquier evento: cada request trae un header
X-SouthGames-Signaturecon el HMAC-SHA256 del body crudo (digest hexadecimal, sin prefijosha256=) usando el secret de tu webhook. El codigo completo en Node.js y Python esta en Webhooks > Verificacion. - Depura por
awardId: si tu servidor no responde 2xx (o hay un error de red), el dispatcher reintenta la entrega hasta 3 veces, asi que el mismo evento puede llegar mas de una vez.awardId("{spinPeriod}_{clientId}") identifica de forma unica el premio de ese cliente en ese periodo de giro — guardalo como clave de idempotencia antes de acreditar nada; si ya lo viste, no acredites de nuevo. - Mapea por
segmentExternalId: el payload deinapp_game.played/inapp_game.wonincluye el identificador externo del gajo ganador que definiste en el Studio (onullsi la opcion no lo declara). Te ahorra mantener en tu backend una tablasegmentIdinterno → premio de tu plataforma. No confundir con elexternalIdde los eventos de cliente (id externo del usuario): en los webhooks el codigo del gajo viaja comosegmentExternalId. - Reporta el canje cuando se haga efectivo: cuando el premio se cobre en tu plataforma (la compra con el descuento, el cupon canjeado), avisale al portal con
POST /api/v1/inapp-games/redemptions— el mismonotificationId/awardIddel webhook, mas el monto de la venta y el descuento aplicado. Con eso el detalle del mini-juego muestra el ROI real de la campana. Ver API REST > Reportar canje de premio.
Nunca acredites valor desde el cliente. La plantilla de la ruleta corre dentro de una WebView sin ningun tipo de confianza: cualquier callback, postMessage o resultado que "diga" el cliente es forjable. El unico resultado en el que conviene confiar para acreditar puntos, cupones o stock es el evento de webhook firmado del lado del servidor: el isPrize que viaja en el payload es el valor que configuraste en el Studio para ese gajo (no gobierna el sorteo en si, que depende de weight/maxWinners/weeklyLimit; solo decide si se emite inapp_game.won), nunca lo que el HTML compilado le muestra al usuario.
Journeys con premios
Ademas de los webhooks, el servidor emite dos eventos internos de journey (no son webhooks ni pasan por /events/track — solo existen dentro del motor de journeys del portal):
| Evento | Cuando se emite | Payload |
|---|---|---|
inapp_game.prize_won | En cada giro nuevo cuyo gajo ganador esta marcado "Es premio" (los replays idempotentes y los gajos "Sigue participando" no lo emiten). | { notificationId, awardId, segmentId, segmentLabel, segmentExternalId } |
inapp_game.redeemed | En cada canje registrado con exito (POST /api/v1/inapp-games/redemptions → 200, en cualquiera de los dos modos de identificacion, override incluido). | { notificationId, awardId, segmentId, segmentExternalId, saleTotal, discountAmount, currency } |
Ambos aparecen como sugerencias en el editor de journeys (disparador por evento y nodo "Esperar evento") y sirven tanto para disparar un journey como para soltar una espera. El caso de uso tipico — recordarle el canje a quien gano y no lo cobro:
- Disparador por evento
inapp_game.prize_won: el cliente entra al journey al ganar un premio real. - Esperar evento
inapp_game.redeemedcon timeout1440minutos (24 h) y accion al timeout "Continuar por timeout": si tu backend reporta el canje dentro de las 24 h, el journey sigue por la rama del evento (o termina ahi); si no, sigue por la rama de timeout. - Push de recordatorio en la rama de timeout: "Tenes un premio sin canjear".
Notas de comportamiento:
- La emision es fire-and-forget: un journey caido jamas afecta la respuesta del spin ni del canje.
- El
clientIdcon el que se enrola/matchea es el dueño del award (siguiendo lapidas de merge al momento de emitir). Dos limites del motor a tener presentes: (1) si el cliente se FUSIONA con otro registro entre el giro y el canje, el enrolamiento quedo con el id antiguo y elredeemedllega con el nuevo — la espera no se suelta y el recordatorio puede enviarse igual; (2) la espera matchea por evento + cliente, sin distinguir premios: si un cliente tiene dos premios pendientes, el canje de cualquiera suelta la espera del otro (y un cliente ya enrolado no se re-enrola por un segundo premio). Para el caso tipico — un premio pendiente por cliente — funciona exacto. - La espera se suelta via el cron de journeys (corre cada pocos minutos), no de forma instantanea.
- Los canjes rechazados (409 duplicado, 404 inexistente, 422 gajo no-premio) no emiten nada.
Recibir el resultado en la app (onGameResult)
Aparte del webhook server-to-server de arriba, el SDK cliente expone un callback para que tu app reaccione al resultado del giro apenas el servidor lo confirma. Es el camino pensado para cuando el premio vive en tu propia plataforma (tu programa de puntos, tu billetera, tu backend de cupones) y necesitas llamarla vos mismo, con tus propias credenciales, en vez de (o ademas de) esperar el webhook.
En Flutter (SDK 0.11.0+), SouthGamesNotificationOverlay acepta un callback onGameResult con el mismo patron que el onCtaTap existente:
SouthGamesNotificationOverlay(
// ...resto de la config habitual del overlay...
onGameResult: (SouthGamesGameResult result) async {
// result: { notificationId, segmentId, label, isPrize, awardId, alreadyPlayed, externalId }
if (result.isPrize) {
await miPlataforma.acreditarPremio(
idempotencyKey: result.awardId, // nunca acredites dos veces el mismo awardId
premio: result.label,
);
miApp.mostrarPantallaDePremio(result.label);
}
},
)
Algunos detalles del comportamiento:
- Se dispara temprano.
onGameResultllega apenas el servidor confirma el giro — ANTES de que termine la animacion de la ruleta (unos 4 segundos) — para que tu llamada a tu plataforma corra en paralelo al giro visual en vez de sumarse despues. El overlay del mini-juego sigue su curso solo (la tarjeta de resultado y eldismissno dependen de este callback ni se ven afectados por el). awardIdes tu clave de idempotencia. El mismo giro puede reintentarse (reconexion, la misma in-app que vuelve a aparecer dentro del mismo periodo) y siempre trae el mismoawardId; guardalo antes de acreditar y no proceses dos veces el mismo valor.- La llamada sale del dispositivo.
onGameResultcorre dentro de la app del cliente — un entorno que no controlas. Igual que cualquier resultado que "diga" un WebView, es forjable en principio: la validacion (autenticacion, limites, anti-fraude) es responsabilidad de tu propia plataforma cuando reciba la llamada. - El webhook
inapp_game.wonsigue siendo la verificacion opcional server-side. Si necesitas una fuente en la que confiar sin depender del dispositivo, usa el webhook (ver arriba) para verificar despues de acreditar, o directamente como unica fuente de verdad si preferis no acreditar nunca desde el callback del cliente. - Mensajes malformados se ignoran. Si el canal nativo entrega algo que no parsea como el resultado esperado, el SDK lo descarta silenciosamente (log de debug, nunca una excepcion) — tu callback no se llama para eventos corruptos.
- Estas garantias dependen de la plantilla compilada, no del SDK. El payload completo y las garantias de arriba (emision temprana, confirmado por el server, replays marcados con
alreadyPlayed) existen recien desde la plantilla de ruleta 1.3.1. Una notificacion guardada con una plantilla anterior sigue trayendo elhtmlContentviejo — que emiteresult:a la vieja usanza: recien despues de que termina la animacion, y tambien para el sorteo local de respaldo y para los replays — asi que contra el SDK 0.11.0 tu callback recibeisPrize: false,awardId: null,notificationId: ''yalreadyPlayed: false(fail-safe: nunca te hace acreditar de mas, pero tampoco te da nada util). Para que una ruleta ya publicada tenga el payload y las garantias completas hay que volver a abrirla en el editor y guardarla — eso la recompila con la plantilla vigente (ver «Limitacion conocida» abajo). Trata cualquier resultado conawardId == nullcomo legacy / no verificado (no tiene clave de dedupe confiable) y nunca acredites valor a partir de el.
El segundo evento: cuando la rueda se detiene (onGameFinished)
onGameResult llega temprano a proposito — pero navegar a tu pantalla de premio en ese momento cortaria el giro a la mitad. Para eso la plantilla emite un segundo evento, finished: (mismo canal nativo que result:), cuando la animacion de la ruleta termina, con exactamente el mismo payload que result:. El contrato de dos eventos:
result:/onGameResult= precarga. Llega apenas el servidor confirma el giro; usa esos ~4 segundos de animacion para llamar a tu plataforma en paralelo.finished:/onGameFinished= navegacion. Llega cuando la rueda se detuvo — el momento correcto para mostrar tu pantalla de premio, con los datos ya precargados.
finished: se emite siempre que hubo result: y la animacion llego a su fin — con la tarjeta de resultado encendida o apagada, replays con alreadyPlayed: true incluidos — y nunca para el sorteo local de respaldo (ahi tampoco hubo result:). La excepcion que debes manejar: el boton de cerrar sigue activo durante el giro, asi que si el usuario cierra la in-app a mitad de la animacion, finished: ya no llega — pero result: si llego y el premio quedo confirmado server-side. Por eso: no difieras a onGameFinished nada critico — acredita/decide con onGameResult (o con el webhook inapp_game.won como respaldo confiable) y usa onGameFinished solo como señal de UI/navegacion. Requiere plantilla de ruleta 1.7.0+: una notificacion ya publicada necesita volver a guardarse para recompilarse (ver «Limitacion conocida» abajo).
Limitacion conocida
Como la compilacion ocurre al guardar, un fix en la plantilla (content/inapp-games/{gameId}/) no recompila automaticamente las notificaciones ya guardadas — solo aplica a notificaciones nuevas o a una notificacion existente cuando se vuelve a editar y guardar (lo que dispara una recompilacion con la plantilla vigente). Si necesitas que un fix de plantilla llegue a una notificacion ya publicada, hay que volver a abrirla en el editor y guardarla.