# API de Integración v1 > Jumbo · juego de crash Este archivo es la documentación íntegra, en Markdown, generada del mismo texto que la página en https://docs.aircrash.net. El contrato legible por máquina, con cada campo y cada código de error, está en https://docs.aircrash.net/openapi.yaml y fue comprobado contra el servidor que corre. Los importes son enteros, en céntimos. El error de negocio llega como HTTP 200 con `status:"error"`. Si va a leer una sola sección, lea la de las trampas. ## Las dos direcciones | Integración | Sentido | Qué transporta | | --- | --- | --- | | Catálogo y lanzamiento | operador → proveedor | qué juegos existen, y abrir una partida para un jugador | | Billetera | proveedor → operador | resolver la sesión, saldo, débito, crédito, reverso | ## Las ocho que rompen integraciones Todas contradicen una convención común, y por eso están juntas y antes de las rutas: son los puntos en los que implementar *lo que suele ser* produce un cliente que compila, pasa la prueba feliz y se equivoca en producción. | la suposición | el contrato | | --- | --- | | El rechazo llega como 4xx | El error de **negocio** llega como **HTTP 200**, con `status:"error"` y `error_code`. Ramifique en `status`, nunca en el estado HTTP. Un cliente que compruebe `response.ok` lee un rechazo como éxito. | | Un código de error desconocido es final | Lo predeterminado es **reintentar**, hasta 20 veces. Solo siete son finales: `INSUFFICIENT_FUNDS`, `TOKEN_EXPIRED`, `TOKEN_INVALID`, `PLAYER_NOT_FOUND`, `BET_NOT_FOUND`, `CURRENCY_MISMATCH` y `PLAYER_BLOCKED`. Inventar un nombre para rechazar en definitiva produce 20 intentos idénticos contra su cartera. | | Firmar el JSON | Firme **los bytes exactos del cuerpo**. Deserializar y volver a serializar antes del HMAC cambia los espacios y el orden de las claves, y la firma deja de coincidir con un contenido idéntico. | | `request_id` es la clave de idempotencia | La clave es `transaction_id`. El `request_id` nombra el **intento** y cambia en cada uno, junto con el `timestamp`. Deduplicar por `request_id` no deduplica nada; comparar el cuerpo entero rechaza todo reintento. | | `/balance` siempre trae `player_id` | La **primera** llamada de cada sesión va solo con el `token`: es la que descubre al jugador. Exigir `player_id` ahí rechaza la entrada de todos. | | El importe es decimal | Es **entero, en céntimos**. `R$ 3,56` es `356`. El bundle del juego llegó a enviar `114.99999999999999` para una apuesta de R$ 1,15 cuando el campo era decimal. | | Las dos direcciones siguen la misma convención | No la siguen. La regla del 200 vale para la **cartera**; las dos rutas del **juego** responden `4xx`, porque ahí no hay decisión de negocio, solo petición inválida. Los dos conjuntos de `error_code` son disjuntos. | | `freebet_id` solo llega cuando es un premio | El campo está **siempre**, y vale `null` en la apuesta común. Ramifique en `freebet_id !== null`, nunca en la presencia de la clave. | > **Si va a implementar con ayuda de IA** Dele al modelo el [`openapi.yaml`](https://docs.aircrash.net/openapi.yaml) y el [texto íntegro en Markdown](https://docs.aircrash.net/llms-full.es.txt), no esta página: el HTML lleva hoja de estilo y los tres idiomas juntos. Después pídale que compruebe el cliente contra esta tabla, fila por fila. ## Autenticación: un solo esquema, en ambas direcciones ``` X-Signature: hex(HMAC-SHA256(cuerpo_de_la_petición, secreto)) Authorization: Bearer ``` Firme **los bytes exactos del cuerpo**, y compruebe sobre esos mismos bytes. Decodificar el JSON y volver a serializarlo antes del HMAC cambia los espacios y el orden de las claves, y la firma deja de coincidir con un contenido idéntico. Es el error más común, en ambos lados. El `timestamp` va **dentro del cuerpo firmado**, en milisegundos, sin cabecera propia. Fuera de una ventana de **5 minutos** la petición se rechaza, en ambos sentidos: es lo que impide reenviar una petición capturada. Toda petición lleva `operator` y `timestamp`; las que salen de aquí llevan además `request_id`, `currency` y `game_id`. El `request_id` nombra el **intento**, no el movimiento: dos intentos del mismo `transaction_id` llevan ids distintos, y así encuentra uno de ellos en su log. El `Bearer` vale solo en la dirección que sale de aquí. En las dos rutas que exponemos no hay clave nuestra, y la firma autentica sola: por eso el juego **se niega a arrancar** en modo operador sin secreto. Sin secreto configurado, `X-Signature` no se envía y la ventana no se aplica. > **Fallo de autenticación** Operador desconocido, operador deshabilitado, sin secreto, firma incorrecta, petición vieja: todos devuelven el mismo `401 INVALID_SIGNATURE`. Distinguirlos le diría a quien está adivinando cuál intento se acercó más. ## Lo que exponemos (operador → proveedor) Dos rutas, sobre `https://jumbo.aircrash.net`, que es también la base de las URLs de lanzamiento que devuelve `/v1/launch`. Una instalación en otra dirección publica la suya. ### POST /v1/games todos los juegos habilitados para el operador `->` ```json {"operator":"casa-do-norte", "timestamp":1755680000000, "currency":"BRL"} ``` `<-` ```json { "status": "ok", "operator": "casa-do-norte", "currencies": ["BRL", "USD"], "games": [ { "game_id": "jumbo-crash", "name": "Jumbo", "studio": "jumbo", "category": "CRASH", "demo": false, "mobile": true, "desktop": true, "freebet": true, "currency": "BRL", "min_bet": 100, "max_bet": 50000, "max_win": 5000000 } ] } ``` `freebet` indica si el juego acepta rondas gratis concedidas por el operador. #### Los tres límites Aparecen solo cuando usted envía `currency`, valen para esa moneda, y son **céntimos enteros**. | Campo | Significado | | --- | --- | | min_bet | apuesta mínima aceptada | | max_bet | apuesta máxima aceptada | | max_win | techo de pago; un premio mayor se paga en el techo | > **Límite ausente** Un campo de límite ausente significa que el límite es **desconocido**, nunca que no hay límite. Por eso se omite en vez de enviarse en cero: un cero se leería como apuesta mínima de cero, que es justamente lo que el servidor rechaza. `currencies` es el conjunto de monedas habilitadas para usted. De ahí sale en qué monedas puede conceder freebets: una concesión en una moneda que la mesa no acepta es una promesa que nunca se cumple. ### POST /v1/launch abre una partida para un jugador Devuelve la URL para cargar en un iframe o redirigir. `->` ```json { "operator": "casa-do-norte", "timestamp": 1755680000000, "game_id": "jumbo-crash", "player_id": "618004", "token": "e7c1f0a9d3b84e2f95a6", "currency": "BRL", "language": "pt", "demo": false, "mobile": true, "lobby_url": "https://seu-site/lobby", "deposit_url": "https://seu-site/deposito" } ``` | Campo | Obligatorio | Notas | | --- | --- | --- | | game_id | sí | debe ser el juego de esta instalación | | token | salvo en demo | se envía en cada llamada de billetera | | player_id | salvo en demo | su identificador de jugador | | currency | recomendado | la moneda de la sesión | | language | no | pt, en o es | | player_name | no | apodo o nombre; se ofusca al mostrarlo | | theme | no | el tema visual de la mesa | | demo, mobile | no | demo no necesita token ni jugador | | lobby_url, deposit_url | no | guardados en la sesión; la mesa aún no los muestra | `<-` ```json {"status":"ok", "url":"https://jumbo.aircrash.net/?sid=v3Nq8Lm2&lang=pt&cur=BRL"} ``` > **El token no va en la URL** La URL lleva un **ticket** nuestro. Su `token` y su `player_id` no están en ella: los tomamos servidor a servidor y los cambiamos por ese ticket, que es aleatorio, de **un solo uso** y vale **15 minutos**. Es la diferencia entre una credencial al portador en el historial del navegador, en la cabecera Referer y en el log de cualquier proxy del camino, y nada de eso. Errores: `MISSING_GAME_ID`, `MISSING_TOKEN`, `MISSING_PLAYER_ID`, `INVALID_TOKEN` (400), `GAME_NOT_FOUND` (404), `DEMO_NOT_AVAILABLE` (400). ## Monedas e idiomas Todo el dinero es **céntimo entero**. `1500` significa 15,00 en la moneda de la sesión. Un campo monetario en unidades de la moneda, o en coma flotante, es inválido. Las monedas habilitadas para un operador vuelven en `currencies` del `POST /v1/games`. Aquí no hay lista fija a propósito: la lista es la configuración de su instalación, y publicarla en dos sitios garantiza que algún día diverjan. ### Idiomas `pt`, `en` y `es`. Cualquier otro valor, y la ausencia del campo, caen en el idioma por defecto de la instalación y no en `en`. La divergencia con la referencia del mercado es deliberada: caer en un idioma que la mayoría de la base no lee, por un campo olvidado, es peor que caer en el idioma de la casa. ## Lo que expone el operador (proveedor → operador) Cuatro rutas. ``` POST {wallet_url}/balance token → jugador, saldo, moneda POST {wallet_url}/bet débito POST {wallet_url}/win crédito POST {wallet_url}/rollback reverso ``` ### Formato de la respuesta `<-` ```json {"status":"ok", "balance":98500, "currency":"BRL", "player_id":"618004", "transaction_id":"w-77120"} ``` `<-` ```json {"status":"error", "error_code":"INSUFFICIENT_FUNDS", "error_message":"saldo insuficiente"} ``` `balance` es céntimo entero, **después** de la operación, y es obligatorio en el éxito. `currency` es la moneda de la cuenta. Envíela en cada respuesta. `transaction_id` en la respuesta es **su** número para el movimiento, no el nuestro. Existe para la conciliación: es lo que se cita al abrir un ticket sobre un movimiento concreto. ### Campos por ruta `-> /bet` ```json { "operator": "casa-do-norte", "timestamp": 1755680000000, "request_id": "req-9f1464ae0da0664c", "game_id": "jumbo-crash", "currency": "BRL", "token": "e7c1f0a9d3b84e2f95a6", "player_id": "618004", "transaction_id": "b-4471902", "round_id": "88213", "amount": 1500 } ``` | Ruta | Campos | | --- | --- | | /balance | player_id, token | | /bet | player_id, token, amount, transaction_id, round_id | | /win | player_id, token, amount, transaction_id, bet_id, round_id, type | | /rollback | player_id, token, amount, transaction_id, bet_id, round_id | Más `operator`, `timestamp`, `request_id` y `currency` en todas, y `game_id` cuando el juego se conoce. El ciclo de freebet añade `freebet_id` al `/bet` y al `/win`; el reverso añade `reason`, que es diagnóstico y no cambia nada de su lado. > **El `type` del /win** Nombra la naturaleza del crédito sin obligarle a cruzar con el débito. `win` es premio de apuesta pagada con dinero del jugador, `freebet` es premio de ronda costeada por usted, y `zero_win` es el evento terminal de una ronda perdida. Sin la etiqueta, un crédito de importe cero sería indistinguible de un premio que resultó cero. > **El `amount` del /rollback** Va incluido, y es **siempre** el importe del movimiento referenciado en `bet_id`, nunca un número elegido en el momento. Compruebe que coincide con el débito que tiene: un reverso que pueda devolver más de lo que se tomó es una puerta abierta, y esa comprobación de su lado es lo que la cierra. ### Códigos de error | Código | Significado | ¿Reintentamos? | | --- | --- | --- | | INSUFFICIENT_FUNDS | saldo insuficiente | no | | TOKEN_EXPIRED | sesión expirada | no | | TOKEN_INVALID | sesión que nunca existió | no | | PLAYER_NOT_FOUND | jugador desconocido | no | | BET_NOT_FOUND | la apuesta referenciada no existe | no | | CURRENCY_MISMATCH | moneda incorrecta para esta cuenta | no | | PLAYER_BLOCKED | jugador impedido de apostar | no | | INTERNAL_ERROR | fallo de su lado | sí |