Evenxa API

Backend corriendo correctamente. Esta pantalla resume las rutas activas por grupo, explica para qué sirve cada endpoint y muestra JSON de ejemplo para probar más rápido.

v1.0.0
15 grupos
80 rutas

Grupo

Sistema

2 rutas

Rutas simples para saber si el backend y la base de datos están vivos.

GET /health Pública

Healthcheck básico

Confirma que la API responde correctamente.

Response

{
  "status": "ok"
}
GET /health/db Pública

Healthcheck de base de datos

Valida conexión con Postgres. Si falla, responde 503.

Response

{
  "status": "ok",
  "database": {
    "connected": true
  }
}

Grupo

Autenticación

8 rutas

Registro, login, tokens y recuperación de cuenta.

POST /auth/register Pública

Registrar usuario

Crea una cuenta nueva. La password debe tener 8 a 32 caracteres, mayúscula, número y carácter especial. accepted_legal_document_ids debe incluir el id de cada documento legal obligatorio (aviso de privacidad y términos y condiciones) devuelto por GET /legal/documents/active; derechos ARCO no se incluye porque es solo informativo. La cuenta y las aceptaciones se crean en una sola transacción.

Request

{
  "email": "ana@evenxa.com",
  "password": "Password1!",
  "nombre": "Ana",
  "apellido_paterno": "López",
  "apellido_materno": "García",
  "telefono": "5512345678",
  "curp": "LOGA990101MDFPRN01",
  "fecha_nacimiento": "1999-01-01",
  "accepted_legal_document_ids": [
    "00000000-0000-4000-8000-000000000000",
    "00000000-0000-4000-8000-000000000000"
  ]
}

Response

{
  "data": {
    "user": {
      "id": "00000000-0000-4000-8000-000000000000",
      "email": "ana@evenxa.com",
      "nombre": "Ana"
    },
    "access_token": "jwt_access_token",
    "refresh_token": "jwt_refresh_token"
  }
}
POST /auth/login Pública

Iniciar sesión

Valida credenciales y regresa tokens para consumir rutas protegidas. El usuario incluye legal_acceptance_complete, privacy_accepted, terms_accepted y los timestamps de aceptación calculados desde la base de datos.

Request

{
  "email": "ana@evenxa.com",
  "password": "Password1!"
}

Response

{
  "data": {
    "user": {
      "id": "00000000-0000-4000-8000-000000000000",
      "email": "ana@evenxa.com",
      "nombre": "Ana",
      "legal_acceptance_complete": true,
      "privacy_accepted": true,
      "privacy_accepted_at": "2026-07-30T12:00:00.000Z",
      "terms_accepted": true,
      "terms_accepted_at": "2026-07-30T12:00:00.000Z"
    },
    "access_token": "jwt_access_token",
    "refresh_token": "jwt_refresh_token"
  }
}
POST /auth/refresh Pública

Renovar access token

Recibe un refresh token válido y entrega un access token nuevo.

Request

{
  "refresh_token": "jwt_refresh_token"
}

Response

{
  "data": {
    "access_token": "new_jwt_access_token"
  }
}
POST /auth/logout Bearer token

Cerrar sesión

Invalida la sesión del usuario autenticado.

Response

{
  "message": "Sesión cerrada correctamente"
}
POST /auth/verificar-email Pública

Verificar email

Confirma el correo usando el código de 6 dígitos enviado al usuario.

Request

{
  "codigo": "123456"
}

Response

{
  "message": "Email verificado correctamente"
}
POST /auth/recuperar-password Pública

Solicitar recuperación

Envía un código para iniciar el flujo de recuperación de password.

Request

{
  "email": "ana@evenxa.com"
}

Response

{
  "message": "Código enviado correctamente"
}
POST /auth/reset-password Pública

Cambiar password

Actualiza la password después de validar el código de recuperación.

Request

{
  "email": "ana@evenxa.com",
  "codigo": "123456",
  "password": "NuevaPass1!"
}

Response

{
  "message": "Password actualizada correctamente"
}
GET /auth/google Pública

Login con Google

Redirige al flujo OAuth de Google.

Response

{
  "redirect": "https://accounts.google.com/..."
}

Grupo

Usuarios

3 rutas

Perfil y acciones de la cuenta autenticada.

GET /usuarios/perfil Bearer token

Obtener perfil

Regresa los datos del usuario autenticado, incluyendo dirección de contacto y el estado canónico de aceptación legal cuando existan.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "ana@evenxa.com",
    "nombre": "Ana",
    "legal_acceptance_complete": true,
    "privacy_accepted": true,
    "privacy_accepted_at": "2026-07-30T12:00:00.000Z",
    "terms_accepted": true,
    "terms_accepted_at": "2026-07-30T12:00:00.000Z",
    "telefono": "5512345678",
    "direccion": {
      "estado_id": "22",
      "estado": "Querétaro",
      "municipio_id": "22014",
      "municipio": "Querétaro",
      "calle": "Av. Constituyentes",
      "numero": "S/N",
      "codigo_postal": "76000",
      "colonia": "Centro"
    }
  }
}
PUT /usuarios/actualizar-perfil Bearer token

Actualizar perfil

Permite cambiar datos personales y dirección de contacto. El municipio debe pertenecer al estado enviado. El código postal debe ser de 5 dígitos.

Request

