# API Xendrai PracmaticD v1

Guía de integración servidor-a-servidor para fichas, sesiones y launcher con skins.

- Versión del contrato: `1.0.0`
- Fecha: 2026-09-01
- API HTTPS: `https://api-xendrai.2.25.195.114.nip.io`
- Launcher HTTPS: `https://games-xendrai.2.25.195.114.nip.io`
- OpenAPI: `openapi-pracmaticd.yaml`

## 1. Qué ofrece

Un casino cliente puede consultar su catálogo autorizado, reservar fichas, crear
una sesión idempotente, abrir el juego mediante una URL temporal, consultar el
estado contable y cerrar/liquidar la sesión. El launcher aplica automáticamente
el skin, HUD, carteles y calibración definidos por Xendrai.

El cliente nunca debe construir ni abrir directamente una URL del proveedor.
Debe abrir únicamente `launchUrl`.

## 2. Alcance legal y comercial

La instalación publicada usa actualmente juegos demo cargados desde
`demogamesfree.pragmaticplay.net` y administra fichas virtuales. El endurecimiento
descripto en este documento es una base técnica de producción; no concede derechos
comerciales sobre los juegos, licencia de apuestas, homologación de RNG ni
certificación regulatoria.

Antes de operar con dinero real corresponden, según jurisdicción, acuerdos con el
proveedor, licencias, KYC/AML, juego responsable, certificación técnica, pentest,
protección DDoS, backups externos, monitoreo y procedimientos de incidentes.

## 3. Credenciales y seguridad

Xendrai entrega por un canal seguro:

- `clientId`;
- `apiSecret`;
- moneda y valor de ficha;
- dominios permitidos para `returnUrl`;
- juegos habilitados;
- fichas mayoristas disponibles.

El secreto existe exclusivamente en el backend del casino. No debe aparecer en
JavaScript de navegador, HTML, URLs, repositorios, analytics o logs.

Todas las rutas `/v1/client/*` requieren:

```http
X-Client-Id: <clientId>
X-Timestamp: <Unix timestamp en segundos>
X-Nonce: <valor único de 12 a 120 caracteres>
X-Signature: <HMAC-SHA256 hexadecimal>
```

Reglas:

- tolerancia del timestamp: 300 segundos;
- nonce: `[A-Za-z0-9_.:-]{12,120}`;
- cada nonce se consume una sola vez por cliente durante 10 minutos;
- el reloj del servidor cliente debe sincronizarse por NTP;
- cada solicitud usa timestamp, nonce y firma nuevos.

### Firma canónica

```text
UPPERCASE_HTTP_METHOD + "\n" +
CANONICAL_PATH + "\n" +
TIMESTAMP + "\n" +
NONCE + "\n" +
SHA256_HEX(RAW_BODY)
```

```text
X-Signature = hex(HMAC_SHA256(apiSecret, canonicalString))
```

El path incluye `/v1`, pero no incluye el host ni el query string. En un GET se
firma el SHA-256 del body vacío. En un POST se serializa el JSON una sola vez y
se firman exactamente los mismos bytes UTF-8 que luego se envían.

## 4. Modelo de fichas

| Campo | Significado |
|---|---|
| `availableChips` | Fichas libres de la cuenta mayorista del casino. |
| `lockedChips` | Fichas reservadas en sesiones activas. |
| `totalChips` | Suma de disponibles y bloqueadas. |
| `chips` | Total actual del jugador informado por el backend cliente. |
| `reserveChips` | Parte del total que se asigna a la sesión. |
| `coinValue` | Valor por ficha configurado por Xendrai. |

```text
balance = sessionChips × coinValue
```

Al crear la sesión, `reserveChips` pasa de disponible a bloqueado. Durante el juego
la reserva sigue el saldo detectado. Al cerrar o vencer la sesión, la liquidación
libera al disponible las fichas finales una sola vez.

## 5. Endpoints v1

### Salud

```http
GET /v1/health
```

Público. Comprueba API, MySQL y mantenimiento de sesiones vencidas.

