API de ventas v1
La API de ventas concentra en el servidor la autorización, disponibilidad de sorteos, límites, validaciones y creación de tickets. Los clientes no deben calcular ni enviar moneda, agencia, comisiones, límites o el total final.
URL de producción:
/api/v1/store/salesAutenticación
La API requiere un usuario autenticado. Desde la web de esta aplicación se usa la sesión por cookies (no se envía el header Authorization). Para un cliente externo se usa un API token de taquilla, que el dueño genera, rota y revoca desde la web (/banca/taquillas/:id):
Authorization: Bearer tbk_<api-token>El token empieza con el prefijo tbk_ y está vinculado a una sola taquilla: todas las solicitudes (access, sales) deben apuntar a esa taquilla; si el taquillaId no coincide, la API responde 403 TOKEN_TAQUILLA_MISMATCH. El token solo se muestra una vez al generarlo o rotarlo: guárdalo de forma segura en el cliente. Si se pierde o se sospecha de fuga, revócalo y genera uno nuevo.
El usuario debe estar activo, no tener rol client y ser el propietario de la taquilla solicitada. La API también aplica las excepciones de desarrollo configuradas por el servidor.
La ruta permite CORS para GET, POST y OPTIONS. No envíes claves service_role ni otros secretos al cliente.
Cada solicitud a access o sales valida el token de manera independiente. No es obligatorio llamar primero a access: aunque un cliente omita esa consulta, POST /sales vuelve a comprobar autenticación y todas las reglas de acceso antes de crear el ticket. Un token tbk_ inválido o revocado devuelve 401 INVALID_API_TOKEN; una sesión web ausente o vencida devuelve 401 AUTHENTICATION_REQUIRED.
Comprobar acceso a una taquilla
Este endpoint ejecuta los mismos controles usados para la creación de tickets. Es útil para bloquear o redirigir al usuario en una aplicación externa antes de cargar el contexto completo.
GET /api/v1/store/access?taquillaId=<uuid>
Authorization: Bearer tbk_<api-token>Respuesta permitida (200):
{
"data": {
"allowed": true,
"timerExpiresAt": 1786993200000
}
}Respuesta denegada (400, 401, 403, 404 o 500, según la causa):
{
"data": {
"allowed": false,
"code": "PORTAL_CLOSED",
"status": 403,
"variant": "warning",
"title": "Portal cerrado",
"message": "El horario de ventas ha finalizado.",
"recoveryAction": "OPEN_TAQUILLA"
}
}El endpoint comprueba banca activa, horario, modo standby y grupos excluidos; autenticación, rol, estado y propiedad del usuario; estado y moneda de la taquilla; agencia, grupo y zona; antigüedad del último cuadre; límites de venta y utilidad de banca; y creación del temporizador de sesión. Los textos, estilos y reglas habilitadas se toman de bank_access_control.
El cliente debe usar el estado HTTP para el flujo de red y data.code para decisiones programáticas. title, message y variant están listos para presentar el rechazo al usuario.
recoveryAction expresa una intención independiente de la plataforma y nunca contiene una URL. Sus valores actuales son:
| Acción | Significado |
|---|---|
GO_HOME | Salir del flujo de venta y volver a la pantalla principal definida por el cliente. |
OPEN_TAQUILLA | Salir de la venta y abrir la información de la taquilla solicitada. |
Cada aplicación decide cómo navegar. Por ejemplo, la web puede mapear OPEN_TAQUILLA a /banca/taquillas/:id, mientras una app móvil puede abrir TaquillaDetails y otro sistema web puede usar su propia ruta. El cliente también puede ignorar recoveryAction y limitarse a mostrar el rechazo.
Consultar la venta disponible
Obtiene el contexto vigente de una taquilla: sorteos abiertos, loterías permitidas, límites, ventas del día y la hora de Caracas usada por el servidor.
GET /api/v1/store/sales?taquillaId=<uuid>
Authorization: Bearer tbk_<api-token>Ejemplo con fetch:
const response = await fetch(
`${API_URL}?taquillaId=${taquillaId}`,
{
headers: { Authorization: `Bearer ${apiToken}` },
}
)
const result = await response.json()
if (!response.ok) throw new Error(result.error.message)
const saleContext = result.dataRespuesta exitosa (200):
{
"data": {
"taquilla": {
"id": "uuid",
"name": "Taquilla principal",
"currency": "VES",
"agency_id": "uuid"
},
"agency": {
"min_bet": 100,
"max_bet": 100000,
"lotteries_ids": ["uuid"]
},
"lotteries": [
{
"id": "uuid",
"name": "Lotería",
"lottery_type": "O",
"timeout": 5,
"min_number": 0,
"max_number": 99
}
],
"schedules": [
{
"id": "uuid",
"lottery_id": "uuid",
"h": 14,
"m": 30,
"days_available": [0, 1, 2, 3, 4, 5, 6]
}
],
"caracasTime": {
"year": 2026,
"month": 7,
"day": 29,
"weekday": 3,
"h": 14,
"m": 12,
"s": 0
}
}
}lottery_type permite filtrar las loterías por tipo y puede tener los valores O, N o Z.
schedules contiene únicamente sorteos activos para el día y la hora de Caracas. Los arreglos adicionales (dailyNumberSales, agencyLimits, customTicketNumbers, customScheduleNumbers y numbersByAgencyIdInDates) se exponen para presentar disponibilidad en tiempo real. El servidor vuelve a consultar y validar todo al crear el ticket.
Crear un ticket
Envía únicamente la taquilla, el número, el sorteo y el monto. amount se expresa en la unidad visible de la moneda, no en centavos. Por ejemplo, para vender 10.00, envía 10.
No envíes prize: el servidor lo calcula con el multiplicador personalizado vigente para el número y la lotería, o con winner_times de la lotería cuando no hay una configuración personalizada.
POST /api/v1/store/sales
Content-Type: application/json
Authorization: Bearer tbk_<api-token>{
"taquillaId": "11111111-1111-1111-1111-111111111111",
"lines": [
{
"number": "05",
"scheduleId": "22222222-2222-2222-2222-222222222222",
"amount": 10
}
]
}Ejemplo con fetch:
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiToken}`,
},
body: JSON.stringify({ taquillaId, lines }),
})
const result = await response.json()
if (!response.ok) throw new Error(result.error.message)
const ticket = result.dataRespuesta exitosa (201):
{
"data": {
"ticket_id": "33333333-3333-3333-3333-333333333333",
"ticket_code": 123456
}
}Validaciones del servidor
Antes de crear el ticket, la API verifica, entre otras reglas:
- Que la taquilla, usuario, agencia, lotería y sorteo estén disponibles.
- Que el token de acceso (si se usa un API token) pertenezca a la taquilla solicitada.
- Que el sorteo pertenezca a una lotería permitida para la agencia.
- Horario de cierre usando la hora de Caracas del servidor.
- Números bloqueados, montos positivos, mínimos, máximos y múltiplos por moneda.
- Límites por ticket, sorteo, número y ventas diarias de agencia.
- Comisiones, moneda, agencia, total y premio usando los datos actuales del servidor.
Una respuesta 422 indica que una regla de la banca no pasó:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "El sorteo ... ha cerrado. (C20)"
}
}Errores
| Estado | Código | Significado |
|---|---|---|
400 | INVALID_REQUEST | El body no cumple el contrato: UUID inválido, línea incompleta, más de 200 líneas, monto negativo, etc. |
401 | INVALID_API_TOKEN | El API token tbk_ es inválido o fue revocado. |
401 | AUTHENTICATION_REQUIRED | Falta el token o la sesión web es inválida o está vencida. |
403 | TOKEN_TAQUILLA_MISMATCH | El API token no pertenece a la taquilla solicitada. |
400 | SALE_ERROR | El usuario autenticado no puede operar la taquilla o falló otra regla de acceso. |
422 | VALIDATION_ERROR | El ticket no cumple una regla de la banca; muestra error.message al usuario. |
429 | RATE_LIMIT_EXCEEDED | Límite de solicitudes excedido. Aplica espera incremental antes de reintentar. |
503 | DATABASE_UNAVAILABLE | La conexión a la base de datos no está disponible. |
Formato común de error:
{
"error": {
"code": "SALE_ERROR",
"message": "No autorizado para vender en esta taquilla"
}
}Recomendaciones de cliente
- Consulta
GETal abrir la pantalla y vuelve a hacerlo al reanudar la aplicación o antes de mostrar disponibilidad actualizada. - Trata el
POSTcomo la decisión final: una disponibilidad mostrada previamente puede cambiar mientras el usuario prepara el ticket. - Tras un
422, actualiza el contexto conGETy muestra el mensaje recibido. - Guarda el API token de forma segura (por ejemplo, en el almacenamiento seguro del dispositivo) y revócalo desde la web si se filtra.
- El endpoint no incluye idempotencia. Cada
POSTes considerado como un nuevo ticket.
Límites de solicitudes
Límites predeterminados:
- 90 solicitudes por API key (o token) por minuto.
- 90 solicitudes por IP por minuto.
Cada respuesta incluye RateLimit-Limit, RateLimit-Remaining y RateLimit-Reset (segundos restantes de la ventana). Al superar el límite, la API responde 429 RATE_LIMIT_EXCEEDED con Retry-After en segundos:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Límite de solicitudes excedido. Espere antes de reintentar."
}
}Evita consultar saldo en cada pulsación del usuario. Ante un 429, usa espera incremental con variación aleatoria y no ejecutes reintentos en bucle.