{
  "nombre": "Ana María",
  "telefono": "5598765432",
  "direccion": {
    "estado_id": "22",
    "municipio_id": "22014",
    "calle": "Av. Constituyentes",
    "numero": "S/N",
    "codigo_postal": "76000",
    "colonia": "Centro"
  }
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "nombre": "Ana Maria",
    "telefono": "5598765432",
    "direccion": {
      "estado_id": "22",
      "estado": "Querétaro",
      "municipio_id": "22014",
      "municipio": "Querétaro",
      "calle": "Av. Constituyentes",
      "numero": "S/N",
      "codigo_postal": "76000",
      "colonia": "Centro"
    }
  }
}
DELETE /usuarios/eliminar-cuenta Bearer token

Eliminar cuenta

Elimina o desactiva la cuenta del usuario autenticado.

Response

{
  "message": "Cuenta eliminada correctamente"
}

Grupo

Documentos legales

3 rutas

Aviso de privacidad, términos y condiciones y derechos ARCO. Los documentos obligatorios se aceptan durante POST /auth/register; POST /legal/acceptances cubre cuentas existentes que aún tengan documentos pendientes.

GET /legal/documents/active Pública

Documentos legales activos

Regresa la versión activa de cada documento legal (aviso de privacidad, términos y condiciones, derechos ARCO) con una URL firmada de solo lectura. Usado por el formulario de registro para mostrar los documentos y sus ids antes de crear la cuenta.

Response

{
  "documents": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "type": "privacy_notice",
      "name": "Aviso de privacidad",
      "version": "2026-07-22",
      "requires_acceptance": true,
      "url": "https://storage.googleapis.com/kustika-documentos/...",
      "published_at": "2026-07-22T16:04:10.000Z"
    }
  ]
}
GET /legal/acceptance-status Bearer token

Estado de aceptación del usuario

Fuente de verdad para saber si al usuario autenticado le falta aceptar algún documento legal obligatorio activo. No bloquea login ni navegación; el frontend decide qué hacer con el resultado.

Response

{
  "is_complete": false,
  "missing_documents": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "type": "terms_and_conditions",
      "name": "Términos y condiciones",
      "version": "2026-07-22",
      "requires_acceptance": true,
      "url": "https://storage.googleapis.com/kustika-documentos/..."
    }
  ]
}
POST /legal/acceptances Bearer token

Aceptar documentos legales pendientes

Para cuentas ya existentes: registra la aceptación de uno o más documentos legales obligatorios y activos por su id. Acepta envíos parciales (no exige incluir todos los pendientes en una sola llamada), es idempotente, y usa transacción. El user_id siempre se toma del token, nunca del body.

Request

{
  "accepted_legal_document_ids": [
    "00000000-0000-4000-8000-000000000000"
  ]
}

Response

{
  "message": "Documentos legales aceptados correctamente",
  "is_complete": true,
  "accepted_document_ids": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "missing_documents": []
}

Grupo

Eventos

11 rutas

Consulta pública de eventos y gestión para organizadores.

GET /eventos?page=1&limit=20&busqueda=rock Pública

Listar eventos publicados

Regresa eventos visibles para usuarios. Acepta filtros por categoria_id, búsqueda, page y limit.

Response

{
  "data": {
    "eventos": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "titulo": "Noche Indie",
        "imagen_portada": "http://127.0.0.1:3000/uploads/local/eventos/portada.webp",
        "imagen_hero": "http://127.0.0.1:3000/uploads/local/eventos/hero.webp",
        "imagen_background": "http://127.0.0.1:3000/uploads/local/eventos/hero.webp",
        "status": "publicado",
        "estado_venue": "Querétaro",
        "estado_venue_id": "22",
        "ciudad_venue": "Querétaro",
        "municipio_venue_id": "22014"
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20,
    "pages": 1
  }
}
GET /eventos/:id Pública

Detalle de evento

Obtiene información completa del evento, incluyendo funciones y tipos de boleto.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "titulo": "Noche Indie",
    "imagen_portada": "http://127.0.0.1:3000/uploads/local/eventos/portada.webp",
    "imagen_hero": "http://127.0.0.1:3000/uploads/local/eventos/hero.webp",
    "imagen_background": "http://127.0.0.1:3000/uploads/local/eventos/hero.webp",
    "categoria": "Conciertos",
    "funciones": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "fecha_inicio": "2026-07-20T21:00:00.000Z",
        "tipos_boleto": []
      }
    ]
  }
}
POST /eventos Bearer token

Crear evento

Crea un evento en borrador. Requiere rol event_manager o admin.

Request

{
  "titulo": "Noche Indie",
  "descripcion": "Concierto en vivo",
  "descripcion_corta": "Una noche de bandas emergentes",
  "categoria_id": "00000000-0000-4000-8000-000000000000",
  "recinto_id": "00000000-0000-4000-8000-000000000000",
  "nombre_venue": "Foro Centro",
  "estado_venue_id": "22",
  "municipio_venue_id": "22014",
  "direccion_venue": "Av. Constituyentes 123",
  "imagen_portada": "http://127.0.0.1:3000/uploads/local/eventos/portada.webp",
  "imagen_hero": "http://127.0.0.1:3000/uploads/local/eventos/hero.webp",
  "imagen_background": "http://127.0.0.1:3000/uploads/local/eventos/hero.webp",
  "tags": [
    "indie",
    "música"
  ],
  "edad_minima": 18
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "titulo": "Noche Indie",
    "imagen_hero": "http://127.0.0.1:3000/uploads/local/eventos/hero.webp",
    "imagen_background": "http://127.0.0.1:3000/uploads/local/eventos/hero.webp",
    "status": "borrador"
  }
}
PUT /eventos/:id Bearer token

Editar datos generales de un evento

Actualización parcial. Permite cambiar categoría, recinto, textos e imágenes. Envía null para limpiar campos opcionales. recinto_id y venue_id son alias y, si se envían juntos, deben coincidir. No modifica funciones ni tipos de boleto.

