Jugar
Ejecutar una jugada en una campana.
/api/v1/games/playEjecuta una jugada. Genera codigo promo si gana. Otorga XP y puntos automaticamente.
Request body
| Parametro | Tipo | Descripcion |
|---|---|---|
| campaignId* | string | ID de la campana |
| externalUserId* | string | ID externo del usuario |
| answers | number[] | Respuestas para trivia (indice por pregunta) |
| gameResult | object | Resultado que reporta TU juego (ver abajo). Los juegos que resuelve el servidor lo ignoran |
| gameResult.score | number | Puntaje del jugador. UNICA via por la que el puntaje llega al ranking |
| gameResult.won | boolean | Si el jugador gano. Solo se honra en campanas con rewardType none |
| gameResult.metadata | object | Datos extra que se guardan junto a la sesion |
Reportar el resultado de tu juego (gameResult)
Cuando el juego corre en tu host —un juego del marketplace embebido en un
WebView, o uno propio— el servidor no tiene forma de saber que paso: hay que
contarselo en gameResult.
scorees obligatorio si te importa el ranking. Si no lo mandas, la sesion se guarda conscore: 0, yleaderboardEntries.totalScorequeda en cero para siempre. El ranking termina decidido por victorias y volumen de jugadas en vez de por habilidad. No hay ningun otro endpoint para mandarlo:/api/v1/rankingses solo de lectura.wonsolo se honra en campanas conrewardType: "none"— ahi el ranking es todo el premio y no hay nada que farmear. En campanas que si entregan premio el resultado lo decide el servidor (probabilidad de la campana) y este campo se ignora.- Los juegos que resuelve el servidor (trivia, ruleta, raspa y gana,
tragamonedas) ignoran el objeto completo: su resultado y su puntaje se
calculan server-side (en trivia, a partir de
answers).
{
"campaignId": "campaign_abc",
"externalUserId": "user_123",
"gameResult": { "score": 1840, "won": true }
}
En los SDKs propios:
// @southgames/js
await sg.play({
campaignId: "campaign_abc",
externalUserId: "user_123",
gameResult: { score: 1840, won: true },
});
// southgames_flutter — SouthGamesGameView ya lo manda solo:
// lee el score del puente postMessage y lo reenvia.
await SouthGamesSDK.instance.play(
campaignId: 'campaign_abc',
externalUserId: 'user_123',
gameResult: const GameOutcome(score: 1840, won: true),
);
Ejemplo
curl -X POST https://southgames.ai/api/v1/games/play \
-H "Authorization: Bearer sg_live_xxx" \
-H "X-Org-Id: mi-empresa" \
-H "Content-Type: application/json" \
-d '{
"campaignId": "campaign_abc",
"externalUserId": "user_123"
}'
Respuesta
{
"sessionId": "session_xyz",
"result": "Premio: 20% de descuento",
"won": true,
"prizeCode": "SG-AB2C-XY9Z",
"rewardType": "promo_code",
"awardsPrize": true,
"message": null,
"messageCode": null,
"metadata": {
"segment": "Premio: 20% de descuento",
"prizeType": "discount",
"discountType": "percentage",
"discountValue": 20
},
"xpAwarded": 50,
"newTotalXp": 320,
"tierUp": {
"previousTier": "Bronce",
"newTier": "Plata",
"newTierColor": "#C0C0C0"
},
"pointsAwarded": 10,
"newPointsBalance": 150,
"limits": {
"playsRemaining": 2,
"playLimitPeriod": "day",
"winsRemaining": 1,
"rewardLimitPeriod": "day"
}
}
Campos de premio
| Campo | Tipo | Descripcion |
|---|---|---|
rewardType | string | Que entrega la campana al ganar: promo_code, points o none. Tratalo como cadena abierta (ver Campanas) |
awardsPrize | boolean | false cuando ganar no entrega nada (rewardType: "none") |
prizeCode | string | null | Codigo emitido por la victoria. Siempre null si rewardType no es promo_code |
message | string | null | Mensaje para el jugador escrito por el servidor, siempre en espanol. null = usa tu propio texto |
messageCode | string | null | Codigo estable del message, para que localices el texto tu mismo. Cadena abierta |
awardsPrize sale de una lista blanca (promo_code o points): si la campana
trae un rewardType desconocido, awardsPrize es false y no se emite nada —
el lado seguro.
Localizar el message
El endpoint no recibe locale, asi que message viene siempre en espanol. Si tu
app esta en otro idioma, no lo pintes tal cual: mapea messageCode a tu
propio catalogo y usa message solo como respaldo.
messageCode | Cuando | Que decir |
|---|---|---|
NO_PRIZE_RANKING_ONLY_WIN | Gano una partida de una campana none | Gano, no hay premio por partida, su puntaje compite en el ranking |
NO_PRIZE_RANKING_ONLY_LOSE | Perdio una partida de una campana none | No gano, pero su puntaje quedo compitiendo en el ranking |
Tratalo como cadena abierta: ante un codigo que no conozcas, muestra message.
Campanas sin premio (rewardType: "none")
Cuando la campana premia solo por ranking, ganar una partida NO entrega nada:
wonsigue siendotruey elscorese guarda igual — el ranking depende de eso. Acuerdate de mandarlo engameResult.score.prizeCodeesnullyawardsPrizeesfalse.- No se consume presupuesto de codigos ni la cuota de
rewardLimits(no hay premio que limitar), asi que la respuesta tampoco traelimits.winsRemaining. - En juegos del marketplace el servidor honra el
gameResult.wonque mandes en vez de sortear el resultado conwinProbability: sin premio que farmear, el ranking debe reflejar lo que hizo el jugador. Si no mandaswon, se cae al sorteo de siempre. messagellega tanto al ganar como al perder, y explica que no hay premio por partida. Muestralo en vez de un "Ganaste"/"Sigue intentando" a secas: sin eso el ganador queda esperando un premio que nunca llega y el perdedor nunca se entera de que su puntaje quedo compitiendo.
Errores
| HTTP | code | Cuando |
|---|---|---|
| 400 | VALIDATION_ERROR | Falta campaignId o externalUserId (o answers en trivia) |
| 400 | CAMPAIGN_INACTIVE | La campana no esta activa |
| 400 | CAMPAIGN_NOT_PLAYABLE | La campana no es de tipo juego |
| 400 | CLIENT_REQUIRED | Campana con premio en puntos y usuario sin registrar |
| 400 | CAMPAIGN_MISCONFIGURED | Campana points sin pointsReward configurado |
| 400 | GAME_NOT_INSTALLED | Juego del marketplace no instalado o inactivo |
| 400 | UNSUPPORTED_GAME_TYPE | gameType desconocido |
| 403 | PLAYER_BANNED | El jugador fue baneado por la organizacion: no puede jugar. No se crea sesion, ni codigo, ni puntos, ni entrada de ranking. Muestra el error del body tal cual (es el mensaje para el jugador) y no reintentes |
| 404 | NOT_FOUND | Campana inexistente |
| 429 | PLAY_LIMIT_REACHED | Limite de jugadas del periodo alcanzado |
El body de error siempre es { "error": string, "code": string }. Trata
code como cadena abierta.
Jugadores baneados (PLAYER_BANNED)
El dashboard permite banear jugadores (tipicamente por nombres inapropiados
en el ranking). Un jugador baneado recibe 403 PLAYER_BANNED en este
endpoint y en el spin de mini-juegos in-app, y desaparece de todos los
rankings. El mensaje del body ("Tu cuenta no puede participar en los
juegos") esta pensado para mostrarse al jugador tal cual: no lo reemplaces
por un "intenta de nuevo" — reintentar no sirve mientras el ban siga.
Que cubre exactamente el ban:
- Jugar:
POST /games/playy el spin de in-app responden 403. El corte aplica tanto si el ban se teclo porexternalUserIdcomo si se teclo por elclientIddel jugador (clientes que todavia no tenianexternalId). - Rankings:
GET /rankingsno devuelve al baneado en el top-N (los puestos se renumeran sin huecos) y su "mi ranking" (?externalUserId=...) vuelve conentry: null. Tampoco puede ganar premios de ranking, ni por periodo ni en la premiacion manual. - NO cubre codigos ni canjes: lo que el jugador ya habia ganado sigue
siendo suyo y sigue siendo canjeable. Por eso el spin sigue devolviendo el
replay idempotente (
alreadyPlayed: true) de un premio ganado ANTES del ban, en vez de un 403: es la superficie donde el jugador ve ese codigo. Solo los giros NUEVOS se rechazan.
Webhooks
Se disparan los siguientes eventos:
game.played— en cada jugadagame.won— cuando el usuario gana (tambien en campanasnone: se gano la partida, aunqueprizeCodevenga ennull)