### Catálogo público

```http
GET /v1/games
```

Público. Devuelve solo `symbol`, `name`, `provider` y `enabled`. El catálogo actual
tiene 623 juegos, pero debe tratarse como dinámico mediante `catalogVersion`.

### Cuenta del cliente

```http
GET /v1/client/account
```

Firmado. Devuelve configuración, dominios, permisos, `availableChips`,
`lockedChips` y `totalChips`.

### Catálogo autorizado

```http
GET /v1/client/games
```

Firmado. Es el endpoint recomendado para construir el lobby de cada casino.

### Crear sesión

```http
POST /v1/client/launch
Idempotency-Key: <clave única de 16 a 120 caracteres>
Content-Type: application/json
```

Body:

```json
{
  "gameSymbol": "vs20wildparty",
  "externalPlayerId": "user_18452",
  "displayName": "Jugador 18452",
  "chips": 500,
  "reserveChips": 100,
  "returnUrl": "https://casino-cliente.example/juegos",
  "closeOnZero": true,
  "ttlMinutes": 60
}
```

La clave idempotente coincide con `[A-Za-z0-9_.:-]{16,120}` y se conserva 24
horas. La misma clave y el mismo body recuperan la misma respuesta. La misma clave
con otro body responde `409 idempotency_conflict`.

No enviar `currency`, `coinValue`, `coin_value` ni `balance`: los administra
Xendrai. `reserveChips` debe ser positivo y no superar `chips`; el juego, jugador
y `returnUrl` deben estar autorizados.

Respuesta abreviada:

```json
{
  "ok": true,
  "requestId": "req_...",
  "token": "<opaco>",
  "launchUrl": "https://games-xendrai.2.25.195.114.nip.io/?token=<opaco>",
  "session": {
    "sessionId": "66bbda1f-6f67-4dd4-8490-70fa24076abd",
    "externalPlayerId": "user_18452",
    "balance": 2500,
    "chips": 100,
    "currency": "ARS"
  },
  "game": {
    "symbol": "vs20wildparty",
    "name": "3 Buzzing Wilds",
    "provider": "pragmaticplay"
  },
  "meta": {
    "idempotentReplay": false
  }
}
```

El `token` y la URL completa son credenciales temporales. No registrarlos.

### Listar sesiones

```http
GET /v1/client/sessions?limit=50
```

Firmar `/v1/client/sessions`, sin query string. `limit` admite 1–100.

### Conciliar una sesión

```http
GET /v1/client/session?sessionId=<UUID>
```

Firmar `/v1/client/session`, sin query string. Devuelve estado, saldo, fichas,
moneda y detalle de la reserva. El servidor solo permite consultar sesiones del
cliente autenticado.

### Cerrar y liquidar

```http
POST /v1/client/session/close
Content-Type: application/json

{
  "sessionId": "66bbda1f-6f67-4dd4-8490-70fa24076abd",
  "reason": "player-returned-to-lobby"
}
```

Es idempotente: repetirlo devuelve la sesión cerrada sin volver a acreditar
fichas. El casino debe llamarlo al cerrar el juego y como recuperación si el
navegador desaparece. Las sesiones que superan su TTL se expiran y liquidan
automáticamente; el mantenimiento se ejecuta cada minuto.

## 6. Flujo recomendado del casino

1. Autenticar al jugador en el casino.
2. Consultar y cachear `GET /v1/client/games`.
3. Derivar `externalPlayerId` de la sesión local y leer `chips` desde la base de datos.
4. Crear localmente una operación `requested` y su `Idempotency-Key`.
5. Llamar a `/v1/client/launch` y persistir el `sessionId`.
6. Entregar al navegador solo la `launchUrl`; abrirla como navegación o iframe autorizado.
7. Marcar la operación `launched` sin guardar token ni URL completa en logs.
8. Al volver al lobby, consultar `/v1/client/session` y cerrar con `/session/close`.
9. Ejecutar una conciliación periódica de operaciones `launched` o `uncertain`.