Request

{
  "titulo": "Noche Indie actualizada",
  "descripcion_corta": null,
  "categoria_id": "00000000-0000-4000-8000-000000000000",
  "recinto_id": "00000000-0000-4000-8000-000000000000",
  "imagen_hero": "http://127.0.0.1:3000/uploads/local/eventos/hero.webp",
  "imagen_background": "http://127.0.0.1:3000/uploads/local/eventos/background.webp"
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "titulo": "Noche Indie actualizada",
    "descripcion_corta": null,
    "categoria_id": "00000000-0000-4000-8000-000000000000"
  }
}
PUT /eventos/:id/imagen-portada/transform Bearer token

Guardar transform de portada

Descarga la imagen fuente, aplica encuadre 16:9 en 1920x1080, guarda una nueva imagen optimizada y actualiza la portada del evento.

Request

{
  "sourceUrl": "http://127.0.0.1:3000/uploads/local/eventos/original.jpg",
  "fitMode": "cover",
  "zoom": 1.2,
  "scaleX": 1,
  "scaleY": 1,
  "offsetX": 0,
  "offsetY": -40,
  "outputWidth": 1920,
  "outputHeight": 1080
}

Response

{
  "imagen_portada": "http://127.0.0.1:3000/uploads/local/eventos/portadas/portada.webp"
}
POST /eventos/:id/funciones Bearer token

Agregar función

Agrega una fecha u horario a un evento administrado por el usuario.

Request

{
  "nombre": "Función principal",
  "fecha_inicio": "2026-07-20T21:00:00.000Z",
  "fecha_fin": "2026-07-20T23:30:00.000Z",
  "fecha_apertura_puertas": "2026-07-20T20:00:00.000Z"
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "nombre": "Función principal",
    "fecha_inicio": "2026-07-20T21:00:00.000Z"
  }
}
POST /eventos/funciones/:funcionId/tipos-boleto Bearer token

Crear tipo de boleto

Crea precio, inventario y modalidad para una función. En TABLE, label_format NUMERIC conserva Mesa 1..N; ALPHANUMERIC exige units_per_row y genera A1..A20, B1... Los UUID y unit_number son identidades estables; label es sólo presentación. Las reservas continúan recibiendo commercial_unit_ids. Cambiar la distribución se bloquea si existen reservas, órdenes o ventas.

Request

{
  "nombre": "Diamante",
  "precio": 6000,
  "cantidad_total": 200,
  "sale_mode": "TABLE",
  "people_per_unit": 4,
  "has_chairs": true,
  "label_format": "ALPHANUMERIC",
  "units_per_row": 20,
  "max_por_orden": null,
  "min_por_orden": 1,
  "zona": "Diamante",
  "color": "#2563eb",
  "sale_phases": [
    {
      "name": "Preventa",
      "price": 350,
      "starts_at": "2026-05-01T06:00:00.000Z",
      "ends_at": "2026-06-01T06:00:00.000Z"
    },
    {
      "name": "Fase 2",
      "price": 450,
      "starts_at": "2026-06-01T06:00:00.000Z",
      "ends_at": "2026-07-20T20:00:00.000Z"
    }
  ]
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "nombre": "General",
    "precio": 450,
    "cargo_servicio": 67.5,
    "cargo_servicio_porcentaje": 15,
    "cantidad_disponible": 200,
    "sale_status": "ACTIVE",
    "effective_price": 350,
    "effective_service_fee": 52.5,
    "active_sale_phase": {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "Preventa",
      "price": 350
    },
    "next_sale_phase": {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "Fase 2",
      "price": 450
    },
    "sale_phases": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "name": "Preventa",
        "phase_order": 1,
        "price": 350
      }
    ]
  }
}
GET /eventos/tipos-boleto/:tipoBoletoId/sale-phases Pública

Consultar fases de venta

Devuelve todas las fases, la activa, la siguiente y sale_status. ACTIVE permite comprar; UPCOMING y ENDED no tienen precio efectivo vendible.

Response

{
  "data": {
    "tipo_boleto_id": "00000000-0000-4000-8000-000000000000",
    "has_sale_phases": true,
    "sale_status": "ACTIVE",
    "active_sale_phase": {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "Preventa",
      "price": 350
    },
    "next_sale_phase": {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "Fase 2",
      "price": 450
    },
    "sale_phases": []
  }
}
PUT /eventos/tipos-boleto/:tipoBoletoId/sale-phases Bearer token

Reemplazar fases de venta

Reemplaza atómicamente las fases de un tipo administrado por el usuario. Enviar sale_phases: [] elimina las fases y restaura el modo de precio fijo.

Request

{
  "sale_phases": [
    {
      "name": "Fase 1",
      "price": 350,
      "starts_at": "2026-05-01T06:00:00.000Z",
      "ends_at": "2026-06-01T06:00:00.000Z"
    }
  ]
}

Response

{
  "data": {
    "tipo_boleto_id": "00000000-0000-4000-8000-000000000000",
    "has_sale_phases": true,
    "sale_status": "UPCOMING",
    "active_sale_phase": null,
    "next_sale_phase": {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "Fase 1",
      "price": 350
    },
    "sale_phases": []
  }
}
PUT /eventos/:id/publicar Bearer token

Publicar evento

Publica un evento en borrador. Debe tener al menos una función activa.

Response

{
  "message": "Evento publicado correctamente"
}
PUT /eventos/:id/cancelar Bearer token

Cancelar evento

Cancela un evento publicado. Si está en borrador, se elimina.

Request

{
  "motivo": "Cambio de fecha"
}

Response

{
  "message": "Evento cancelado correctamente"
}

