Jugar

Ejecutar una jugada en una campana.

POST/api/v1/games/play

Ejecuta una jugada. Genera codigo promo si gana. Otorga XP y puntos automaticamente.

Request body

ParametroTipoDescripcion
campaignId*stringID de la campana
externalUserId*stringID externo del usuario
answersnumber[]Respuestas para trivia (indice por pregunta)
gameResultobjectResultado que reporta TU juego (ver abajo). Los juegos que resuelve el servidor lo ignoran
gameResult.scorenumberPuntaje del jugador. UNICA via por la que el puntaje llega al ranking
gameResult.wonbooleanSi el jugador gano. Solo se honra en campanas con rewardType none
gameResult.metadataobjectDatos 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.

  • score es obligatorio si te importa el ranking. Si no lo mandas, la sesion se guarda con score: 0, y leaderboardEntries.totalScore queda 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/rankings es solo de lectura.
  • won solo se honra en campanas con rewardType: "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

CampoTipoDescripcion
rewardTypestringQue entrega la campana al ganar: promo_code, points o none. Tratalo como cadena abierta (ver Campanas)
awardsPrizebooleanfalse cuando ganar no entrega nada (rewardType: "none")
prizeCodestring | nullCodigo emitido por la victoria. Siempre null si rewardType no es promo_code
messagestring | nullMensaje para el jugador escrito por el servidor, siempre en espanol. null = usa tu propio texto
messageCodestring | nullCodigo 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.

messageCodeCuandoQue decir
NO_PRIZE_RANKING_ONLY_WINGano una partida de una campana noneGano, no hay premio por partida, su puntaje compite en el ranking
NO_PRIZE_RANKING_ONLY_LOSEPerdio una partida de una campana noneNo 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:

  • won sigue siendo true y el score se guarda igual — el ranking depende de eso. Acuerdate de mandarlo en gameResult.score.
  • prizeCode es null y awardsPrize es false.
  • No se consume presupuesto de codigos ni la cuota de rewardLimits (no hay premio que limitar), asi que la respuesta tampoco trae limits.winsRemaining.
  • En juegos del marketplace el servidor honra el gameResult.won que mandes en vez de sortear el resultado con winProbability: sin premio que farmear, el ranking debe reflejar lo que hizo el jugador. Si no mandas won, se cae al sorteo de siempre.
  • message llega 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

HTTPcodeCuando
400VALIDATION_ERRORFalta campaignId o externalUserId (o answers en trivia)
400CAMPAIGN_INACTIVELa campana no esta activa
400CAMPAIGN_NOT_PLAYABLELa campana no es de tipo juego
400CLIENT_REQUIREDCampana con premio en puntos y usuario sin registrar
400CAMPAIGN_MISCONFIGUREDCampana points sin pointsReward configurado
400GAME_NOT_INSTALLEDJuego del marketplace no instalado o inactivo
400UNSUPPORTED_GAME_TYPEgameType desconocido
403PLAYER_BANNEDEl 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
404NOT_FOUNDCampana inexistente
429PLAY_LIMIT_REACHEDLimite 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/play y el spin de in-app responden 403. El corte aplica tanto si el ban se teclo por externalUserId como si se teclo por el clientId del jugador (clientes que todavia no tenian externalId).
  • Rankings: GET /rankings no devuelve al baneado en el top-N (los puestos se renumeran sin huecos) y su "mi ranking" (?externalUserId=...) vuelve con entry: 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 jugada
  • game.won — cuando el usuario gana (tambien en campanas none: se gano la partida, aunque prizeCode venga en null)