Si un launch produce timeout, no se crea una clave nueva. Se repite únicamente
con la misma `Idempotency-Key` y el mismo body o se concilia la operación.

## 7. SDK Node.js incluido

Requiere Node.js 18 o posterior.

```js
import crypto from "node:crypto";
import { XendraiClient } from "./xendrai-integration-kit/xendrai-client.mjs";

const client = new XendraiClient({
  baseUrl: process.env.XENDRAI_API_BASE,
  clientId: process.env.XENDRAI_CLIENT_ID,
  apiSecret: process.env.XENDRAI_API_SECRET,
});

const games = await client.listAllowedGames();
const account = await client.account();

const launch = await client.createLaunch({
  idempotencyKey: crypto.randomUUID(),
  gameSymbol: games[0].symbol,
  externalPlayerId: "user_18452",
  chips: 500,
  reserveChips: 100,
  returnUrl: process.env.XENDRAI_RETURN_URL,
  ttlMinutes: 60,
});

const state = await client.getSession(launch.session.sessionId);
await client.closeSession({ sessionId: state.sessionId });
```

Operaciones expuestas:

- `health()`
- `listGames()`
- `account()`
- `listAllowedGames()`
- `createLaunch(input)`
- `listSessions(limit)`
- `getSession(sessionId)`
- `closeSession({ sessionId, reason })`

Verificación:

```bash
cd xendrai-integration-kit
npm test
```

## 8. Errores y observabilidad

```json
{
  "ok": false,
  "error": "Firma cliente invalida",
  "code": "invalid_signature",
  "requestId": "req_..."
}
```

Usar `code` para lógica y conservar `requestId` para soporte. La API diferencia
`400`, `401`, `403`, `404`, `409`, `429` y `500`. Un `409 nonce_replayed` exige
nuevo nonce y firma; un `409 idempotency_conflict` requiere revisar la operación.

Límites configurados:

- rutas cliente: 30 solicitudes/segundo por `X-Client-Id`;
- rutas públicas: 10 solicitudes/segundo por IP;
- body máximo: 64 KiB.

El integrador debe registrar sin secretos: ID local de operación, `requestId`,
`sessionId`, jugador, juego, fichas, timestamps, HTTP y estado de conciliación.

## 9. Cierre de navegador y recuperación

Los eventos `pagehide` o `beforeunload` no garantizan entrega. Úselos solo como
optimización; la garantía real debe estar en el backend:

- cierre explícito al volver al lobby;
- consulta periódica de sesiones pendientes;
- cierre idempotente de sesiones abandonadas;
- expiración automática a los 120 minutos por defecto.

No hay webhook externo v1 en esta entrega. La conciliación soportada es polling
HMAC mediante `GET /v1/client/session` y cierre explícito idempotente.

## 10. Superadmin y calibración

El Superadmin es exclusivo del proveedor Xendrai y no forma parte del contrato
del casino cliente. Permite administrar clientes, fichas, jugadores, catálogo,
permisos, auditoría y calibración por proveedor o por juego. Los clientes nunca
deben recibir credenciales administrativas ni usar rutas `/admin/*`.

La calibración persiste en MySQL y admite carteles con texto vacío. El selector
enumera todo el catálogo; los cambios se aplican al launcher sin que el casino
tenga que modificar su integración.

## 11. Pruebas de aceptación

- salud y catálogo responden por HTTPS;
- catálogo autenticado contiene solo juegos autorizados;
- firma alterada, timestamp vencido y nonce repetido son rechazados;
- una clave idempotente repetida con el mismo body devuelve el mismo `sessionId`;
- la clave repetida con otro body responde 409;
- una reserva insuficiente o un dominio no autorizado son rechazados;
- la `launchUrl` abre el juego con su skin y calibración;
- consulta y cierre reflejan el saldo final;
- repetir el cierre no duplica la liquidación;
- una sesión vencida libera su reserva automáticamente;
- ningún secreto o token aparece en logs del casino.