Grupo

SeatMap Engine

19 rutas

Recintos reutilizables, layouts SVG y disponibilidad por evento sin duplicar geometria.

GET /recintos?estado_id=11&municipio_id=11020 Pública

Listar recintos por ubicacion

Regresa recintos precargados para poblar el selector del admin. Si no hay resultados, el frontend puede usar captura manual.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "nombre": "Palenque de la Feria de Leon",
      "estado_id": "11",
      "municipio_id": "11020",
      "direccion": "C. Olimpo, Zona Recreativa y Cultural, 37500 Leon de los Aldama, Gto.",
      "capacidad_total": 6985,
      "venue_layout_id": "palenque-de-leon",
      "zonas": [
        {
          "categoria": "vip",
          "capacidad": 1210,
          "precio_desde": 1800,
          "precio_hasta": 1800
        }
      ]
    }
  ]
}
GET /venues Pública

Listar recintos

Regresa recintos fisicos reutilizables. El venue no contiene precios, ventas ni disponibilidad.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "nombre": "Palenque de la Feria de León",
      "ciudad": "León de los Aldama",
      "tipo": "palenque",
      "layout_id": "00000000-0000-4000-8000-000000000000"
    }
  ]
}
GET /venues/:id/layout Pública

Obtener layout de recinto

Regresa geometria SVG por seccion y referencias a seat layouts numerados.

Response

{
  "data": {
    "venue": {
      "id": "00000000-0000-4000-8000-000000000000",
      "nombre": "Palenque de la Feria de León"
    },
    "layout": {
      "id": "00000000-0000-4000-8000-000000000000",
      "viewBox": {
        "x": 0,
        "y": 0,
        "width": 1000,
        "height": 1000
      }
    },
    "sections": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "section_id": "VIP-B7",
        "name": "Vip B7",
        "svgPath": "M0 0H100V100H0Z",
        "seatLayoutId": "00000000-0000-4000-8000-000000000000"
      }
    ]
  }
}
GET /eventos/:id/map Pública

Mapa completo de evento

Combina venue, layout reusable, precios y disponibilidad del evento. La geometria nunca se guarda en el evento.

Response

{
  "data": {
    "event": {
      "id": "00000000-0000-4000-8000-000000000000",
      "venue_id": "palenque-de-leon",
      "recinto_id": "00000000-0000-4000-8000-000000000000"
    },
    "venue": {
      "id": "00000000-0000-4000-8000-000000000000",
      "nombre": "Palenque de la Feria de León"
    },
    "layout": {
      "id": "00000000-0000-4000-8000-000000000000"
    },
    "sections": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "availability": {
          "AVAILABLE": 120,
          "SOLD": 10
        },
        "prices": {
          "min_price": 1800,
          "max_price": 1800,
          "moneda": "MXN"
        }
      }
    ]
  }
}
GET /eventos/:id/section/:sectionId Pública

Detalle de seccion

Regresa seat layout, estados de asientos y precios para abrir una seccion numerada o general.

Response

{
  "data": {
    "section": {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "Vip B7"
    },
    "generatedSeats": [
      {
        "seat_id": "00000000-0000-4000-8000-000000000000",
        "seat_code": "VIP-B7:M:701",
        "row": "M",
        "column": 701,
        "x": 0,
        "y": 0
      }
    ],
    "availability": [
      {
        "seat_id": "00000000-0000-4000-8000-000000000000",
        "seat_code": "VIP-B7:M:701",
        "status": "AVAILABLE"
      }
    ],
    "prices": []
  }
}
GET /eventos/:id/section/:sectionId/:subsectionId Pública

Detalle oficial de subseccion

Regresa solamente los asientos de una subseccion oficial del recinto, con geometria backend para pintar posiciones, huecos y bloques separados sin recalcular en frontend. Disponible inicialmente para Diamante A1-A8 del Palenque de Leon.

Response

{
  "data": {
    "section": {
      "section_id": "diamante",
      "name": "Diamante"
    },
    "subsection": {
      "id": "A6",
      "name": "Diamante A6",
      "seatCount": 28,
      "available": 27,
      "layout": {
        "grid": {
          "rows": 10,
          "columns": 15,
          "cellSize": 34,
          "gap": 8
        },
        "stage": {
          "label": "Escenario",
          "gridRow": 9,
          "startColumn": 1,
          "endColumn": 15
        }
      }
    },
    "seats": [
      {
        "seat_id": "00000000-0000-4000-8000-000000000000",
        "seat_code": "diamante:F:601",
        "row": "F",
        "seat_number": 601,
        "estado": "AVAILABLE",
        "disponibilidad": true,
        "precio": 2200,
        "grid_row": 2,
        "grid_column": 2,
        "x": 42,
        "y": 42
      }
    ],
    "prices": [
      {
        "tipo_boleto_id": "00000000-0000-4000-8000-000000000000",
        "precio": 2200,
        "moneda": "MXN"
      }
    ]
  }
}
POST /eventos/:id/reserve Bearer token

Reservar asientos

Crea holds temporales independientes del layout. Conserva el contrato anterior; para una experiencia sin mapa acepta event_section_price_id y quantity representa personas o mesas según event-forms.

Request

{
  "funcion_id": "uuid opcional",
  "section_id": "uuid para mapa",
  "event_section_price_id": "uuid para evento sin mapa",
  "commercial_unit_ids": [
    "uuid de Mesa 7"
  ],
  "quantity": "personas; para TABLE se deriva de commercial_unit_ids",
  "seats": [
    {
      "seat_id": "00000000-0000-4000-8000-000000000000"
    }
  ],
  "hold_minutes": 15
}

