API de Casino · v1
Te damos la bienvenida a la API de Casino de Provibet
Una sola integración servidor a servidor para sumar a tu plataforma un catálogo de más de 3.400 juegos de 27 estudios, con jugadores propios, fichas prepagas y apertura de juegos embebible en tu sitio.
Cómo funciona
Provibet funciona con un modelo prepago de transferencia. Provibet asigna fichas (CRD) a la billetera de tu cuenta; tu backend las entrega a cada jugador antes de jugar y las recupera cuando termina. Mientras juega, el saldo del jugador cambia dentro de la plataforma de juego: no hay notificaciones por apuesta ni por premio.
- Consultás tu billetera prepaga para saber cuántas fichas tenés disponibles.
- Registrás al jugador con un identificador estable de tu sistema.
- Le entregás fichas con una clave idempotente propia de esa orden.
- Elegís un juego del catálogo y pedís su apertura.
- Al terminar, descontás el saldo del jugador para devolverlo a tu billetera.
- Cada movimiento de fichas tiene una operación con estado consultable.
Endpoints de la API v1
Todas las rutas cuelgan de la base https://api.provibet.com/api/b2b/v1 y requieren firma HMAC.
- GET
/walletBilletera prepaga - POST
/playersCrear jugador - GET
/players/{externalPlayerId}/balanceSaldo del jugador - POST
/players/{externalPlayerId}/creditsEntregar fichas - POST
/players/{externalPlayerId}/debitsDescontar fichas - GET
/gamesCatálogo - GET
/studiosEstudios y categorías - POST
/games/{gameId}/launchAbrir un juego - GET
/operations/{operationId}Consultar operación
Además, las portadas del catálogo se sirven públicamente desde https://api.provibet.com/media/{mediaId} y la página de apertura vive en https://api.provibet.com/play/. Ninguna de las dos requiere firma.
Convenciones
- Transporte: HTTPS y JSON en UTF-8. Los bodies no pueden superar 64 KiB.
- Sólo backend: la API se llama desde tu servidor. El secreto HMAC nunca viaja al navegador ni a una app.
- Importes: strings decimales con hasta dos posiciones (
"50","50.25"); las respuestas siempre usan dos ("50.00"). Ver Moneda. - Fechas: ISO 8601 en UTC, por ejemplo
2026-09-24T12:00:00.000Z. - Identificadores: los IDs de juegos, medios y operaciones son opacos. Usalos tal como los recibís; no los construyas.
- Campos: los bodies aceptan sólo los campos documentados (un campo extra responde
400 invalid_request). Tu lector de respuestas debe tolerar campos nuevos. - Correlación: cada respuesta incluye el encabezado
X-Request-Id. Guardalo junto con tu referencia.
Qué no incluye
- No hay callbacks ni webhooks por apuesta o premio: el saldo se consulta con
GET /players/{id}/balance. - No procesa pagos en dinero ni retiros bancarios: mueve fichas entre tu billetera y tus jugadores.
- No incluye bonos ni promociones.
Soporte e incidentes
Usá el canal que acordaste con Provibet al habilitar tu cuenta. Para que podamos correlacionar el caso, incluí la hora UTC, el endpoint, el código HTTP, error.code, el X-Request-Id de la respuesta y el operation.id si existe.
Nunca compartas tu secreto HMAC, encabezados firmados completos, URLs de apertura de juegos ni datos personales innecesarios.
Primeros pasos
Inicio rápido
Este recorrido te lleva de las credenciales al primer juego abierto. Cada paso muestra la llamada en cURL, Node.js y PHP usando los helpers descargables.
Antes de empezar
Necesitás un backend con salida por IP fija, un gestor de secretos y el reloj sincronizado por NTP. Provibet da de alta tu integración y te entrega:
- Clave pública (
publicKey): empieza conb2b_. Va en el encabezadoX-B2B-Key. - Secreto HMAC (
secret): se muestra una sola vez al crearlo. Guardalo en tu gestor de secretos. - Permisos (scopes) y la lista de IP o redes autorizadas desde las que vas a llamar.
Paso a paso
-
Configurá el entorno
Descargá node.mjs, request.php o sign-for-curl.mjs y definí estas variables en tu gestor de secretos:
Variables de entornoPROVIBET_PUBLIC_KEY=b2b_TU_CLAVE_PUBLICA PROVIBET_SECRET=TU_SECRETO # nunca en el repositorio ni en logs PROVIBET_ORIGIN=https://api.provibet.comPara cURL, esta función firma cada solicitud con
sign-for-curl.mjssin pasar el secreto como argumento. La usamos en todos los ejemplos:bash · función provibet# Requiere Node.js 20+, cURL, sign-for-curl.mjs y node.mjs en la carpeta actual. provibet() { # uso: provibet METODO RUTA [BODY_JSON] [IDEMPOTENCY_KEY] local work rc url="https://api.provibet.com$2" work="$(mktemp -d)" || return 1 printf '%s' "${3:-}" > "$work/body" node sign-for-curl.mjs "$1" "$url" "$work/body" "$work/headers" ${4:+"$4"} && curl -sS -X "$1" "$url" --header "@$work/headers" \ ${3:+--data-binary "@$work/body"} -w '\nHTTP %{http_code}\n' rc=$?; rm -rf "$work"; return $rc } -
Consultá tu billetera
Es una lectura sin efectos: ideal para validar clave, secreto, reloj e IP.
cURLprovibet GET /api/b2b/v1/walletNode.jsimport { provibetRequest } from './node.mjs'; const { status, requestId, data } = await provibetRequest('GET', '/api/b2b/v1/wallet'); if (status !== 200) throw new Error(`Consulta fallida: ${status} · ${requestId}`); console.log(data.wallet.availableCredits); // "9500.00"PHPrequire __DIR__ . '/request.php'; $r = provibetRequest('GET', '/api/b2b/v1/wallet'); if ($r['status'] !== 200) throw new RuntimeException('Consulta fallida: ' . $r['status']); echo $r['data']['wallet']['availableCredits']; // "9500.00" -
Registrá un jugador
Usá un
externalPlayerIdestable de tu sistema. Si ya existe, la respuesta es la misma.cURLprovibet POST /api/b2b/v1/players '{"externalPlayerId":"jugador-123"}' player-jugador-123-0001Node.jsconst body = JSON.stringify({ externalPlayerId: 'jugador-123' }); const { status } = await provibetRequest('POST', '/api/b2b/v1/players', body, 'player-jugador-123-0001'); // 201 → { "player": { "externalPlayerId": "jugador-123" } }PHP$body = json_encode(['externalPlayerId' => 'jugador-123'], JSON_THROW_ON_ERROR); $r = provibetRequest('POST', '/api/b2b/v1/players', $body, 'player-jugador-123-0001'); // 201 → { "player": { "externalPlayerId": "jugador-123" } } -
Entregale fichas
Generá la clave idempotente de la orden y guardala antes de enviar. Si la conexión se corta, reintentás con la misma clave y el mismo importe.
cURLprovibet POST /api/b2b/v1/players/jugador-123/credits '{"amountCredits":"50","currency":"CRD"}' credit-order-5001Node.jsconst body = JSON.stringify({ amountCredits: '50', currency: 'CRD' }); // Persistí body y clave en tu base ANTES de enviar. const { status, data } = await provibetRequest('POST', '/api/b2b/v1/players/jugador-123/credits', body, 'credit-order-5001'); // 200 COMPLETED · 202 pendiente (consultá la operación) · 422 REJECTEDPHP$body = json_encode(['amountCredits' => '50', 'currency' => 'CRD'], JSON_THROW_ON_ERROR); // Persistí $body y la clave en tu base ANTES de enviar. $r = provibetRequest('POST', '/api/b2b/v1/players/jugador-123/credits', $body, 'credit-order-5001'); // 200 COMPLETED · 202 pendiente (consultá la operación) · 422 REJECTED -
Elegí un juego
cURLprovibet GET '/api/b2b/v1/games?category=slots&limit=20'Node.jsconst { data } = await provibetRequest('GET', '/api/b2b/v1/games?category=slots&limit=20'); const gameId = data.games[0].id; // ID opaco: guardalo tal cualPHP$r = provibetRequest('GET', '/api/b2b/v1/games?category=slots&limit=20'); $gameId = $r['data']['games'][0]['id']; // ID opaco: guardalo tal cual -
Abrí el juego
La respuesta trae una URL temporal de un solo uso. Entregala únicamente al jugador correspondiente; podés abrirla en una pestaña o dentro de un iframe de tu sitio.
cURLprovibet POST "/api/b2b/v1/games/$GAME_ID/launch" '{"externalPlayerId":"jugador-123","locale":"es-AR"}' launch-7f3a9c21Node.jsconst body = JSON.stringify({ externalPlayerId: 'jugador-123', locale: 'es-AR' }); const { data } = await provibetRequest('POST', `/api/b2b/v1/games/${gameId}/launch`, body, `launch-${crypto.randomUUID()}`); // data.launch.url → entregala al navegador del jugador antes de data.launch.expiresAtPHP$body = json_encode(['externalPlayerId' => 'jugador-123', 'locale' => 'es-AR'], JSON_THROW_ON_ERROR); $r = provibetRequest('POST', "/api/b2b/v1/games/{$gameId}/launch", $body, 'launch-' . bin2hex(random_bytes(8))); // $r['data']['launch']['url'] → entregala al navegador del jugador -
Recuperá el saldo al terminar
Cuando el jugador sale, consultá su saldo y descontalo hacia tu billetera con otra clave idempotente. Ver Descontar fichas.
Checklist antes de habilitar tráfico
- Clave, secreto, IP de salida y permisos confirmados con Provibet.
- Secreto sólo en backend: fuera del frontend, del repositorio y de los logs.
- Reloj sincronizado, nonce nuevo por intento y cadena canónica de nueve líneas verificada.
- Claves idempotentes de créditos y débitos persistidas antes del envío y reutilizadas tras un timeout.
- Importes como strings decimales, sin pasar por números de punto flotante.
- Manejo de 202, 409, 422, 429 y fallas de transporte probado.
- Operaciones
UNKNOWNseguidas porGET /operations/{id}, sin liberar ni repetir por tiempo. operation.id, referencia propia yX-Request-Idregistrados y correlacionados.- Apertura probada en móvil y escritorio; dominio del iframe registrado por Provibet.
Primeros pasos
Panel de gestión
El back office de Provibet, en admin.provibet.com, es donde se administra cada cuenta de integración: credenciales, permisos, fichas asignadas y seguimiento de operaciones.
Acceso administrado por Provibet. Hoy el panel lo opera el equipo de Provibet, con usuario, contraseña y código de verificación de dos pasos. Tu equipo consulta su saldo y sus operaciones por API; cualquier cambio de configuración se pide por el canal acordado.
Integraciones y credenciales
- Alta de integración: nombre del cliente, IP o redes autorizadas (notación CIDR, por ejemplo
203.0.113.10/32) y permisos. - Entrega de credenciales: al crear la integración se generan la clave pública y el secreto HMAC. El secreto se muestra una única vez y se borra de la vista al cerrarla.
- Revocación: una clave revocada deja de autenticar de inmediato (
401 invalid_client_credentials). Para reemplazar credenciales, coordiná el cambio con Provibet.
Asignación de fichas
Las fichas de tu billetera prepaga se cargan desde el panel. Cada asignación registra la cantidad en CRD, una referencia única y el motivo o constancia. Repetir una asignación con la misma referencia no duplica fichas.
Operaciones y conciliación
- Listado de operaciones de crédito y débito, filtrable por estado.
- Conciliación de operaciones
UNKNOWN: consulta el resultado real y, si se confirma, la operación pasa aCOMPLETED. La conciliación nunca reenvía una transferencia. - Revocación de sesiones de juego abiertas desde una URL de apertura.
Disponibilidad del servicio
Provibet puede pausar la creación de jugadores, los movimientos de fichas o la apertura de juegos. Mientras una función está pausada, la API responde 503 con un código que lo indica (por ejemplo financial_operations_disabled). El catálogo se actualiza periódicamente desde el panel.
Primeros pasos
Autenticación y firma
Cada solicitud a /api/b2b/v1, incluidas las consultas GET, se firma con HMAC-SHA256. El secreto nunca se transmite: sólo se usa para calcular la firma de la solicitud exacta.
Encabezados
| Encabezado | Regla |
|---|---|
X-B2B-Key obligatorio | Tu clave pública: b2b_ seguido de 43 caracteres A-Z a-z 0-9 _ -. |
X-B2B-Timestamp obligatorio | Hora Unix UTC en segundos, exactamente 10 dígitos. Debe estar dentro de ±300 segundos de la hora del servidor. |
X-B2B-Nonce obligatorio | Valor aleatorio de 16 a 128 caracteres A-Z a-z 0-9 _ -. De un solo uso por credencial: generá uno nuevo en cada intento, también en los reintentos. |
X-B2B-Signature obligatorio | HMAC-SHA256 en hexadecimal minúscula (64 caracteres). Se acepta el prefijo sha256=. |
Idempotency-Key en todo POST | 8 a 128 caracteres: empieza con letra o dígito y sigue con A-Z a-z 0-9 . _ : -. Forma parte de la firma. Ver Idempotencia. |
Content-Type en todo POST | application/json. Con otro tipo el body no se lee y la firma no coincide. |
X-Request-Id opcional | Para tus propios registros. La API no lo reutiliza: cada respuesta trae su propio X-Request-Id (UUID). |
Cada encabezado X-B2B-* e Idempotency-Key se envía una sola vez; si se repite, la respuesta es 400 duplicate_authentication_header.
Cadena canónica
La firma se calcula sobre nueve líneas unidas por salto de línea \n (LF), sin salto final:
B2B-HMAC-SHA256
<publicKey>
<timestamp>
<nonce>
<MÉTODO en mayúsculas>
<path exacto enviado, incluido /api/b2b/v1>
<query canonicalizado, o vacío>
<Idempotency-Key, o vacío>
<SHA-256 hexadecimal del body exacto>La firma es hex(HMAC-SHA256(secret, cadenaCanónica)).
Reglas de cada línea
- Path: la parte de la URL anterior a
?, exactamente como la enviás, con el prefijo/api/b2b/v1y los escapes%XXque transmitas. No puede contener espacios, caracteres de control ni#. - Query: decodificalo como
application/x-www-form-urlencoded(igual queURLSearchParams:+equivale a espacio), ordená los pares por nombre y, a igualdad, por valor (comparación de unidades UTF-16), volvé a codificar cada componente según RFC 3986 y unilos con&. Los parámetros repetidos se conservan. - Líneas vacías: sin query o sin clave idempotente, la línea queda vacía. Nunca elimines una línea.
- Body: SHA-256 en hexadecimal minúscula de los bytes exactos enviados. Un GET sin body usa el hash de cero bytes:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. - JSON: serializalo una sola vez y enviá esos mismos bytes. Cambiar espacios u orden después de firmar invalida la firma.
Ejemplo
Para GET /api/b2b/v1/games?limit=20&category=slots el query se reordena alfabéticamente:
B2B-HMAC-SHA256
b2b_CLAVE_PUBLICA_DE_EJEMPLO_000000000000000000
1790251200
nonce-unico-de-ejemplo-0001
GET
/api/b2b/v1/games
category=slots&limit=20
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Calcular la firma
import { createHash, createHmac, randomBytes } from 'node:crypto';
const rfc3986 = v => encodeURIComponent(v).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
const canonicalQuery = params => [...params.entries()]
.sort(([ak, av], [bk, bv]) => ak !== bk ? (ak < bk ? -1 : 1) : av === bv ? 0 : av < bv ? -1 : 1)
.map(([k, v]) => `${rfc3986(k)}=${rfc3986(v)}`).join('&');
export function sign({ method, url, body = '', idempotencyKey = '', key, secret }) {
const u = new URL(url);
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(24).toString('base64url');
const canonical = ['B2B-HMAC-SHA256', key, timestamp, nonce, method.toUpperCase(), u.pathname,
canonicalQuery(u.searchParams), idempotencyKey, createHash('sha256').update(body).digest('hex')].join('\n');
return {
'X-B2B-Key': key, 'X-B2B-Timestamp': timestamp, 'X-B2B-Nonce': nonce,
'X-B2B-Signature': createHmac('sha256', secret).update(canonical).digest('hex'),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey, 'Content-Type': 'application/json' } : {}),
};
}// Canonicalización del query: ver provibetCanonicalQuery() en request.php.
function provibetSign(string $method, string $target, string $body, string $idemKey, string $key, string $secret): array {
[$path, $query] = array_pad(explode('?', $target, 2), 2, '');
$timestamp = (string) time();
$nonce = rtrim(strtr(base64_encode(random_bytes(24)), '+/', '-_'), '=');
$canonical = implode("\n", ['B2B-HMAC-SHA256', $key, $timestamp, $nonce, strtoupper($method), $path,
provibetCanonicalQuery($query), $idemKey, hash('sha256', $body)]);
return ['X-B2B-Key: ' . $key, 'X-B2B-Timestamp: ' . $timestamp, 'X-B2B-Nonce: ' . $nonce,
'X-B2B-Signature: ' . hash_hmac('sha256', $canonical, $secret)];
}# sign-for-curl.mjs escribe un archivo de encabezados nuevo (modo 0600) por intento.
# El secreto se lee de PROVIBET_SECRET: nunca aparece en la línea de comandos.
umask 077
work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT
: > "$work/body"
url='https://api.provibet.com/api/b2b/v1/wallet'
node sign-for-curl.mjs GET "$url" "$work/body" "$work/headers"
curl --fail-with-body -sS "$url" --header "@$work/headers"Los helpers completos están en Ejemplos.
Orden de validación
El servidor valida en este orden; el primer fallo determina la respuesta:
| # | Verificación | Si falla |
|---|---|---|
| 1 | Encabezados de autenticación no repetidos | 400 duplicate_authentication_header |
| 2 | Formato de clave, timestamp, nonce y firma | 401 invalid_authentication_headers |
| 3 | Timestamp dentro de ±300 s | 401 request_timestamp_outside_window |
| 4 | Límite de 120 solicitudes por minuto por clave | 429 rate_limit_exceeded |
| 5 | Clave activa | 401 invalid_client_credentials |
| 6 | IP de origen autorizada | 403 source_ip_not_allowed |
| 7 | Idempotency-Key válida en POST | 400 idempotency_key_required |
| 8 | Firma | 401 invalid_request_signature |
| 9 | Nonce no usado | 409 nonce_replayed_or_invalid |
| 10 | Permiso del endpoint | 403 insufficient_scope |
Permisos (scopes)
Pedí sólo los permisos que usa tu integración.
| Scope | Endpoints |
|---|---|
wallet:read | GET /wallet |
players:write | POST /players |
players:read | GET /players/{id}/balance |
credits:write | POST /players/{id}/credits y POST /players/{id}/debits |
games:read | GET /games y GET /studios |
games:launch | POST /games/{id}/launch |
operations:read | GET /operations/{id} |
IP de salida
Cada integración tiene una lista de IP o redes autorizadas. Llamá desde direcciones fijas y avisá a Provibet antes de cambiarlas: una IP no registrada recibe 403 source_ip_not_allowed.
Si la firma no coincide
- Confirmá que usás la clave y el secreto activos, y segundos Unix (no milisegundos).
- Compará tu cadena canónica línea por línea: nueve líneas, path con prefijo
/api/b2b/v1, query ordenado, clave idempotente exacta y hash de los bytes enviados. - Verificá que ningún proxy o cliente HTTP de tu lado reescriba la ruta, el query o el body después de firmar.
- Generá timestamp, nonce y firma nuevos antes de volver a enviar.
Nunca registres el secreto ni la cadena canónica junto con el secreto. Para depurar, loguéa la cadena canónica sin el secreto y el X-Request-Id de la respuesta.
Primeros pasos
Idempotencia
Un intento puede repetirse; una entrega de fichas, no. La Idempotency-Key identifica la intención de negocio y el nonce identifica cada intento de transporte.
La regla
Idempotency-Keyes obligatoria en todo POST y forma parte de la firma. Sin una clave válida la respuesta es400 idempotency_key_required.- El nonce se renueva en cada intento. La clave idempotente se conserva mientras la intención sea la misma.
- Generá la clave a partir de tu orden (por ejemplo
credit-order-5001) y guardala antes de enviar.
Comportamiento por endpoint
| Endpoint | Qué hace la clave |
|---|---|
POST /players/{id}/creditsPOST /players/{id}/debits | Deduplica. La misma clave con la misma intención devuelve la misma operación con "replayed": true y su estado actual (200, 202 o 422), sin volver a mover fichas. La misma clave con otra intención responde 409 idempotency_conflict. |
POST /players | Sólo se exige y se firma. El alta ya es idempotente por externalPlayerId: repetirla devuelve el mismo jugador. |
POST /games/{id}/launch | Sólo se exige y se firma. Cada llamada emite una URL de apertura nueva; usá una clave distinta por solicitud. |
Qué se compara en créditos y débitos
La intención se identifica por tipo (crédito o débito), jugador e importe. "50" y "50.00" son el mismo importe, y omitir currency equivale a enviar "CRD". Aun así, conservá y reenviá los mismos bytes: simplifica la auditoría.
- Las claves son únicas por cuenta: una clave usada en un crédito no sirve para un débito, ni para otro jugador u otro importe.
- Las claves no vencen: quedan asociadas a su operación.
- Si la solicitud se rechazó antes de crear la operación (por ejemplo
409 insufficient_creditso un error de firma), la clave no queda consumida y podés reintentar la misma intención. - Un reintento con una clave ya registrada responde aunque los movimientos nuevos estén pausados o el jugador ya no esté activo.
Cuándo y cómo reintentar
| Situación | Acción correcta |
|---|---|
| Timeout, conexión cortada o respuesta no JSON | El resultado es incierto. Reintentá con la misma clave e importe y timestamp, nonce y firma nuevos. |
202 con operation | Guardá operation.id y consultá GET /operations/{id} con backoff. No crees otra intención. |
409 idempotency_conflict | Compará con la intención guardada. No cambies la clave para forzar el movimiento. |
409 nonce_replayed_or_invalid | Volvé a firmar con un nonce nuevo; conservá la clave. |
422 con state: REJECTED | Rechazo definitivo. Una nueva intención necesita una decisión de negocio y otra clave. |
429 o 503 | Esperá con backoff exponencial y jitter; reintentá con la misma clave. |
Nunca reintentes con una clave nueva una entrega cuyo resultado no conocés. Si lo hacés, podés entregar las fichas dos veces. Consultá la operación o repetí la solicitud con la clave original.
Backoff sugerido
Para consultar una operación pendiente: 1, 2, 5 y 10 segundos; después cada 30 segundos con jitter, hasta el límite de tiempo de tu aplicación. Llegar a ese límite no cambia el estado financiero: la orden sigue pendiente.
Primeros pasos
Entornos y URLs
Direcciones públicas de Provibet y requisitos de transporte.
URLs
| Uso | URL | Autenticación |
|---|---|---|
| API v1 (base) | https://api.provibet.com/api/b2b/v1 | Firma HMAC |
| Página de apertura | https://api.provibet.com/play/ | Ticket de un solo uso en la URL |
| Portadas del catálogo | https://api.provibet.com/media/{mediaId} | Pública |
| Panel de gestión | https://admin.provibet.com/ | Equipo de Provibet |
| Documentación | https://provibet.com/docs/ | Pública |
| Contrato OpenAPI | https://provibet.com/docs/openapi.json | Pública |
Pruebas y producción
No existe un entorno de pruebas público ni un alta automática. Provibet confirma el entorno, las credenciales, los límites y las condiciones de tu cuenta antes de que envíes tráfico con fichas. Separá credenciales, registros y referencias de prueba de los productivos.
Todos los IDs, claves, importes y URLs de esta documentación son ilustrativos. No sirven para autenticar ni para abrir juegos.
Transporte
- Sólo HTTPS, contra el host exacto
api.provibet.com. Otro host responde421 unexpected_host. - Bodies JSON de hasta 64 KiB; más grandes responden
413 request_too_large. - Reloj sincronizado por NTP: la firma admite un desvío máximo de 300 segundos.
- Las respuestas de la API llevan
Cache-Control: no-store. Las portadas se pueden cachear (public, max-age=86400, immutable). - No sigas redirecciones automáticamente en llamadas firmadas.
Integración
Billetera prepaga
Tu cuenta tiene una billetera de fichas CRD. De ahí salen las fichas que entregás a tus jugadores y ahí vuelven las que descontás.
Consultar la billetera
/api/b2b/v1/walletprovibet GET /api/b2b/v1/walletconst { status, data } = await provibetRequest('GET', '/api/b2b/v1/wallet');$r = provibetRequest('GET', '/api/b2b/v1/wallet');{
"wallet": {
"currency": "CRD",
"availableCredits": "9500.00",
"reservedCredits": "500.00",
"version": "14"
}
}| Campo | Descripción |
|---|---|
currencystring | Siempre CRD. |
availableCreditsstring decimal | Fichas disponibles para nuevas entregas. |
reservedCreditsstring decimal | Fichas apartadas por entregas pendientes o inciertas. No están disponibles. |
versionstring entero | Aumenta con cada movimiento de la billetera. Sirve para detectar cambios. |
Cómo se mueve el saldo
| Evento | Disponibles | Reservadas |
|---|---|---|
| Asignación de fichas desde el panel | + importe | — |
Entrega a un jugador: se crea la operación (RESERVED) | − importe | + importe |
Entrega COMPLETED | — | − importe (sale hacia el jugador) |
Entrega REJECTED | + importe (se libera) | − importe |
Entrega UNKNOWN | — | Sigue reservada hasta la conciliación |
Descuento COMPLETED | + importe (vuelve del jugador) | — |
Descuento RESERVED, UNKNOWN o REJECTED | Sin cambios | Sin cambios |
Saldo insuficiente
Si availableCredits no alcanza para una entrega, la respuesta es 409 insufficient_credits y no se crea ninguna operación. La clave idempotente no queda consumida: cuando Provibet cargue fichas, podés reintentar la misma intención con la misma clave.
No hay un endpoint de compra de fichas: las cargas las registra Provibet en el panel, con una referencia única por asignación.
Integración
Jugadores
Cada jugador se identifica con un externalPlayerId de tu sistema. Provibet no entrega usuarios ni contraseñas de jugadores.
Crear o recuperar un jugador
/api/b2b/v1/players| Campo | Regla |
|---|---|
externalPlayerId obligatoriostring | 3 a 128 caracteres; empieza con letra o dígito y admite A-Z a-z 0-9 . _ : @ -. Tiene que ser estable: es la identidad del jugador en tu cuenta. |
displayName opcionalstring | Hasta 100 caracteres. Se acepta pero hoy no se guarda ni se devuelve. |
provibet POST /api/b2b/v1/players '{"externalPlayerId":"jugador-123"}' player-jugador-123-0001const body = JSON.stringify({ externalPlayerId: 'jugador-123' });
const { status, data } = await provibetRequest('POST', '/api/b2b/v1/players', body, 'player-jugador-123-0001');$body = json_encode(['externalPlayerId' => 'jugador-123'], JSON_THROW_ON_ERROR);
$r = provibetRequest('POST', '/api/b2b/v1/players', $body, 'player-jugador-123-0001');{
"player": {
"externalPlayerId": "jugador-123"
}
}La respuesta es 201 tanto para un jugador nuevo como para uno ya registrado. No cambies el externalPlayerId al repetir la solicitud.
| Respuesta | Significado |
|---|---|
400 invalid_request | ID con formato inválido o campos no documentados. |
409 player_initialization_pending | El alta de ese jugador está en curso. Reintentá en unos segundos. |
409 player_not_ready · 503 player_initialization_review | El alta necesita revisión de Provibet. Contactá a soporte con el X-Request-Id. |
503 registration_unavailable | El registro de jugadores está pausado. |
Consultar el saldo
/api/b2b/v1/players/{externalPlayerId}/balanceprovibet GET /api/b2b/v1/players/jugador-123/balanceconst id = encodeURIComponent('jugador-123');
const { data } = await provibetRequest('GET', `/api/b2b/v1/players/${id}/balance`);$id = rawurlencode('jugador-123');
$r = provibetRequest('GET', "/api/b2b/v1/players/{$id}/balance");{
"player": {
"externalPlayerId": "jugador-123",
"balanceCredits": "250.00",
"currency": "CRD"
}
}- El saldo se lee en el momento: incluye las partidas jugadas y puede cambiar mientras el jugador está en un juego.
- Codificá el ID como segmento de URL y firmá ese mismo path codificado.
- Un jugador inexistente, inactivo o de otra cuenta responde
404 player_not_found.
Integración
Fichas: créditos y débitos
Las fichas pasan de tu billetera al jugador con un crédito y vuelven con un débito. Cada movimiento es una operación idempotente con un estado explícito.
Modelo de transferencia
Provibet trabaja en modo transferencia: movés fichas antes y después de jugar, y durante la partida el saldo del jugador cambia dentro de la plataforma de juego. No hay callbacks por apuesta ni por premio.
Estados de una operación
| Estado | HTTP | Qué significa | Qué hacés |
|---|---|---|---|
COMPLETED | 200 | Movimiento confirmado. | Cerrá la orden como exitosa. Terminal. |
RESERVED | 202 | Intención registrada, procesamiento en curso (por ejemplo, un reintento mientras el original sigue en proceso). | Consultá la operación con backoff. |
UNKNOWN | 202 | No hay certeza del resultado. En un crédito, las fichas siguen reservadas. | Mantené la orden pendiente y consultá la operación. No acredites ni liberes por tu cuenta. |
REJECTED | 422 | Rechazo definitivo; no hubo movimiento (en un crédito, la reserva se liberó). | Registrá el rechazo. Terminal. |
Las respuestas 200, 202 y 422 traen el mismo formato: {"operation": {…}, "replayed": false}. El 422 no usa el formato de error: leé operation.state.
Entregar fichas (crédito)
/api/b2b/v1/players/{externalPlayerId}/creditsReserva el importe de tu billetera y lo transfiere al jugador, que tiene que estar registrado y activo.
| Campo | Regla |
|---|---|
amountCredits obligatoriostring | Decimal mayor que cero con hasta dos posiciones; máximo 10000000000.00. Ver Moneda. |
currency opcionalstring | Si se envía, tiene que ser CRD. |
provibet POST /api/b2b/v1/players/jugador-123/credits '{"amountCredits":"50","currency":"CRD"}' credit-order-5001// 1) Persistí la intención: { orderId, playerId, body, key } ANTES de enviar.
const body = JSON.stringify({ amountCredits: '50', currency: 'CRD' });
const key = 'credit-order-5001';
// 2) Enviá. Ante un timeout, repetí con el MISMO body y la MISMA clave.
const { status, data } = await provibetRequest('POST', '/api/b2b/v1/players/jugador-123/credits', body, key);
// 3) Guardá data.operation.id y decidí según data.operation.state.// 1) Persistí la intención ($orderId, $playerId, $body, $key) ANTES de enviar.
$body = json_encode(['amountCredits' => '50', 'currency' => 'CRD'], JSON_THROW_ON_ERROR);
$key = 'credit-order-5001';
// 2) Enviá. Ante un timeout, repetí con el MISMO body y la MISMA clave.
$r = provibetRequest('POST', '/api/b2b/v1/players/jugador-123/credits', $body, $key);
// 3) Guardá $r['data']['operation']['id'] y decidí según su 'state'.{
"operation": {
"id": "00000000-0000-4000-8000-000000000001",
"kind": "CREDIT",
"state": "COMPLETED",
"amountCredits": "50.00",
"currency": "CRD",
"createdAt": "2026-09-24T12:00:00.000Z",
"updatedAt": "2026-09-24T12:00:01.000Z"
},
"replayed": false
}| HTTP | Interpretación |
|---|---|
| 200 | COMPLETED. Si es un reintento de la misma intención, replayed es true. |
| 202 | RESERVED o UNKNOWN. Las fichas siguen reservadas. Consultá la operación; no abras otra intención. |
| 422 | REJECTED: la reserva se liberó. |
| 409 | insufficient_credits (sin operación creada), idempotency_conflict o nonce_replayed_or_invalid. |
| 404 | player_not_found: jugador inexistente o inactivo. |
| 503 | financial_operations_disabled o service_temporarily_unavailable: no se creó la operación. |
Si la transferencia al jugador falla después de reservar, la operación queda UNKNOWN y la reserva se mantiene hasta que la conciliación confirme el resultado. Un corte de conexión no equivale a un rechazo.
Descontar fichas (débito)
/api/b2b/v1/players/{externalPlayerId}/debitsDevuelve a tu billetera fichas del saldo del jugador, por ejemplo cuando cierra su sesión o retira. El body, los límites y las reglas de reintento son los del crédito; operation.kind vale DEBIT.
provibet POST /api/b2b/v1/players/jugador-123/debits '{"amountCredits":"25.50","currency":"CRD"}' debit-order-7001const body = JSON.stringify({ amountCredits: '25.50', currency: 'CRD' });
const { status, data } = await provibetRequest('POST', '/api/b2b/v1/players/jugador-123/debits', body, 'debit-order-7001');$body = json_encode(['amountCredits' => '25.50', 'currency' => 'CRD'], JSON_THROW_ON_ERROR);
$r = provibetRequest('POST', '/api/b2b/v1/players/jugador-123/debits', $body, 'debit-order-7001');{
"operation": {
"id": "00000000-0000-4000-8000-000000000003",
"kind": "DEBIT",
"state": "REJECTED",
"amountCredits": "25.50",
"currency": "CRD",
"createdAt": "2026-09-24T12:00:00.000Z",
"updatedAt": "2026-09-24T12:00:00.400Z"
},
"replayed": false
}| HTTP | Interpretación |
|---|---|
| 200 | COMPLETED: las fichas ya suman en availableCredits. |
| 202 | RESERVED o UNKNOWN: tu billetera todavía no cambió. Consultá la operación; no abras otra intención. |
| 422 | REJECTED: el saldo del jugador no alcanzaba o el descuento no llegó a enviarse. La billetera no cambió. |
| 409 | idempotency_conflict (por ejemplo, una clave ya usada en un crédito) o nonce_replayed_or_invalid. |
Sin reserva previa. El débito no aparta fichas: se acreditan en tu billetera recién al confirmarse. Si el jugador está en una partida su saldo puede cambiar; descontá cuando la partida terminó, por ejemplo al recibir el aviso de salida.
Resultado incierto: qué hacer
- Mantené la orden como pendiente en tu sistema. No acredites, no devuelvas y no muestres el movimiento como fallido.
- Si no recibiste respuesta, repetí la solicitud con la misma clave: vas a obtener la operación original.
- Con el
operation.id, consultáGET /operations/{id}con backoff. - Si sigue en
UNKNOWN, contactá a Provibet con eloperation.id: la conciliación consulta el resultado real y nunca reenvía la transferencia.
Nunca reintentes con una clave nueva ni compenses con otra operación: podés duplicar el movimiento. Consultá /operations/{id} y esperá un estado terminal.
Integración
Catálogo de juegos
El catálogo habilitado para tu cuenta, paginado y filtrable por texto, categoría, estudio y portada animada.
Listar juegos
/api/b2b/v1/games| Parámetro | Descripción |
|---|---|
limit opcionalentero | Tamaño de página, de 1 a 200. Por defecto 50. |
offset opcionalentero | Desplazamiento, de 0 a 1.000.000. Por defecto 0. |
q opcionalstring | Busca en el título (contiene, sin distinguir mayúsculas). Se consideran los primeros 100 caracteres. |
category opcionalstring | slots, live, crash, table o sports. Ver Categorías. |
studio opcionalstring | Nombre público del estudio (Pragmatic Play) o su clave (pragmaticplay). Ver Estudios y categorías. |
cover opcionalstring | animated: sólo juegos con portada animada oficial, ideal para vidrieras destacadas. Las animaciones generadas por Provibet no cuentan para este filtro. |
provibet GET '/api/b2b/v1/games?studio=pragmaticplay&category=slots&limit=100&offset=0'const params = new URLSearchParams({ studio: 'pragmaticplay', category: 'slots', limit: '100', offset: '0' });
const { data } = await provibetRequest('GET', `/api/b2b/v1/games?${params}`);$query = http_build_query(['studio' => 'pragmaticplay', 'category' => 'slots', 'limit' => 100, 'offset' => 0], '', '&', PHP_QUERY_RFC3986);
$r = provibetRequest('GET', '/api/b2b/v1/games?' . $query);{
"revision": "2026-09-24T09:05:12.000Z",
"total": 612,
"count": 1,
"limit": 1,
"offset": 0,
"hasMore": true,
"games": [
{
"id": "game_0123456789abcdef0123456789abcdef01234567",
"title": "Sweet Bonanza",
"category": "slots",
"studio": "Pragmatic Play",
"studioId": "pragmaticplay",
"coverStaticUrl": "https://api.provibet.com/media/ID_DE_MEDIO_ESTATICO",
"coverAnimatedUrl": "https://api.provibet.com/media/ID_DE_MEDIO_ANIMADO",
"coverAnimatedSource": "generated",
"source": "primary"
}
]
}Campos de cada juego
| Campo | Descripción |
|---|---|
idstring | Identificador opaco. Usalo para abrir el juego; no lo construyas ni deduzcas datos de él. |
titlestring | Título público del juego. |
categorystring | Categoría de lobby. |
studio · studioIdstring | null | Nombre público del estudio y su clave de filtro. Pueden ser null si no está informado. |
coverStaticUrlstring | null | Portada estática, o null. |
coverAnimatedUrlstring | null | Portada animada: la oficial si el juego la tiene; si no, una animación generada por Provibet a partir de su portada oficial; o null. |
coverAnimatedSourcestring | null | Origen de coverAnimatedUrl: official (portada animada oficial del juego), generated (animación generada por Provibet a partir de la portada estática oficial, sólo cuando no hay oficial) o null si no hay portada animada. Usalo, por ejemplo, para destacar sólo las oficiales. |
sourcestring | Origen del juego: primary (catálogo del proveedor principal) o partner (catálogo de un socio integrado, por ejemplo 4Salas). Sirve para agrupar o contar proveedores en tu lobby; no cambia la apertura ni el saldo. Ignorá valores que no conozcas. |
Paginación y sincronización
- Avanzá con
offset = offset + countmientrashasMoreseatrue. revisionidentifica la versión del catálogo filtrado (hoy es la fecha de su último cambio, o"empty"si no hay juegos). Si cambia entre páginas, reiniciá la sincronización. Incorporar portadas nuevas también la cambia.- El orden es estable: primero los juegos con portada animada oficial y después por título. Las animaciones generadas (
coverAnimatedSource: "generated") no cambian el orden ni el filtrocover=animated. - Sincronizá el catálogo en tu base (por ejemplo una vez por día) en lugar de consultarlo en cada visita: así respetás el límite de solicitudes.
- El total no es fijo: los juegos pueden habilitarse o deshabilitarse. Un juego que desaparece del catálogo ya no puede abrirse.
- Una categoría inexistente devuelve una lista vacía.
limituoffsetinválidos responden400 invalid_pagination; otro valor decover,400 invalid_cover_filter.
Portadas
- Las URLs apuntan a
https://api.provibet.com/media/{mediaId}: son públicas, no requieren firma y se sirven conCache-Control: public, max-age=86400, immutable. Tienen su propia cuota por IP de origen, separada de la de apertura. - Los formatos son WebP, PNG, JPEG o GIF, según el recurso.
- Las animaciones generadas por Provibet son WebP animados livianos cuyo primer cuadro es la portada oficial. Una portada animada oficial siempre tiene prioridad: si el juego la tiene, es la que llega en
coverAnimatedUrl. - Usá la portada estática como alternativa cuando no haya animada, y mostrala a quienes prefieren movimiento reducido (
prefers-reduced-motion). - Usá sólo las URLs recibidas: no construyas rutas de medios ni tomes imágenes de otros sitios.
<picture>
<source srcset="COVER_ANIMATED_URL" media="(prefers-reduced-motion: no-preference)">
<img src="COVER_STATIC_URL" alt="Sweet Bonanza" loading="lazy" width="300" height="400">
</picture>Integración
Estudios y categorías
Las facetas del catálogo para armar los filtros de tu lobby: estudios con juegos habilitados y cantidad de juegos por categoría.
Consultar facetas
/api/b2b/v1/studiosprovibet GET /api/b2b/v1/studiosconst { data } = await provibetRequest('GET', '/api/b2b/v1/studios');$r = provibetRequest('GET', '/api/b2b/v1/studios');{
"studios": [
{ "id": "pragmaticplay", "name": "Pragmatic Play", "games": 614 },
{ "id": "amusnet", "name": "Amusnet", "games": 347 },
{ "id": "playngo", "name": "Play'n GO", "games": 317 }
],
"categories": [
{ "id": "slots", "games": 3222 },
{ "id": "live", "games": 211 },
{ "id": "table", "games": 7 },
{ "id": "crash", "games": 2 },
{ "id": "sports", "games": 1 }
]
}- Ambas listas vienen ordenadas por cantidad de juegos, de mayor a menor.
- Usá
studios[].idcomo valor destudioenGET /games, ycategories[].idcomo valor decategory. - Los estudios sin nombre informado no se listan.
La clave del estudio
studioId se deriva del nombre público: se quitan acentos, se pasa a minúsculas y se conservan sólo letras y dígitos (hasta 60 caracteres). Por eso studio=Play'n GO y studio=playngo filtran lo mismo.
| Nombre público | studioId |
|---|---|
| Pragmatic Play Live | pragmaticplaylive |
| Play'n GO | playngo |
| Hacksaw Gaming | hacksawgaming |
La lista completa, con la cantidad de juegos de cada estudio, está en el apéndice Estudios.
Integración
Apertura de juegos
Tu backend pide la apertura y recibe una URL temporal de un solo uso. El jugador la abre en una pestaña o dentro de tu sitio, y cuando sale del juego tu página recibe un aviso.
Solicitar la apertura
/api/b2b/v1/games/{gameId}/launch| Campo | Regla |
|---|---|
externalPlayerId obligatoriostring | Jugador registrado y activo en tu cuenta. |
locale opcionalstring | Idioma preferido, por ejemplo es, es-AR o en-US. Por defecto es. Ver Idiomas. |
device opcionalstring | mobile o desktop. Se valida, pero hoy no cambia la apertura: el juego se adapta solo al dispositivo. |
provibet POST "/api/b2b/v1/games/$GAME_ID/launch" '{"externalPlayerId":"jugador-123","locale":"es-AR"}' "launch-$(date +%s)-$RANDOM"import { randomUUID } from 'node:crypto';
const body = JSON.stringify({ externalPlayerId: 'jugador-123', locale: 'es-AR' });
const { status, data } = await provibetRequest('POST', `/api/b2b/v1/games/${gameId}/launch`, body, `launch-${randomUUID()}`);
if (status === 200) sendToPlayer(data.launch.url); // sólo al navegador de ese jugador$body = json_encode(['externalPlayerId' => 'jugador-123', 'locale' => 'es-AR'], JSON_THROW_ON_ERROR);
$r = provibetRequest('POST', "/api/b2b/v1/games/{$gameId}/launch", $body, 'launch-' . bin2hex(random_bytes(12)));
$url = $r['data']['launch']['url']; // sólo al navegador de ese jugador{
"launch": {
"gameId": "game_0123456789abcdef0123456789abcdef01234567",
"url": "https://api.provibet.com/play/#tenant=CUENTA_OPACA&ticket=TICKET_TEMPORAL",
"expiresAt": "2026-09-24T12:03:00.000Z"
}
}- La URL vence en
expiresAt(hoy, 3 minutos después de emitida) y se canjea una sola vez. Cada solicitud genera una URL nueva. - Es una credencial temporal: no la publiques, no la registres completa y no la compartas entre jugadores. Si venció o ya se usó, pedí otra apertura.
- Abrir un juego no mueve fichas: el jugador juega con el saldo que le entregaste.
| Respuesta | Significado |
|---|---|
404 game_not_found | El juego no existe o ya no está habilitado. Actualizá tu catálogo. |
404 player_not_found | Jugador inexistente o inactivo. |
409 player_not_ready | El alta del jugador no terminó o necesita revisión. |
503 game_launch_unavailable · game_launch_disabled | La apertura de juegos está pausada. Informá al jugador y reintentá más tarde. |
503 service_temporarily_unavailable | Servicio no disponible por el momento. Reintentá con backoff. |
Juego dentro de tu sitio
Podés abrir launch.url en una pestaña nueva o embeberla en un iframe de tu lobby:
<iframe
src="LAUNCH_URL"
title="Juego"
allow="fullscreen; autoplay"
allowfullscreen
referrerpolicy="no-referrer"></iframe>El iframe sólo funciona desde orígenes registrados por Provibet. La página de apertura indica al navegador qué sitios pueden embeberla; desde cualquier otro origen el navegador bloquea el iframe. Pedí el alta de cada origen exacto (por ejemplo https://www.tucasino.com, sólo HTTPS) antes de publicar.
La página de apertura es neutra, sin marcas: quita el ticket de la barra de direcciones, lo canjea y muestra el juego a pantalla completa dentro de sí misma. Si el ticket venció o ya se usó, muestra «El enlace de juego expiró o no es válido. Volvé a abrir el juego desde tu casino.».
Aviso de salida del juego
Cuando el jugador sale del juego (por ejemplo, con el botón de volver al lobby del juego), la página de salida de Provibet envía a la ventana que lo contiene este mensaje con postMessage:
{ "type": "provibet:game-exit", "version": 1 }El mensaje no lleva identificadores, tokens ni saldos. Llega desde el origen https://api.provibet.com: verificalo antes de actuar.
window.addEventListener('message', event => {
if (event.origin !== 'https://api.provibet.com') return;
const msg = event.data;
if (!msg || msg.type !== 'provibet:game-exit' || msg.version !== 1) return;
closeGameFrame(); // quitá el iframe y volvé al lobby
refreshPlayerBalance(); // tu backend: GET /players/{id}/balance y, si corresponde, un débito
});Si el juego se abrió en una pestaña propia, el jugador ve «Juego finalizado. Podés cerrar esta ventana y volver al casino.».
Contrato interno de la página de apertura
Tu integración no necesita llamar a estas rutas: las usa la página /play/.
POST https://api.provibet.com/session/exchangecon{"tenantId":"…","ticket":"…"}. Requiere el encabezadoOrigin: https://api.provibet.com(si no,403 origin_not_allowed); no usa HMAC. Responde{"session":{"id","expiresAt"},"playUrl":"/session/play/{id}","target":"…"}y establece una cookie de sesiónSecure,HttpOnly,SameSite=Laxlimitada al host, válida por una hora.GET https://api.provibet.com/session/play/{sessionId}responde HTML con el juego para esa cookie de sesión.- Provibet puede revocar una sesión desde el panel. La revocación impide nuevos accesos a la sesión; no garantiza cortar un juego ya cargado en el navegador.
Integración
Operaciones
Cada crédito o débito crea una operación con un UUID. Consultala para conocer su estado real, sobre todo después de un 202 o de un corte de conexión.
Consultar una operación
/api/b2b/v1/operations/{operationId}provibet GET /api/b2b/v1/operations/00000000-0000-4000-8000-000000000001const { data } = await provibetRequest('GET', `/api/b2b/v1/operations/${operationId}`);
if (data.operation.state === 'COMPLETED') markOrderPaid();$r = provibetRequest('GET', '/api/b2b/v1/operations/' . $operationId);
$state = $r['data']['operation']['state'];{
"operation": {
"id": "00000000-0000-4000-8000-000000000001",
"kind": "CREDIT",
"state": "UNKNOWN",
"amountCredits": "50.00",
"currency": "CRD",
"createdAt": "2026-09-24T12:00:00.000Z",
"updatedAt": "2026-09-24T12:00:31.000Z"
}
}| Campo | Descripción |
|---|---|
id | UUID de la operación. |
kind | CREDIT: de tu billetera al jugador. DEBIT: del jugador a tu billetera. |
state | RESERVED, COMPLETED, REJECTED o UNKNOWN. Ver Estados. |
amountCredits · currency | Importe con dos decimales y unidad CRD. |
createdAt · updatedAt | Fechas ISO 8601 en UTC. |
- Una consulta exitosa responde
200aunque la operación siga pendiente: el resultado financiero está enoperation.state. - Sólo ves operaciones de tu cuenta; otra responde
404 operation_not_found. Usá exactamente el UUID recibido. - No hay listado ni búsqueda por referencia: guardá el
operation.idde cada respuesta junto a tu orden.
Seguimiento recomendado
- Persistí
operation.idapenas lo recibas. - Consultá con backoff: 1, 2, 5 y 10 segundos; luego cada 30 segundos con jitter.
- Cerrá la orden sólo con un estado terminal:
COMPLETEDoREJECTED. - Si una operación sigue en
UNKNOWN, escalala a Provibet con suoperation.idpara la conciliación.
Integración
Errores
Los errores usan un código estable en inglés y un identificador de correlación. Tu código debe decidir por error.code, no por el texto.
Formato
{
"error": {
"code": "invalid_request_signature",
"requestId": "5d1f0b7e-6c1a-4f1e-9a53-0e7b2b1c9d10"
}
}- Todas las respuestas incluyen el encabezado
X-Request-Id. Una ruta inexistente responde404 not_foundy puede omitirrequestIden el body: usá el encabezado. - Los rechazos de créditos y débitos (
422) no usan este formato: devuelvenoperationconstate: REJECTED. - Ante una respuesta HTML o vacía (por ejemplo, de un intermediario de red), no la interpretes como éxito ni como rechazo.
Códigos
| HTTP | Códigos | Qué hacer |
|---|---|---|
| 400 | invalid_request, invalid_json, invalid_amount, invalid_pagination, invalid_cover_filter, idempotency_key_required, duplicate_authentication_header, invalid_request_path | Corregí el formato o el campo. No repitas sin cambios. |
| 401 | invalid_authentication_headers, request_timestamp_outside_window, invalid_client_credentials, invalid_request_signature | Revisá clave activa, reloj, path, query y bytes firmados. |
| 403 | source_ip_not_allowed, source_ip_unavailable, insufficient_scope | Revisá la IP de salida y los permisos con Provibet. |
| 404 | player_not_found, game_not_found, operation_not_found, not_found | Verificá la ruta y el identificador dentro de tu cuenta. |
| 409 | nonce_replayed_or_invalid, idempotency_conflict, insufficient_credits, player_not_ready, player_initialization_pending | Distinguí nonce, intención y saldo antes de actuar. |
| 413 | request_too_large | El body no puede superar 64 KiB. |
| 421 | unexpected_host | Llamá a api.provibet.com exactamente. |
| 422 | operation.state = REJECTED | Registrá el rechazo del crédito o débito. |
| 429 | rate_limit_exceeded | Backoff exponencial con jitter. Ver Límite de solicitudes. |
| 500 | service_unavailable | Guardá el requestId. En un crédito o débito, tratá el resultado como incierto. |
| 503 | service_temporarily_unavailable, financial_operations_disabled, registration_unavailable, game_launch_unavailable, game_launch_disabled, player_initialization_review, service_busy | Esperá y reintentá con backoff; conservá la intención si había fichas involucradas. |
Diagnóstico rápido
401 invalid_request_signature
Compará tu cadena canónica con la de Autenticación: nueve líneas, path con prefijo, query ordenado y codificado, clave idempotente exacta y hash de los bytes enviados. Verificá que el Content-Type sea application/json.
401 request_timestamp_outside_window
Tu reloj está desfasado más de 300 segundos o estás enviando milisegundos. Sincronizá por NTP y usá segundos Unix de 10 dígitos.
409 nonce_replayed_or_invalid
Reutilizaste un nonce o reenviaste encabezados ya firmados. Generá timestamp, nonce y firma nuevos en cada intento.
Timeout o conexión cortada en un crédito o débito
El resultado es incierto. Repetí la solicitud con la misma clave idempotente y el mismo importe, o consultá la operación si ya tenés su UUID. Ver Resultado incierto.
Integración
Límite de solicitudes
La API limita la frecuencia por clave pública y por IP de origen para proteger el servicio de todos los operadores.
Cuotas
| Ámbito | Límite | Alcance |
|---|---|---|
Por clave pública (X-B2B-Key) | 120 / minuto | Todas las solicitudes a /api/b2b/v1 con esa clave, incluidas las rechazadas por firma o permisos. |
| Por IP de origen | 360 / minuto | Todo el tráfico de esa IP hacia api.provibet.com, incluida la apertura (/play/ y /session/exchange). Las portadas no cuentan para esta cuota. |
| Portadas, por IP de origen | 2.400 / minuto | Solicitudes GET o HEAD a https://api.provibet.com/media/{mediaId}. Es una cuota aparte: un lobby con muchas portadas no bloquea la apertura de juegos. |
Las ventanas son fijas de 60 segundos: cada una empieza con la primera solicitud y se reinicia al vencer.
Si recibís 429
- La respuesta es
429 rate_limit_exceededy no incluyeRetry-After. - Esperá con backoff exponencial y jitter (por ejemplo 2, 4, 8 segundos…) y reducí la concurrencia. La ventana se libera, como máximo, 60 segundos después de abrirse.
- En un crédito o débito, un
429significa que la solicitud no se procesó: reintentá con la misma clave idempotente. - Con carga excepcional, la API puede responder
503 service_busy: tratala igual.
Buenas prácticas
- Sincronizá el catálogo y los estudios en tu base; no los pidas en cada visita.
- Pedí páginas de 200 juegos para reducir llamadas.
- Consultá saldos cuando los necesites (al abrir y al cerrar un juego), no en intervalos cortos.
- Consultá operaciones pendientes con el backoff sugerido.
- Si descargás portadas desde tu servidor, cachealas: esas descargas cuentan para la cuota de portadas de tu IP.
Otros límites
| Límite | Valor |
|---|---|
| Tamaño del body JSON | 64 KiB |
| Juegos por página | 1 a 200 (por defecto 50) |
offset máximo | 1.000.000 |
Búsqueda q | 100 caracteres |
| Importe por solicitud | 10000000000.00 CRD |
| Vigencia de la URL de apertura | 3 minutos, un solo uso |
| Desvío de reloj admitido | ±300 segundos |
Próximamente
Carreras de caballos En preparación
Una nueva vertical de apuestas a carreras de caballos para sumar a tu plataforma, integrada al mismo saldo de fichas de tus jugadores.
Todavía no está disponible. Esta página describe el concepto. No hay endpoints de carreras en la API v1 y nada de lo que integres hoy cambia por esta vertical. El contrato de API se va a publicar en esta documentación antes de su activación.
Cómo va a funcionar
- Mismo jugador, mismo saldo: el jugador usa las fichas que ya le entregaste; no vas a necesitar una segunda billetera ni un segundo registro.
- Movimiento automático al abrir: cuando el jugador abre las carreras, su saldo pasa automáticamente a la billetera de carreras.
- Regreso automático al cerrar: cuando sale, el saldo vuelve a su saldo de casino.
- Apertura como un juego más: se va a abrir con una URL temporal, igual que hoy la apertura de juegos.
Qué esperar
Antes de activar la vertical vamos a publicar el contrato completo: endpoints, estados de los movimientos entre billeteras, errores y reglas de conciliación. Si te interesa sumarla, avisale a tu contacto de Provibet.
Apéndice
Estudios
Los 27 estudios del catálogo, con su clave de filtro y cantidad de juegos: 3.443 juegos en total al 24/09/2026.
Es una foto del catálogo a esa fecha. Los juegos habilitados pueden variar: la lista vigente para tu cuenta se obtiene con GET /studios.
| # | Estudio | studioId | Juegos | Por categoría |
|---|---|---|---|---|
| 1 | Pragmatic Play | pragmaticplay | 614 | slots 612 · table 2 |
| 2 | Amusnet | amusnet | 347 | slots 347 |
| 3 | Play'n GO | playngo | 317 | slots 317 |
| 4 | Reel Kingdom | reelkingdom | 216 | slots 216 |
| 5 | Pragmatic Play Live | pragmaticplaylive | 212 | live 211 · crash 1 |
| 6 | EGT Digital | egtdigital | 163 | slots 163 |
| 7 | Joker Gaming | jokergaming | 155 | slots 155 |
| 8 | CQ9 | cq9 | 145 | slots 145 |
| 9 | PG Soft | pgsoft | 134 | slots 133 · table 1 |
| 10 | Amatic | amatic | 132 | slots 132 |
| 11 | Hacksaw Gaming | hacksawgaming | 130 | slots 130 |
| 12 | Habanero | habanero | 122 | slots 122 |
| 13 | Ruby Play | rubyplay | 122 | slots 122 |
| 14 | Royalgaming | royalgaming | 81 | slots 81 |
| 15 | DreamTech | dreamtech | 76 | slots 76 |
| 16 | FastSpin | fastspin | 69 | slots 68 · table 1 |
| 17 | Spadegaming | spadegaming | 62 | slots 62 |
| 18 | NextSpin | nextspin | 58 | slots 58 |
| 19 | Booongo | booongo | 55 | slots 55 |
| 20 | Fa Chai | fachai | 51 | slots 49 · table 2 |
| 21 | Evoplay | evoplay | 49 | slots 49 |
| 22 | Playson | playson | 48 | slots 48 |
| 23 | Fat Panda | fatpanda | 33 | slots 33 |
| 24 | TopTrend Gaming | toptrendgaming | 33 | slots 33 |
| 25 | Fish Hunter | fishhunter | 9 | slots 9 |
| 26 | Spribe | spribe | 9 | slots 7 · crash 1 · table 1 |
| 27 | Sportsbook | sportsbook | 1 | sports 1 |
| Total | 3.443 | 68 con portada animada oficial |
Apéndice
Categorías
Cada juego pertenece a una de cinco categorías de lobby. Son los valores válidos del filtro category.
category | Nombre sugerido | Qué incluye | Juegos al 24/09/2026 |
|---|---|---|---|
slots | Tragamonedas | Slots de video y clásicos, incluidos juegos de pesca y arcade. | 3.222 |
live | Casino en vivo | Mesas con crupier en vivo. | 211 |
table | Juegos de mesa | Ruleta, blackjack, baccarat, póker y similares, no en vivo. | 7 |
crash | Crash | Juegos de multiplicador creciente. | 2 |
sports | Deportes | Apuestas deportivas. | 1 |
Los nombres sugeridos son orientativos: mostralos como prefieras en tu lobby, pero enviá siempre el valor de category tal cual. La cantidad vigente se consulta con GET /studios.
Apéndice
Idiomas
El idioma se elige al abrir un juego, con el campo opcional locale de POST /games/{id}/launch.
Formato aceptado
Dos letras en minúscula (código de idioma) y, opcionalmente, un guion y dos letras en mayúscula (región): ^[a-z]{2}(-[A-Z]{2})?$. Si no se envía, se usa es.
| Válidos | Inválidos (400 invalid_request) |
|---|---|
es, es-AR, es-MX | ES, es-ar, es_AR |
en, en-US | eng, en-USA |
pt, pt-BR | pt-br, zh-Hans |
Qué idiomas ve el jugador
- El valor se entrega al juego como preferencia. Cada estudio soporta su propio conjunto de idiomas; si no admite el pedido, el juego se muestra en su idioma por defecto.
- La página de apertura y sus mensajes están en español.
- Los códigos de error de la API son identificadores estables en inglés (por ejemplo
player_not_found): traducilos en tu interfaz.
Apéndice
Moneda
CRD es la unidad de fichas de Provibet. No es una moneda de curso legal y la API no convierte importes: las condiciones comerciales de las fichas se acuerdan fuera de la API.
Importes que enviás
amountCredits es un string decimal:
- Parte entera sin ceros a la izquierda (salvo
0) y, opcionalmente, un punto con una o dos posiciones decimales. - Mayor que cero y hasta
10000000000.00. - Sin signo, sin separador de miles, sin coma decimal, sin exponentes y nunca como número JSON.
currencyes opcional; si lo enviás, tiene que ser"CRD".
| Válidos | Inválidos (400 invalid_request) |
|---|---|
"50", "50.5", "50.25" | 50 (número), "50.255", "050" |
"0.01", "0.5" | "0", "0.00", "-5", "+5" |
"10000000000.00" | "1,5", "1.000" como miles, "1e3", "10000000000.01" |
Importes que recibís
Las respuestas usan siempre dos decimales: availableCredits, reservedCredits, balanceCredits y amountCredits tienen el formato ^(0|[1-9][0-9]*)\.[0-9]{2}$, por ejemplo "0.00" o "9500.00".
Precisión
Internamente cada importe es un entero de centésimos. Guardalo como decimal exacto o como entero de centésimos en tu base, y no lo conviertas a punto flotante.
// String CRD → centésimos (BigInt) y vuelta, sin punto flotante.
const toCents = s => { const [i, f = ''] = s.split('.'); return BigInt(i) * 100n + BigInt(f.padEnd(2, '0')); };
const toCrd = c => `${c / 100n}.${String(c % 100n).padStart(2, '0')}`;
toCrd(toCents('50.5') + toCents('0.75')); // "51.25"// Con BCMath: strings decimales exactos.
$total = bcadd('50.5', '0.75', 2); // "51.25"Apéndice
Referencia OpenAPI
El contrato completo de la API v1 en formato OpenAPI 3.1: rutas, parámetros, esquemas, ejemplos y respuestas de error.
Qué incluye
- Los nueve endpoints de
/api/b2b/v1, con su permiso requerido enx-required-scope. - Las rutas del navegador:
/play/,/media/{mediaId},/session/exchangey/session/play/{sessionId}. - Esquemas de billetera, jugadores, operaciones, juegos, estudios y errores, con los códigos de cada respuesta.
Podés importarlo en tu cliente HTTP o generador de código preferido. La firma HMAC no se puede expresar en OpenAPI: implementala según Autenticación y firma o usá los helpers.
Cambios
| Fecha | Cambio |
|---|---|
| 26/09/2026 | Catálogo: campo nuevo source en cada juego (primary o partner). Es aditivo: los demás campos, el orden y los filtros no cambian. |
| 25/09/2026 | Catálogo: campo nuevo coverAnimatedSource (official, generated o null). coverAnimatedUrl entrega una animación generada por Provibet cuando el juego no tiene portada animada oficial. El orden y el filtro cover=animated siguen considerando sólo las oficiales. Las portadas (/media/{mediaId}) tienen su propia cuota por IP de origen (2.400 por minuto) y ya no descuentan de las 360 por minuto que protegen la apertura. |
| 24/09/2026 | Nueva documentación. OpenAPI alineado con la implementación: límites de frecuencia y ventana de ±300 s explícitos, códigos de error reales, sin respuestas 415/502, sin mobileSupported en juegos, ruta pública /media/{mediaId} y descripciones de estudios, categorías e idiomas. |
| 23/09/2026 | v1: autenticación HMAC, billetera, jugadores, créditos y débitos, catálogo con estudios y categorías, apertura embebible y operaciones. |
Apéndice
Ejemplos
Helpers sin dependencias externas para firmar y enviar solicitudes desde tu backend. No hacen reintentos financieros automáticos: tu aplicación decide según el estado.
Los tres leen PROVIBET_PUBLIC_KEY, PROVIBET_SECRET y, opcionalmente, PROVIBET_ORIGIN (por defecto https://api.provibet.com) del entorno.
Node.js
Node.js 20 o superior. provibetRequest(method, path, rawBody, idempotencyKey) devuelve status, requestId, retryAfter y data. signRequest() expone la firma por separado.
Cargando el helper… Si no aparece, usá el enlace de descarga.PHP
PHP 8.1 o superior, con las extensiones cURL e iconv. Descargalo como request.php e incluilo sólo desde tu backend.
Cargando el helper… Si no aparece, usá el enlace de descarga.cURL
Linux o macOS con Node.js 20+ y cURL. sign-for-curl.mjs importa node.mjs: descargá ambos en la misma carpeta. Crea un archivo de encabezados nuevo por intento; el secreto nunca se pasa como argumento.
Cargando el helper… Si no aparece, usá el enlace de descarga.umask 077
work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT
printf '%s' '{"amountCredits":"50","currency":"CRD"}' > "$work/body" # guardá estos bytes y la clave
url='https://api.provibet.com/api/b2b/v1/players/jugador-123/credits'
node sign-for-curl.mjs POST "$url" "$work/body" "$work/headers" credit-order-5001
curl -sS -X POST "$url" --header "@$work/headers" --data-binary "@$work/body"No reutilices un archivo de encabezados: cada intento necesita timestamp, nonce y firma nuevos. Un error de cURL no demuestra que la entrega se haya rechazado.