Response

{
  "data": {
    "hold_token": "uuid",
    "reserved_until": "2026-07-03T18:15:00.000Z",
    "sale_mode": "TABLE opcional",
    "people_per_unit": 6,
    "total_capacity": 12,
    "commercial_units": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "label": "Mesa 7",
        "unit_number": 7,
        "capacity": 6
      }
    ],
    "pricing": {
      "currency": "MXN",
      "price_per_unit": 6000,
      "subtotal": 12000,
      "service_fee": 1800,
      "total": 13800
    }
  }
}
POST /eventos/:id/release Bearer token

Liberar reserva

Libera asientos reservados por el usuario autenticado usando hold_token o lista de asientos.

Request

{
  "hold_token": "uuid"
}

Response

{
  "data": {
    "released": [
      "VIP-B7:M:701"
    ]
  }
}
POST /eventos/:id/replace-hold Bearer token

Reemplazar completamente un hold numerado

Operacion atomica, multiseccion y exclusiva para asientos numerados. El conjunto enviado se convierte en el estado final completo del hold. Requiere el token cuando ya existe un hold activo; un hold ligado a una orden es inmutable. seats: [] con token libera el hold numerado. Errores de contrato usan 400 y conflictos de token, inventario, expiracion u orden usan 409.

Request

{
  "hold_token": "uuid opcional",
  "seats": [
    {
      "seat_id": "00000000-0000-4000-8000-000000000000",
      "section_code": "oro"
    }
  ],
  "hold_minutes": 15
}

Response

{
  "data": {
    "hold_token": "uuid",
    "reserved_until": "2026-07-03T18:15:00.000Z",
    "seats": [
      {
        "seat_id": "00000000-0000-4000-8000-000000000000",
        "seat_code": "oro:Q:1101",
        "section_code": "oro",
        "row": "Q",
        "number": "1101",
        "status": "RESERVED",
        "reserved_until": "2026-07-03T18:15:00.000Z"
      }
    ]
  }
}
POST /reservas Bearer token

Crear orden desde un hold de asientos

Flujo estandar de reservas: con evento_id y hold_token crea (o reutiliza) la orden pendiente del hold vigente. ticket_holder_names es opcional y, cuando llega, sólo su primer nombre identifica al comprador de toda la orden; no se exige un nombre por mesa o boleto. Si se omite, el backend usa el nombre completo del usuario autenticado. Clientes anteriores pueden seguir enviando varios nombres, pero los boletos nuevos usarán el primero en todos los accesos. El pago se inicia despues con POST /pagos/tarjeta o POST /pagos/oxxo, igual que cualquier otra orden.

Request

{
  "evento_id": "00000000-0000-4000-8000-000000000000",
  "hold_token": "uuid",
  "ticket_holder_names": [
    "Leonardo López"
  ],
  "form_submission": {
    "form_id": "00000000-0000-4000-8000-000000000000",
    "answers": [
      {
        "field_id": "00000000-0000-4000-8000-000000000000",
        "value": [
          {
            "value": "vodka",
            "quantity": 3
          },
          {
            "value": "whisky",
            "quantity": 2
          }
        ]
      }
    ]
  },
  "general_form_submission": {
    "form_id": "00000000-0000-4000-8000-000000000000",
    "answers": [
      {
        "field_id": "00000000-0000-4000-8000-000000000000",
        "value": {
          "quantity": 2
        }
      }
    ]
  }
}

Response

{
  "data": {
    "orden_id": "00000000-0000-4000-8000-000000000000",
    "numero_orden": "KST-ABC123",
    "subtotal": 1400,
    "cargo_servicio": 210,
    "descuento": 0,
    "addons_subtotal": 200,
    "total": 1810,
    "expires_at": "2026-07-03T18:15:00.000Z",
    "es_cortesia": false
  }
}
GET /event-forms/function/:funcionId/section/:sectionId Pública

Formulario y modalidad comercial de una sección

Conserva fields para preguntas y extras. commercial_config es null en secciones normales y contiene PERSON/TABLE, precio canónico, unidades disponibles y capacidad por unidad en secciones configuradas. TABLE siempre publica max_units_per_order como null: su límite es la disponibilidad real de commercial_units. En cortesías PER_UNIT, quantityPerScope se multiplica por las mesas del hold y maxSelections se interpreta por unidad de alcance, no como límite global de la orden.

Response

{
  "success": true,
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "commercial_config": {
      "sale_mode": "TABLE",
      "people_per_unit": 6,
      "has_chairs": true,
      "inventory_units": 20,
      "available_units": 20,
      "price_per_unit": 8500,
      "min_units_per_order": 1,
      "max_units_per_order": null,
      "label_format": "ALPHANUMERIC",
      "units_per_row": 20
    },
    "fields": [
      {
        "field_type": "checkbox",
        "required": true,
        "config": {
          "createsAddon": true,
          "includedBenefit": true,
          "benefitMode": "ELECTIVE",
          "benefitScope": "PER_UNIT",
          "quantityPerScope": 1,
          "maxSelections": 1,
          "options": [
            {
              "label": "José Cuervo",
              "value": "jose_cuervo",
              "price": 0
            },
            {
              "label": "Whisky",
              "value": "whisky",
              "price": 0
            }
          ]
        }
      }
    ]
  }
}
GET /event-forms/function/:funcionId/commercial-section/:eventSectionPriceId Pública

Formulario de una sección comercial sin mapa

Resuelve la experiencia por event_section_prices.id sin consultar seat_map_sections. Devuelve la misma configuración, campos dinámicos y addons del flujo existente.

Response

{
  "success": true,
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "source": "TICKET_COMMERCIAL_SECTION",
    "event_section_price_id": "00000000-0000-4000-8000-000000000000",
    "commercial_section": {
      "id": "00000000-0000-4000-8000-000000000000",
      "source": "TICKET_COMMERCIAL_SECTION",
      "label": "Mesa VIP",
      "ticket_type_id": "00000000-0000-4000-8000-000000000000"
    },
    "commercial_config": {
      "sale_mode": "TABLE",
      "people_per_unit": 6,
      "has_chairs": false,
      "label_format": "ALPHANUMERIC",
      "units_per_row": 20
    },
    "fields": []
  }
}
GET /event-forms/function/:funcionId/general-extras Pública

Extras generales de preventa

Devuelve campos createsAddon disponibles para cualquier comprador de la función, sin depender de su sección.

Response

{
  "success": true,
  "data": {
    "source": "FUNCTION_GENERAL",
    "fields": []
  }
}
GET /event-forms/function/:funcionId Bearer token

Listar formularios editables de una función

Admin recibe SECTION y FUNCTION activos e inactivos. Event manager conserva acceso a sus SECTION, pero los FUNCTION se excluyen de su respuesta.

Response

{
  "success": true,
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "funcion_id": "00000000-0000-4000-8000-000000000000",
      "scope_type": "FUNCTION",
      "section_id": null,
      "event_section_price_id": null,
      "source": "FUNCTION_GENERAL",
      "activo": true,
      "fields": []
    }
  ],
  "total": 1
}
GET /event-forms/:id Bearer token

Obtener formulario editable por ID

Devuelve todos los campos, incluidos los desactivados. Los formularios FUNCTION son exclusivamente administrativos; event_manager recibe 403.

Response

{
  "success": true,
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "scope_type": "FUNCTION",
    "source": "FUNCTION_GENERAL",
    "activo": true,
    "version": 1,
    "fields": []
  }
}
POST /event-forms Bearer token

Crear formulario global de extras

Sólo admin puede crear FUNCTION. extras acepta el contrato simplificado; backend deriva key, tipo, orden y configuración técnica. fields sigue disponible para compatibilidad. No admite commercial_config ni cortesías.

Request

{
  "funcion_id": "00000000-0000-4000-8000-000000000000",
  "scope_type": "FUNCTION",
  "nombre": "Extras globales",
  "extras": [
    {
      "name": "Cerveza",
      "price": 100,
      "description": "Opcional",
      "maxQuantity": 20
    }
  ]
}

Response

{
  "success": true,
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "scope_type": "FUNCTION",
    "source": "FUNCTION_GENERAL",
    "activo": true,
    "version": 1
  },
  "message": "Formulario creado correctamente"
}
PUT /event-forms/:id Bearer token

Actualizar o reactivar formulario global

Sólo admin puede actualizar o reactivar FUNCTION. extras permite editar por id sin desactivar los extras omitidos. fields conserva la semántica completa anterior.

Request

{
  "activo": true,
  "extras": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "Cerveza",
      "price": 120,
      "maxQuantity": 20
    }
  ]
}

Response

{
  "success": true,
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "scope_type": "FUNCTION",
    "activo": true,
    "version": 2
  },
  "message": "Formulario actualizado correctamente"
}
DELETE /event-forms/:id Bearer token

Desactivar formulario global

Sólo admin puede desactivar FUNCTION. Es lógica e idempotente; no elimina respuestas, addons ni snapshots históricos.

Response

{
  "success": true,
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "activo": false
  },
  "message": "Formulario desactivado correctamente"
}
GET /eventos/funciones/:funcionId/commercial-sections/:eventSectionPriceId/units Pública

Mesas concretas de una sección comercial

Lista id, label, unit_number, capacity y status. Estados posibles: AVAILABLE, RESERVED, SOLD, BLOCKED y DISABLED. Un RESERVED vencido se expone como AVAILABLE; los demas estados se conservan.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "Mesa 7",
      "unit_number": 7,
      "capacity": 6,
      "status": "AVAILABLE"
    }
  ]
}

Grupo

Ubicaciones

2 rutas

Catálogo público de estados y municipios para normalizar venues de eventos.

GET /ubicaciones/estados Pública

Listar estados

Regresa entidades federativas con clave oficial para poblar selects.

Response

{
  "data": [
    {
      "id": "22",
      "nombre": "Querétaro",
      "clave": "22"
    }
  ]
}
GET /ubicaciones/estados/:estadoId/municipios Pública

Listar municipios por estado

Regresa municipios del estado indicado con clave oficial.

Response

{
  "data": [
    {
      "id": "22014",
      "estado_id": "22",
      "nombre": "Querétaro",
      "clave": "014"
    }
  ]
}

Grupo

Categorías

1 rutas

Catálogo público para clasificar eventos.

GET /categorias Pública

Listar categorías

Regresa categorías activas para filtros y formularios.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "nombre": "Conciertos",
      "slug": "conciertos"
    }
  ]
}

Grupo

Organizadores

6 rutas

Solicitudes para convertirse en organizador y revision por administradores.

POST /organizadores/solicitar Bearer token

Solicitar organizador

Recibe multipart/form-data. rfc_file es obligatorio, solo admite PDF de hasta 5 MB. Acepta company_name, description, company_phone, contact_email y website, además de sus nombres históricos en español. El campo de texto rfc queda deprecado y es opcional. No establezcas Content-Type manualmente: el cliente debe agregar el boundary.

Request

{
  "formData": {
    "company_name": "Kustika Producciones",
    "rfc_file": "constancia-rfc.pdf (application/pdf, máximo 5 MB)",
    "description": "Productora de eventos",
    "company_phone": "5512345678",
    "contact_email": "contacto@kustika.com",
    "website": "(opcional) https://kustika.com"
  }
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "status": "pendiente",
    "nombre_empresa": "Kustika Producciones",
    "rfc_file": {
      "url": "/organizadores/solicitudes/:id/rfc",
      "name": "constancia-rfc.pdf",
      "mime_type": "application/pdf",
      "size": 123456,
      "uploaded_at": "2026-06-18T00:00:00.000Z"
    }
  }
}
GET /organizadores/mi-solicitud Bearer token

Ver mi solicitud

Muestra el estado de la solicitud del usuario autenticado.

Response

{
  "data": "Solicitud más reciente o null si el usuario aún no tiene solicitudes"
}
GET /organizadores/solicitudes?status=pendiente Admin

Listar solicitudes

Permite al admin revisar solicitudes, opcionalmente filtradas por status.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "pendiente",
      "nombre_empresa": "Evenxa Producciones",
      "rfc_file": {
        "url": "/organizadores/solicitudes/:id/rfc",
        "name": "constancia-rfc.pdf",
        "mime_type": "application/pdf",
        "size": 123456
      }
    }
  ]
}
GET /organizadores/solicitudes/:id/rfc Bearer token

Ver o descargar RFC

Devuelve el PDF con acceso autenticado. Un usuario solo puede consultar su propio documento; un admin puede revisar cualquier solicitud.

Response

"application/pdf"
PUT /organizadores/solicitudes/:id/aprobar Admin

Aprobar solicitud

Convierte la solicitud en organizador aprobado.

Response

{
  "message": "Solicitud aprobada correctamente"
}
PUT /organizadores/solicitudes/:id/rechazar Admin

Rechazar solicitud

Marca una solicitud como rechazada y guarda el motivo.

Request

{
  "motivo": "Falta documentación fiscal"
}

Response

{
  "message": "Solicitud rechazada correctamente"
}

Grupo

Uploads

2 rutas

Carga y lectura de archivos usados por la plataforma.

POST /uploads/imagen Bearer token

Subir imagen

Recibe multipart/form-data con el archivo en el campo file. Máximo 10MB.

Request

{
  "formData": {
    "file": "imagen.jpg"
  }
}

Response

{
  "data": {
    "url": "http://127.0.0.1:3000/uploads/local/eventos/imagen.jpg"
  }
}
GET /uploads/local/* Pública

Ver archivo local

Sirve un archivo guardado localmente por la API.

Response

"Contenido binario del archivo"

Grupo

Sorteos

3 rutas

Landing y administración simple de sorteos.

GET /sorteos Pública

Listar sorteos publicos

Regresa sorteos visibles para la landing.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "title": "Viaje a festival",
      "ticketPrice": 99,
      "featured": true
    }
  ]
}
GET /sorteos/:id Pública

Detalle de sorteo

Obtiene la información completa de un sorteo.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "title": "Viaje a festival",
    "status": "hot",
    "entries": "1200"
  }
}
POST /sorteos/:id/reservas Bearer token

Reservar boletos de sorteo

Crea una orden pendiente de sorteo para iniciar pago con Stripe.

Request

{
  "cantidad": 5
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "orden_id": "00000000-0000-4000-8000-000000000000",
    "reserva_id": "00000000-0000-4000-8000-000000000000",
    "expira_en": "2026-06-17T23:59:00.000Z"
  }
}

Grupo

Boletos

2 rutas

Boletos del usuario con identidad UUID y datos visuales estructurados del asiento.

GET /boletos/mis-boletos Bearer token

Listar mis boletos

seat_id es el UUID canónico. seat_code es únicamente la etiqueta visual; fila, número, sección y subsección se entregan sin interpretar identificadores.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "seat_id": "00000000-0000-4000-8000-000000000000",
      "seat_code": "oro:Q:1101",
      "row_label": "Q",
      "seat_number": "1101",
      "section_name": "Oro",
      "subsection_name": "Oro C11",
      "commercial_details": {
        "commercial_units": [
          {
            "label": "Mesa 7",
            "capacity": 6
          }
        ],
        "complimentary_items": [
          {
            "label": "Cortesia",
            "quantity": 1
          }
        ],
        "extras": [
          {
            "label": "Cerveza",
            "quantity": 2,
            "unit_price": 100,
            "subtotal": 200
          }
        ],
        "pricing_breakdown": {
          "base_subtotal": 6000,
          "service_fee": 900,
          "discount": 0,
          "extras_subtotal": 200,
          "total": 7100
        }
      }
    }
  ]
}
GET /boletos/:id Bearer token

Consultar un boleto

Los boletos de admisión general devuelven seat_id, seat_code, row_label y seat_number como null.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "seat_id": "00000000-0000-4000-8000-000000000000",
    "seat_code": "oro:Q:1101",
    "row_label": "Q",
    "seat_number": "1101",
    "section_name": "Oro",
    "subsection_name": "Oro C11",
    "commercial_details": {
      "commercial_units": [
        {
          "label": "Mesa 7",
          "capacity": 6
        }
      ],
      "complimentary_items": [
        {
          "label": "Cortesia",
          "quantity": 1
        }
      ],
      "extras": [
        {
          "label": "Cerveza",
          "quantity": 2,
          "unit_price": 100,
          "subtotal": 200
        }
      ],
      "pricing_breakdown": {
        "base_subtotal": 6000,
        "service_fee": 900,
        "discount": 0,
        "extras_subtotal": 200,
        "total": 7100
      }
    }
  }
}

Grupo

Staff

9 rutas

Invitaciones, membresías y administración de staff por organizador.

POST /event-manager/staff/invitaciones Bearer token

Invitar staff

Envía invitaciones de siete días. Sólo event_manager.

Request

{
  "emails": [
    "staff@correo.com"
  ]
}

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "email": "staff@correo.com",
      "estado": "pendiente",
      "expires_at": "2026-06-29T23:59:59.000Z"
    }
  ]
}
GET /event-manager/staff/invitaciones Bearer token

Listar invitaciones

Lista únicamente invitaciones del organizador autenticado.

Response

{
  "data": []
}
POST /event-manager/staff/invitaciones/:id/reenviar Bearer token

Reenviar invitación

Invalida el token anterior y renueva el vencimiento.

Response

{
  "data": {
    "estado": "pendiente"
  }
}
DELETE /event-manager/staff/invitaciones/:id Bearer token

Cancelar invitación

Cancela una invitación pendiente sin borrar historial.

Response

{
  "data": {
    "estado": "cancelada"
  }
}
GET /event-manager/staff Bearer token

Listar staff

Lista membresías del organizador autenticado.

Response

{
  "data": []
}
DELETE /event-manager/staff/:membershipId Bearer token

Desactivar staff

Desactiva la membresía sin borrar historial.

Response

{
  "data": {
    "message": "Integrante de staff desactivado"
  }
}
GET /staff/invitaciones/:token Pública

Consultar invitación

Devuelve datos mínimos para presentar el registro o login.

Response

{
  "data": {
    "valida": true,
    "organizador": "Kustika",
    "email": "st***@correo.com"
  }
}
POST /staff/invitaciones/:token/aceptar Bearer token

Aceptar invitación

Asigna rol y membresía si el correo verificado coincide.

Response

{
  "data": {
    "message": "Invitación aceptada",
    "role": "staff"
  }
}
GET /admin/event-managers/staff Admin

Vista global de staff

Agrupa event managers, organizadores, staff e invitaciones pendientes.

Response

{
  "data": []
}

Grupo

Analytics

2 rutas

Registro anónimo de navegación y métricas agregadas para administración.

POST /analytics/events Pública

Registrar una vista de página

Acepta page_view anónimo, deduplica por event_id y no almacena IP ni datos personales.

Request

{
  "event_id": "00000000-0000-4000-8000-000000000000",
  "visitor_id": "00000000-0000-4000-8000-000000000000",
  "session_id": "00000000-0000-4000-8000-000000000000",
  "event": "page_view",
  "path": "/eventos/concierto",
  "referrer": "https://www.google.com/"
}

Response

{
  "data": {
    "accepted": true
  }
}
GET /admin/analytics?period=30d&granularity=day Admin

Consultar métricas de navegación

Devuelve pageviews, visitantes anónimos, sesiones, páginas principales y serie temporal.

Response

{
  "data": {
    "timezone": "America/Mexico_City",
    "summary": {
      "pageviews": 0,
      "unique_visitors": 0,
      "sessions": 0,
      "views_per_session": 0
    },
    "top_pages": [
      {
        "path": "/",
        "pageviews": 0,
        "unique_visitors": 0
      }
    ],
    "timeline": [
      {
        "period": "2026-08-04",
        "pageviews": 0,
        "visitors": 0,
        "sessions": 0
      }
    ]
  }
}

Grupo

Admin

7 rutas

Acciones protegidas para usuarios administradores.

GET /admin/usuarios Admin

Listar usuarios

Lista usuarios registrados para administración.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "email": "ana@evenxa.com",
      "roles": [
        "user"
      ]
    }
  ]
}
GET /admin/usuarios/metricas?period=30d&granularity=day Admin

Métricas globales de usuarios

Devuelve resumen, crecimiento, distribución por rol, serie temporal y registros recientes en America/Mexico_City.

Response

{
  "data": {
    "timezone": "America/Mexico_City",
    "summary": {
      "total": 0,
      "registered_today": 0,
      "active": 0,
      "verified": 0
    },
    "growth": {
      "current": 0,
      "previous": 0,
      "absolute": 0,
      "percentage": null
    },
    "by_role": [
      {
        "role": "customer",
        "count": 0
      }
    ],
    "timeline": [
      {
        "period": "2026-08-04",
        "count": 0
      }
    ],
    "recent_users": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "name": "Ana López",
        "role": "customer",
        "created_at": "2026-08-04T17:00:00.000Z"
      }
    ]
  }
}
PUT /admin/usuarios/:id/rol Admin

Cambiar rol

Actualiza el rol de un usuario.

Request

{
  "role": "event_manager"
}

Response

{
  "message": "Rol actualizado correctamente"
}
GET /admin/sorteos Admin

Listar sorteos admin

Lista todos los sorteos para administración.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "title": "Viaje a festival",
      "featured": true
    }
  ]
}
POST /admin/sorteos Admin

Crear sorteo

Crea un sorteo para mostrar en la landing.

Request

{
  "title": "Viaje a festival",
  "subtitle": "Todo incluido",
  "description": "Participa por una experiencia completa.",
  "ticketPrice": 99,
  "entries": "1200",
  "endsIn": "5 días",
  "status": "hot",
  "image": "https://example.com/sorteo.jpg",
  "featured": true
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "title": "Viaje a festival",
    "featured": true
  }
}
PUT /admin/sorteos/:id Admin

Editar sorteo

Actualiza uno o mas campos de un sorteo.

Request

{
  "featured": false,
  "status": "limited"
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "status": "limited",
    "featured": false
  }
}
DELETE /admin/sorteos/:id Admin

Eliminar sorteo

Elimina un sorteo existente.

Response

{
  "message": "Sorteo eliminado correctamente"
}