kustika 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
26 grupos
143 rutas

Grupo

Estrategia de ventas

3 rutas

Bloqueo inicial por porcentaje y liberación por venta pagada sobre mesas, palcos y asientos PERSON numerados del evento (general no numerado excluido). Requiere event_manager propietario o admin.

GET /eventos/:eventId/inventory-strategy Bearer token

Estado y desglose de la estrategia

status NOT_CONFIGURED|ACTIVE|INACTIVE. eligible_units es el universo configurado (unidades TABLE/BOX activas y asientos habilitados de ofertas PERSON numeradas activas), no la disponibilidad en vivo. currently_blocked_units y released_units corresponden a la última activación. Los *_breakdown separan tables, boxes y seats y suman los totales.

Response

{
  "data": {
    "enabled": true,
    "status": "ACTIVE",
    "initial_block_percentage": 30,
    "release_per_sale": 2,
    "eligible_units": 26,
    "currently_blocked_units": 7,
    "released_units": 1,
    "eligible_breakdown": {
      "tables": 4,
      "boxes": 2,
      "seats": 20
    },
    "blocked_breakdown": {
      "tables": 1,
      "boxes": 0,
      "seats": 6
    },
    "released_breakdown": {
      "tables": 0,
      "boxes": 1,
      "seats": 0
    },
    "allocation": "FLOOR_TOTAL_LARGEST_REMAINDER_BY_SECTION",
    "activated_at": "ISO-8601",
    "deactivated_at": null
  }
}
POST /eventos/:eventId/inventory-strategy/activate Bearer token

Activar estrategia

Body initial_block_percentage (0-100) y release_per_sale (entero ≥ 1). Elegibles: unidades y asientos libres, sin hold vigente ni orden asociada (aunque el hold haya vencido). Reparto: piso global de total × porcentaje / 100 repartido por sección comercial (oferta) con residuos mayores; dentro de cada sección la selección es aleatoria. El orden de liberación intercala secciones de forma proporcional. 409 si ya hay una activa o STRATEGY_NO_ELIGIBLE_INVENTORY si no hay mesas, palcos ni asientos elegibles.

Response

{
  "data": {
    "enabled": true,
    "status": "ACTIVE"
  }
}
POST /eventos/:eventId/inventory-strategy/deactivate Bearer token

Desactivar estrategia

Libera todos los bloqueos de la activación vigente (release_reason STRATEGY_DISABLED). Cada orden pagada con total > 0 y una sola función libera release_per_sale bloqueos en total, de esa función, por orden de liberación; las cortesías, reintentos y reembolsos no liberan ni vuelven a bloquear. Renumerar o eliminar asientos con bloqueos activos responde 409 NUMBERED_CONFIGURATION_HAS_STRATEGY_BLOCKS.

Response

{
  "data": {
    "enabled": false,
    "status": "INACTIVE"
  }
}

Grupo

Disponibilidad y recuperación PERSON física

2 rutas

Consultas sobre inventario persistido. Gates comerciales cerrados; TABLE/BOX y GENERAL físicos no están habilitados.

GET /eventos/:id/availability Pública

Snapshot de disponibilidad física PERSON

Acepta funcion_id opcional (UUID de la función única). Solo instancia efectiva y ofertas vigentes. sections[] incluye physical_mode, quantity, available, price, service_fee y units para TABLE/BOX; seats[] conserva PERSON. Con estrategia de ventas activa, una unidad TABLE/BOX o un asiento PERSON numerado libre y bloqueado aparece con status STRATEGY_BLOCKED y no cuenta en available (en /map pasa de cantidad_disponible a cantidad_bloqueada; en /section y /section/:sub el asiento lleva STRATEGY_BLOCKED y no suma en available de la subsección); SOLD/RESERVED prevalecen. Taquilla lo muestra como estado bloqueado con bloqueo_estrategia=true. No devuelve geometría ni datos privados. observed_at no garantiza una reserva: reserve revalida. commercial_selection_enabled=false se conserva aun con oferta disponible. 409 PHYSICAL_INSTANCE_REQUIRED o errores existentes de contexto físico.

Response

{
  "data": {
    "map_kind": "VENUE_INSTANCE",
    "event_id": "00000000-0000-4000-8000-000000000000",
    "function_id": "00000000-0000-4000-8000-000000000000",
    "observed_at": "ISO-8601",
    "commercial_selection_enabled": false,
    "seats": [
      {
        "seat_id": "00000000-0000-4000-8000-000000000000",
        "section_id": "00000000-0000-4000-8000-000000000000",
        "seat_code": "A-1",
        "row_label": "A",
        "seat_number": "1",
        "status": "AVAILABLE",
        "tipo_boleto_id": "00000000-0000-4000-8000-000000000000",
        "price": 500,
        "currency": "MXN",
        "min_per_order": 1,
        "max_per_order": 2
      }
    ]
  }
}
GET /eventos/:id/hold Bearer token

Recuperar hold vigente del comprador

Únicamente el hold del JWT autenticado, sin sesiones de taquilla. Sin hold devuelve hold:null. Consulta privada no-store. release rechaza HOLD_LOCKED_BY_ORDER cuando una orden física ya posee el inventario; cancelar mediante el flujo canónico de orden. Pagos físicos PERSON: Stripe test validado, HeyBanco físico bloqueado. Sin apertura de gates.

Response

{
  "data": {
    "event_id": "00000000-0000-4000-8000-000000000000",
    "hold": {
      "hold_token": "00000000-0000-4000-8000-000000000000",
      "reserved_until": "ISO-8601",
      "order_id": null,
      "seats": [
        {
          "seat_id": "00000000-0000-4000-8000-000000000000",
          "section_id": "00000000-0000-4000-8000-000000000000"
        }
      ]
    }
  }
}

Grupo

Ofertas físicas por sección — 4F.4C.1B

2 rutas

Geometría independiente de oferta. PERSON numerado; compras físicas siguen cerradas. Sin migraciones nuevas.

GET /eventos/:id/map Pública

Estado comercial por sección efectiva

Extensión aditiva solo VENUE_INSTANCE, también en /events y preview autorizado. commercial_context.function_id identifica la única función o es null. sections[].commercial_status: NOT_OFFERED sin binding activo; OFFERED con binding PERSON numerado válido, función activa única, capacidad/counters/límites/precio vigente coherentes y evento publicado; UNAVAILABLE con binding activo no elegible, fase no vigente o lifecycle incompatible. No equivale a disponibilidad ni permiso de compra: read_only=true, commercial_selection_enabled=false. No altera spatial_document v1 ni omite geometría. Estados privados y guards conservados. Detalles de sección/subsección mantienen su contrato anterior.

Response

{
  "data": {
    "map_kind": "VENUE_INSTANCE",
    "read_only": true,
    "commercial_selection_enabled": false,
    "commercial_context": {
      "function_id": "00000000-0000-4000-8000-000000000000"
    },
    "sections": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "commercial_status": "NOT_OFFERED"
      }
    ]
  }
}
PUT /eventos/:eventId/funciones/:functionId/secciones/:sectionId/ofertas/:ticketTypeId/desactivar Bearer token

Desactivar oferta física en borrador sin actividad

Sin body o {}. Roles actuales en BD: admin o event_manager propietario. Valida evento, función única, sección UUID efectiva y binding f?sico por modalidad. Solo borrador sin historial de actividad en todo el evento; ningún bypass admin. Transacción con gate exclusivo, preserva binding y geometría, marca tipo inactivo, elimina únicamente proyección AVAILABLE sin compromisos y registra auditoría. PUT monotónico e idempotente mientras siga en estado permitido; no reactiva ni reasigna. Recuperar estado con GET configuracion-recinto y preview/map. 400 identidad/body; 401 JWT; 403 EVENT_FORBIDDEN; 404 EVENT_NOT_FOUND/PHYSICAL_OFFER_NOT_FOUND; 409 PHYSICAL_OFFER_DRAFT_REQUIRED, PHYSICAL_OFFER_HAS_COMMERCIAL_ACTIVITY, PHYSICAL_BINDING_INCOMPATIBLE, PHYSICAL_SECTION_NOT_IN_INSTANCE, INSTANCE_SINGLE_FUNCTION_REQUIRED, PHYSICAL_SALE_MODE_UNSUPPORTED o EVENT_COORDINATION_CONFLICT. Sin cambios de counters, reembolsos ni cancelaciones.

Response

{
  "data": {
    "event_id": "00000000-0000-4000-8000-000000000000",
    "function_id": "00000000-0000-4000-8000-000000000000",
    "section_id": "00000000-0000-4000-8000-000000000000",
    "ticket_type_id": "00000000-0000-4000-8000-000000000000",
    "layout_id": "00000000-0000-4000-8000-000000000000",
    "offer_active": false,
    "commercial_status": "NOT_OFFERED",
    "physical_section_preserved": true,
    "binding_preserved": true,
    "inventory_reconciled": true
  }
}

Grupo

Lectura segura de mapas

2 rutas

4F.3C: geometria publica y preview privado. No habilita comercializacion de plantillas.

GET /eventos/:id/map Pública

Mapa efectivo publico

Alias /events/:id/map. Allowlist: publicado, finalizado, archivado y cancelado; borrador, abandonado, estados desconocidos e inexistentes: 404 EVENT_NOT_FOUND, incluso con JWT. /:id/section/:sectionId y /:id/section/:sectionId/:subsectionId aplican la misma politica. Cache-Control: no-store, max-age=0. funcion_id opcional exige UUID de funcion activa del evento; no cambia la agregacion historica legacy. map_kind LEGACY o VENUE_INSTANCE; intencion/instancia fisica invalida: 409 PHYSICAL_INCOMPLETE sin fallback. Metadata fisica limitada a source, source_version_id, source_venue_id y spatial_document schema_version=1 con UUID efectivos. Lectura READ ONLY; geometria no implica oferta ni disponibilidad. Sin oferta vigente no habilita seleccion fisica.

Response

{
  "data": {
    "map_kind": "VENUE_INSTANCE",
    "read_only": true,
    "commercial_selection_enabled": false,
    "layout": {
      "id": "00000000-0000-4000-8000-000000000000",
      "event_id": "00000000-0000-4000-8000-000000000000",
      "metadata": {
        "source": "VENUE_INSTANCE",
        "source_version_id": "00000000-0000-4000-8000-000000000000",
        "source_venue_id": "00000000-0000-4000-8000-000000000000",
        "spatial_document": {
          "schema_version": 1
        }
      }
    }
  }
}
GET /eventos/:id/preview/map Bearer token

Preview privado autorizado

Alias /events; variantes preview/section/:sectionId y preview/section/:sectionId/:subsectionId. Requiere JWT legitimo y rol actual en BD: admin o event_manager propietario. Sin JWT: 401; sin autorizacion: 404 identico a evento inexistente. No confia en rol del JWT. Mismo DTO minimo y validacion estructural; no devuelve identidad privada. Cache-Control: private, no-store, max-age=0; Vary: Authorization.

Response

{
  "data": {
    "map_kind": "VENUE_INSTANCE",
    "read_only": true,
    "commercial_selection_enabled": false
  }
}

Grupo

Recuperacion de configuracion de recinto

5 rutas

4F.2A: admin o event_manager propietario, con roles comprobados en base de datos. Solo intencion o instancia fisica persistida; venue_id por si solo es legacy. No habilita ventas.

GET /eventos/:id/configuracion-recinto Bearer token

Recuperar configuracion persistida

Sin escrituras. UUID efectivos, funciones, tipos y bindings; no reconstruye recursos. state: INSTANCE_PENDING, FUNCTION_PENDING, TICKET_TYPES_PENDING, CONFIGURATION_INCOMPLETE, PARTIALLY_BOUND, READY, PUBLISHED, ABANDONED o TERMINAL. pending reutiliza el validador canonico de publicacion. can_publish es una instantanea, no garantiza exito futuro. PARTIALLY_BOUND distingue secciones numeradas sin oferta; can_publish puede ser true porque el validador vigente permite ofrecer un subconjunto valido. Las secciones sin binding no implican obligacion comercial nueva. 400 UUID, 403 actor/rol, 404 evento, 409 PHYSICAL_CONFIGURATION_REQUIRED o EVENT_COORDINATION_CONFLICT. Sin paginacion: snapshot completo de un solo evento autorizado.

Response

{
  "data": {
    "event_id": "00000000-0000-4000-8000-000000000000",
    "status": "borrador",
    "state": "INSTANCE_PENDING",
    "venue_id": "00000000-0000-4000-8000-000000000000",
    "requested_venue_layout_version_id": "00000000-0000-4000-8000-000000000000",
    "requested_version": {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "PUBLISHED",
      "version": 1,
      "venue_active": true
    },
    "instance": null,
    "effective_layout": null,
    "inventory_scope": "EVENT_SINGLE_FUNCTION",
    "functions": [],
    "ticket_types": [],
    "bindings": [],
    "physical_sections": [],
    "unbound_section_ids": [],
    "pending": [
      {
        "code": "PHYSICAL_INSTANCE_REQUIRED",
        "message": "El evento de plantilla no esta listo para publicarse"
      }
    ],
    "can_continue": true,
    "can_publish": false,
    "can_abandon": true,
    "readiness_is_snapshot": true
  }
}
GET /eventos/:id/editable Bearer token

Lectura privada para continuar la configuración

Mismo detalle que GET /eventos/:id más editing. Admin, event_manager propietario o customer con su borrador PROSPECTO (flag CUSTOMER_EVENT_DRAFTS_ENABLED). Evento ajeno, inexistente o no legible: 404 EVENT_NOT_FOUND sin revelar existencia. venue_schedule_conflict es informativo para borradores customer: no apartan fechas del recinto y la disponibilidad se revalida al solicitar revisión. can_publish_directly sigue a PUT /eventos/:id/publicar: false para customer, para borradores PROSPECTO y con solicitud pendiente; admin true (ADMIN_DIRECTO); event_manager propietario true solo con event_publication_policy inactiva. Es una instantánea: PUT /publicar revalida.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "status": "borrador",
    "funciones": [],
    "editing": {
      "mode": "customer_draft",
      "can_edit": true,
      "review_pending": false,
      "can_publish_directly": false,
      "can_abandon": true,
      "organizer_status": "PROSPECTO",
      "venue_schedule_conflict": false
    }
  }
}
PUT /eventos/:id/abandonar Bearer token

Abandonar un borrador normal o físico

Sin body. Abandono lógico: status=abandonado y auditoría ABANDONAR_BORRADOR; conserva funciones, boletos, configuración física e historial de revisión, y libera la cuota del customer. Repetir devuelve replayed=true. 403 actor, 404 evento, 409 EVENT_NOT_DRAFT, EVENT_REVIEW_PENDING, EVENT_BUSINESS_HISTORY o EVENT_COORDINATION_CONFLICT. DELETE /eventos/:id con historial de revisión responde 409 EVENT_REVIEW_HISTORY.

Response

{
  "data": {
    "event_id": "00000000-0000-4000-8000-000000000000",
    "status": "abandonado",
    "replayed": false
  }
}
POST /eventos/:id/imagenes Bearer token

Subir imagen de un evento editable

multipart/form-data con un archivo de imagen (JPG, PNG, WebP o GIF; máximo 10MB). Exige poder editar el evento: admin, event_manager propietario o customer con su borrador editable. Mismo procesamiento y almacenamiento que POST /uploads/imagen, que sigue reservado a admin y event_manager. 400 archivo, 403 actor, 404 evento, 409 EVENT_REVIEW_PENDING, 503 almacenamiento.

Response

{
  "data": {
    "url": "http://127.0.0.1:3000/uploads/local/eventos/imagen.webp"
  }
}
PUT /eventos/:id/configuracion-recinto/abandonar Bearer token

Abandonar borrador fisico explicitamente

Sin body. Solo borrador sin historial comercial, guarda status=abandonado y auditoria atomicamente. Repetir devuelve replayed=true sin nueva auditoria. Conserva instancia, UUID, tipos, bindings y recibos. Terminal: no continuar, publicar, editar o cancelar como evento historico. 403 actor, 404 evento, 409 EVENT_NOT_DRAFT, EVENT_BUSINESS_HISTORY, PHYSICAL_CONFIGURATION_REQUIRED o EVENT_COORDINATION_CONFLICT. Salir del asistente no llama esta ruta: conserva borrador. DELETE y cancelar borrador fisico devuelven 409 PHYSICAL_DRAFT_DELETE_PROTECTED.

Response

{
  "data": {
    "event_id": "00000000-0000-4000-8000-000000000000",
    "status": "abandonado",
    "replayed": false
  }
}

Grupo

Solicitudes de publicación y validación empresarial

11 rutas

Envío a revisión administrativa, retiro e historial. Solo el propietario envía; enviar no publica, no cambia roles ni activa perfiles; la decisión administrativa (aprobar o rechazar) lo hace en una sola transacción. Con una solicitud pendiente el evento sigue en borrador y toda escritura de configuración, publicación, cancelación o abandono responde 409 EVENT_REVIEW_PENDING para cualquier actor. La documentación empresarial es independiente del evento y se reutiliza.

POST /eventos/:id/solicitudes-publicacion Bearer token

Enviar el borrador a revisión

JSON { request_id } cuando la documentación está vigente, es legacy aprobada con RFC o hay una validación empresarial pendiente. multipart/form-data con request_id, nombre_empresa, rfc, descripcion, telefono_empresa, email_contacto, sitio_web opcional y rfc_file (PDF, máx. 5 MB) cuando hace falta un nuevo proceso documental. Bajo el gate exclusivo del evento: propietario, borrador, sin pendiente, validación de publicación (incluye la física), calendario del recinto, documentación, huella y snapshot. Idempotente por (usuario, request_id): mismo contenido devuelve replayed=true; otro contenido 409 PUBLICATION_REQUEST_ID_CONFLICT. Errores: 400 BUSINESS_DOCUMENTS_REQUIRED, DOCUMENTATION_NOT_REQUIRED o INVALID_BUSINESS_DOCUMENTATION; 403 PUBLICATION_REQUEST_OWNER_REQUIRED; 404 evento ajeno; 409 PUBLICATION_REQUEST_PENDING, EVENT_NOT_DRAFT, VENUE_SCHEDULE_CONFLICT, BUSINESS_VALIDATION_ALREADY_PENDING, LEGACY_ORGANIZER_REQUEST_PENDING o EVENT_NOT_READY_FOR_PUBLICATION.

Request

{
  "request_id": "00000000-0000-4000-8000-000000000000"
}

Response

{
  "data": {
    "replayed": false,
    "request": {
      "id": "00000000-0000-4000-8000-000000000000",
      "evento_id": "00000000-0000-4000-8000-000000000000",
      "status": "pendiente",
      "origen": "CUSTOMER",
      "solicitud_organizador_id": "00000000-0000-4000-8000-000000000000",
      "solicitud_anterior_id": null,
      "evento_fingerprint": "sha256",
      "submit_request_id": "00000000-0000-4000-8000-000000000000",
      "created_at": "2026-10-28T12:00:00Z"
    }
  }
}
GET /eventos/:id/solicitudes-publicacion Bearer token

Estado de revisión e historial del propietario

review_status (sin_solicitud, pendiente, aprobada, rechazada, retirada), solicitud pendiente, historial reciente primero con motivos de rechazo, estado documental reutilizable y acciones disponibles. Sin snapshot, revisor ni claves de almacenamiento. Evento ajeno: 404.

Response

{
  "data": {
    "evento_id": "00000000-0000-4000-8000-000000000000",
    "review_status": "pendiente",
    "solicitud_pendiente": {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "pendiente"
    },
    "historial": [],
    "documentacion": {
      "estado": "PENDIENTE",
      "reutilizable": true,
      "requiere_documentos": false,
      "modo": "REUTILIZAR_PENDIENTE"
    },
    "acciones": {
      "puede_enviar": false,
      "puede_retirar": true,
      "requiere_documentos": false
    }
  }
}
POST /eventos/:id/solicitudes-publicacion/:requestId/retirar Bearer token

Retirar una solicitud pendiente

Body { request_id } es la clave idempotente de la operación; :requestId identifica la solicitud. Solo el solicitante; bajo el gate exclusivo; retiro y auditoría transaccionales. No borra la fila ni el RFC y el borrador vuelve a ser editable. Repetir devuelve replayed=true sin efectos. 403 PUBLICATION_REQUEST_FORBIDDEN, 404, 409 PUBLICATION_REQUEST_NOT_PENDING u OPERATION_REQUEST_ID_CONFLICT.

Request

{
  "request_id": "00000000-0000-4000-8000-000000000000"
}

Response

{
  "data": {
    "replayed": false,
    "request": {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "retirada"
    }
  }
}
GET /organizadores/validacion-empresarial Bearer token

Estado documental e historial propio

Estado efectivo (VIGENTE, PENDIENTE, VENCIDA, REVOCADA, SIN_VALIDAR o LEGACY), si es reutilizable y el historial de validaciones empresariales. El RFC se descarga en GET /organizadores/solicitudes/:id/rfc (propietario o admin vigente).

Response

{
  "data": {
    "estado": "PENDIENTE",
    "modo": "REUTILIZAR_PENDIENTE",
    "reutilizable": true,
    "requiere_documentos": false,
    "historial": []
  }
}
POST /organizadores/validacion-empresarial/:id/retirar Bearer token

Retirar una validación empresarial pendiente

Solo la propia y pendiente; no borra el RFC. 409 BUSINESS_VALIDATION_IN_USE si una solicitud de publicación pendiente depende de ella. Repetir devuelve replayed=true.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "status": "retirada",
    "replayed": false
  }
}
GET /admin/solicitudes-publicacion?status=pendiente&page=1&limit=20 Admin

Bandeja administrativa

Filtros status, origen, evento_id y solicitante_id; limit máximo 100. Pendientes en orden de llegada.

Response

{
  "data": {
    "solicitudes": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "evento_id": "00000000-0000-4000-8000-000000000000",
        "status": "pendiente",
        "titulo": "Noche Indie",
        "organizador_estado": "PROSPECTO",
        "validacion_empresarial_status": "pendiente"
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20,
    "pages": 1
  }
}
GET /admin/solicitudes-publicacion/:requestId Admin

Detalle para revisión comercial

Snapshot enviado y configuración actual (funciones, tipos con precio y cargo por servicio, fases, secciones, capacidad e instancia física), huella enviada y actual con cambio_desde_envio, organizador y estado documental, solicitante y roles, validación empresarial (sin clave de almacenamiento), conflictos de recinto e historial.

Response

{
  "data": {
    "huella": {
      "enviada": "sha256",
      "actual": "sha256",
      "cambio_desde_envio": false
    },
    "conflictos_recinto": [],
    "historial": []
  }
}
POST /admin/solicitudes-publicacion/:requestId/aprobar Admin

Aprobar y publicar

Una transacción bajo el gate exclusivo del evento: admin vigente en BD, recibo idempotente por request_id, huella revisada (evento_fingerprint) igual a la enviada y a la actual, documentación vigente (o aprobar_documentacion: true con la validación exacta vinculada, vigencia y rfc_revisado: true), validaciones de publicación normales y físicas y disponibilidad del recinto. Perfil PROSPECTO: se activa con los datos validados y la cuenta customer pasa a event_manager. Event_manager: no cambia el rol ni el perfil. El evento queda publicado; cualquier fallo revierte todo. Misma clave y contenido: replayed=true; otro contenido: 409 DECISION_REQUEST_ID_CONFLICT. Errores 409: PUBLICATION_REQUEST_NOT_PENDING, EVENT_NOT_DRAFT, REVIEWED_VERSION_MISMATCH, EVENT_CHANGED_SINCE_REVIEW, BUSINESS_DOCUMENTATION_NOT_VALID, BUSINESS_VALIDATION_MISMATCH, BUSINESS_VALIDATION_NOT_PENDING, VENUE_SCHEDULE_CONFLICT, REQUESTER_ROLE_INCOMPATIBLE, EVENT_NOT_READY_FOR_PUBLICATION; 400 INVALID_DECISION_PAYLOAD o INVALID_DOCUMENTATION_VALIDITY; 403 REVIEW_DECISION_FORBIDDEN.

Request

{
  "request_id": "00000000-0000-4000-8000-000000000000",
  "evento_fingerprint": "sha256",
  "aprobar_documentacion": true,
  "solicitud_organizador_id": "00000000-0000-4000-8000-000000000000",
  "documentacion_vigente_hasta": "2028-06-30",
  "rfc_revisado": true
}

Response

{
  "data": {
    "replayed": false,
    "solicitud": {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "aprobada"
    },
    "evento": {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "publicado"
    },
    "organizador": {
      "estado": "ACTIVO",
      "documentacion_estado": "VIGENTE"
    },
    "solicitante": {
      "roles_anteriores": [
        "customer"
      ],
      "roles": [
        "event_manager"
      ],
      "promovido": true
    },
    "documentacion": {
      "aprobada_en_esta_decision": true,
      "solicitud_organizador_id": "00000000-0000-4000-8000-000000000000"
    }
  }
}
POST /admin/solicitudes-publicacion/:requestId/rechazar Admin

Rechazar una solicitud de publicación

Motivo de 10 a 2000 caracteres. El evento sigue en borrador; rol, perfil y documentación empresarial no cambian. El propietario puede corregir y reenviar (el reenvío se liga a esta solicitud). Idempotente por request_id.

Request

{
  "request_id": "00000000-0000-4000-8000-000000000000",
  "motivo": "La configuración comercial requiere ajustes"
}

Response

{
  "data": {
    "replayed": false,
    "solicitud": {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "rechazada"
    },
    "evento": {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "borrador"
    }
  }
}
POST /admin/validaciones-empresariales/:id/aprobar Admin

Aprobar una validación empresarial

Independiente del evento: verifica documento RFC registrado y RFC del perfil, registra revisor, fecha y vigencia obligatoria (no anterior a hoy; sin plazo máximo impuesto) y deja la documentación del perfil VIGENTE. No promueve el rol, no activa un perfil PROSPECTO ni publica. Una solicitud legacy responde 409 ORGANIZER_REQUEST_IS_LEGACY; nunca usa PUT /organizadores/solicitudes/:id/aprobar.

Request

{
  "request_id": "00000000-0000-4000-8000-000000000000",
  "vigente_hasta": "2028-06-30",
  "rfc_revisado": true
}

Response

{
  "data": {
    "replayed": false,
    "validacion": {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "aprobada"
    },
    "organizador": {
      "estado": "PROSPECTO",
      "documentacion_estado": "VIGENTE"
    }
  }
}
POST /admin/validaciones-empresariales/:id/rechazar Admin

Rechazar una validación empresarial

Motivo de 10 a 2000 caracteres. Conserva RFC y documento, no cambia el rol ni rechaza la solicitud de publicación vinculada; el usuario puede enviar una nueva validación.

Request

{
  "request_id": "00000000-0000-4000-8000-000000000000",
  "motivo": "El RFC adjunto no es legible"
}

Response

{
  "data": {
    "replayed": false,
    "validacion": {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "rechazada"
    }
  }
}

Grupo

Distribuciones elegibles para eventos

2 rutas

Lectura para admin y event_manager, y para customer apto con CUSTOMER_EVENT_DRAFTS_ENABLED=true. Catalogo global antes de crear evento; no concede permisos administrativos. Solo recintos activos y versiones PUBLISHED.

GET /recintos/:id/layout-versions Bearer token

Consultar versiones seleccionables

page=1, limit=20 (maximo 100). evento_id opcional: exige evento borrador propio o admin, y recinto compatible. Sin evento_id es un catalogo compartido, no acceso a eventos ajenos. No expone auditoria ni borradores. La elegibilidad se vuelve a comprobar al instanciar.

Response

{
  "data": {
    "versions": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "venue_id": "00000000-0000-4000-8000-000000000000",
        "version": 1,
        "status": "PUBLISHED",
        "published_at": "2026-10-17T12:00:00Z"
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20,
    "pages": 1
  }
}
GET /recintos/:id/layout-versions/:versionId Bearer token

Previsualizar distribucion publicada elegible

Mismos permisos y evento_id opcional. Documento espacial publicado y resumen estructural sin doble contar sillas de grupos; no es disponibilidad comercial. 404 para origen inactivo, borrador, otro recinto o recinto inactivo. No incluye created_by, updated_by, claves de operacion o auditoria.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "venue_id": "00000000-0000-4000-8000-000000000000",
    "status": "PUBLISHED",
    "version": 1,
    "published_at": "2026-10-17T12:00:00Z",
    "document": {},
    "summary": {
      "section_count": 1,
      "subsection_count": 0,
      "seat_count": 4,
      "table_count": 1,
      "box_count": 0,
      "capacity_total": 4
    }
  }
}

Grupo

Instancias de recinto por evento

2 rutas

Admin o event_manager propietario. Copia independiente desde versión PUBLISHED de recinto activo a evento borrador sin actividad. No genera inventario ni tipos de boleto; no reemplaza distribuciones existentes.

POST /eventos/:id/recinto-instance Bearer token

Instanciar una versión publicada

Operación atómica. UUID nuevos; source_version_id y request_id obligatorios. Mismo request_id y versión devuelve 200/replayed=true; primera creación 201. Otro payload o instancia existente devuelve 409. El recinto debe coincidir con el seleccionado o el evento no tener recinto. No permite eventos publicados, configuración comercial, inventario, holds, órdenes o boletos.

Request

{
  "source_version_id": "00000000-0000-4000-8000-000000000000",
  "request_id": "00000000-0000-4000-8000-000000000000"
}

Response

{
  "data": {
    "event_id": "00000000-0000-4000-8000-000000000000",
    "venue_id": "00000000-0000-4000-8000-000000000000",
    "source_version_id": "00000000-0000-4000-8000-000000000000",
    "effective_layout_id": "00000000-0000-4000-8000-000000000000",
    "status": "INSTANTIATED",
    "request_id": "00000000-0000-4000-8000-000000000000",
    "replayed": false,
    "identity_maps": {
      "sections": {},
      "subsections": {},
      "seats": {},
      "elements": {}
    }
  }
}
GET /eventos/:id/recinto-instance Bearer token

Consultar origen e identidad de instancia

Mismos permisos de gestión. 404 si no existe instancia. Conserva referencias aunque se desactive el origen.

Response

{
  "data": {
    "event_id": "00000000-0000-4000-8000-000000000000",
    "venue_id": "00000000-0000-4000-8000-000000000000",
    "source_version_id": "00000000-0000-4000-8000-000000000000",
    "effective_layout_id": "00000000-0000-4000-8000-000000000000",
    "status": "INSTANTIATED",
    "identity_maps": {
      "sections": {},
      "subsections": {},
      "seats": {},
      "elements": {}
    }
  }
}

Grupo

Distribuciones de autoría

10 rutas

Solo admin. Autoría JSONB aislada: no modifica layouts legacy ni inventario. DRAFT → PUBLISHED → INACTIVE. Versiones publicadas inmutables; INACTIVE conserva contenido e identidad. Body máximo 8 MiB, 1000 operaciones/lote, 30 solicitudes/minuto. 4E.1C: nuevas ediciones/publicaciones comprueban contención completa de asientos y grupos en sección/subsección y de grupos en BOX padre. POLYGON/PATH nuevos admiten geometry.coordinate_space=LOCAL_UNIT (puntos/comandos en caja 0..1, escala por width/height, traslado x/y, rotación alrededor del centro). Sin marcador conserva proyección absoluta histórica; no se mueve/escala un trazado histórico implícitamente. PATH físico requiere un solo contorno cerrado simple, sin huecos; decoración abierta permitida. Transformar padres no mueve hijos automáticamente: coordinar el lote o se rechaza. Lecturas históricas se conservan; copiar o instanciar un origen inválido devuelve 409 INVALID_SOURCE_DOCUMENT. TABLE/BOX representa la huella completa de ocupación, incluidas sus sillas: todo asiento con group_id debe quedar íntegramente dentro del grupo, incluso deshabilitado; incumplimiento CHILD_OUTSIDE_CONTAINER. Respuestas {data: ...}; 400 validación con details de elemento/contenedor, 401/403 permisos, 404 identidad/pertenencia, 409 revisión/estado/idempotencia, 413 tamaño, 422 publicación inválida.

POST /admin/recintos/:id/layout-drafts Admin

Crear borrador único

201. Body {} o source_version_id de versión publicada/inactiva del mismo recinto. Copia independiente, revisión 1, sin número de versión. Nunca importa layouts legacy ni cambia venues.layout_id.

Request

{}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "venue_id": "00000000-0000-4000-8000-000000000000",
    "status": "DRAFT",
    "revision": 1,
    "version": null,
    "document": {
      "schema_version": 1,
      "view_box": {
        "x": 0,
        "y": 0,
        "width": 1200,
        "height": 1200
      },
      "sections": [],
      "subsections": [],
      "seats": [],
      "elements": []
    }
  }
}
GET /admin/recintos/:id/layout-drafts Admin

Consultar borrador activo

Documento completo o data:null si no existe borrador. Recinto inexistente: 404.

Response

{
  "data": null
}
GET /admin/layout-drafts/:id Admin

Consultar documento editable

Identidad, recinto, origen, revisión, fechas y documento completo. Si ya fue publicado: 409; consultar por historial.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "status": "DRAFT",
    "revision": 1,
    "document": {}
  }
}
PATCH /admin/layout-drafts/:id Admin

Aplicar lote atómico

expected_revision y request_id UUID obligatorios. Colecciones sections/subsections/seats/elements; operaciones create, update, transform, delete, duplicate y view_box. Geometría x/y/width/height/rotation/shape; RECTANGLE, ELLIPSE, POLYGON o PATH. Máximos 500 secciones, 2000 subsecciones, 30000 asientos, 5000 elementos. Campos desconocidos y referencias ajenas rechazados. GENERAL físico: tipo='general', nombre saneado no vacío y capacidad entera de 1 a 1000000 desde borrador; no admite asientos, TABLE ni BOX asociados. Coexiste con numbered; no genera inventario. Cambiar de tipo con unidades presentes devuelve 400 INVALID_LAYOUT sin borrarlas. Otros campos incompletos conservan advertencias. Repetir el último request_id y payload devuelve replay sin incrementar revisión; payload distinto: 409.

Request

{
  "expected_revision": 1,
  "request_id": "00000000-0000-4000-8000-000000000000",
  "operations": [
    {
      "op": "view_box",
      "value": {
        "x": 0,
        "y": 0,
        "width": 1500,
        "height": 1200
      }
    }
  ]
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "revision": 2,
    "results": [
      {
        "index": 0
      }
    ],
    "validation": {
      "valid": true,
      "errors": [],
      "warnings": []
    },
    "replayed": false
  }
}
POST /admin/layout-drafts/:id/numbering-preview Admin

Previsualizar numeración sin guardar

Selección ordenada de hasta 1000 seat_ids. AUTO: start/step no nulo, skip_numbers, prefix/suffix. MANUAL: labels por asiento. row_label opcional o row_index (0=A, 26=AA). Devuelve operaciones que se aplican mediante PATCH; conserva UUID y seat_code.

Request

{
  "expected_revision": 2,
  "seat_ids": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "mode": "AUTO",
  "start": 10,
  "step": -2,
  "row_label": "A"
}

Response

{
  "data": {
    "revision": 2,
    "operations": [],
    "validation": {
      "valid": true
    }
  }
}
POST /admin/layout-drafts/:id/validate Admin

Validar para publicación

Solo lectura. Revisión obligatoria. Comprueba referencias, códigos/etiquetas por ámbito, capacidades sin doble conteo, agrupaciones acíclicas y geometría. Hasta 200 detalles por categoría con contadores totales.

Request

{
  "expected_revision": 2
}

Response

{
  "data": {
    "revision": 2,
    "valid": false,
    "errors": [
      {
        "element_id": null,
        "field": "sections",
        "code": "EMPTY_LAYOUT",
        "message": "Agrega al menos una sección"
      }
    ],
    "warnings": [],
    "error_count": 1,
    "warning_count": 0,
    "capacity_total": 0,
    "sections": []
  }
}
POST /admin/layout-drafts/:id/publish Admin

Publicar versión inmutable

Recinto activo y distribución válida. Lock por recinto/documento, versión única incremental y revisión +1. idempotency_key con la revisión original permite reintento. No hace la distribución seleccionable en compra: integración futura.

Request

{
  "expected_revision": 2,
  "idempotency_key": "00000000-0000-4000-8000-000000000000"
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "status": "PUBLISHED",
    "version": 1,
    "revision": 3,
    "replayed": false
  }
}
GET /admin/recintos/:id/layout-versions Admin

Historial de versiones

Publicadas e inactivas. page=1, limit=20 (máximo 100). Orden descendente por versión. Metadatos sin árboles completos.

Response

{
  "data": {
    "versions": [],
    "total": 0,
    "page": 1,
    "limit": 20,
    "pages": 0
  }
}
GET /admin/recintos/:id/layout-versions/:versionId Admin

Consultar versión histórica

versionId es UUID. Documento completo de versión publicada o inactiva perteneciente al recinto; sin modificación.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "status": "INACTIVE",
    "version": 1,
    "revision": 4,
    "document": {}
  }
}
POST /admin/layout-versions/:id/deactivate Admin

Desactivar versión conservando historia

Revisión obligatoria. Solo cambia estado/revisión/metadatos; nunca contenido, versión o fecha de publicación. Una versión inactiva sigue consultable y puede servir de origen a otro borrador. Sin reactivación en esta fase.

Request

{
  "expected_revision": 3
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "status": "INACTIVE",
    "version": 1,
    "revision": 4
  }
}

Grupo

Administración de recintos

5 rutas

JWT con rol admin. Datos generales de venues; no crea ni modifica planos o inventario. Respuestas {data: ...}; errores 400 validación, 401/403 permisos, 404 no encontrado y 409 slug duplicado.

GET /admin/recintos Admin

Listar recintos activos e inactivos

Query: page (1), limit (20, máximo 100), search (nombre, máximo 200), estado_id (2 dígitos), municipio_id (5 dígitos), is_active (true/false). Orden estable nombre/id. No incluye árboles de asientos.

Response

{
  "data": {
    "recintos": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "nombre": "Foro Centro",
        "slug": "foro-centro",
        "estado_id": "11",
        "municipio_id": "11020",
        "ciudad": "León",
        "estado": "Guanajuato",
        "capacidad_total": null,
        "is_active": true,
        "has_layout": false,
        "layout_id": null,
        "venue_layout_id": null
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20,
    "pages": 1
  }
}
GET /admin/recintos/:id Admin

Consultar datos generales

Incluye slug, dirección, código postal, ubicación, capacidad estimada, actividad, referencias existentes de layout y fechas. También admite recintos inactivos.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "nombre": "Foro Centro",
    "slug": "foro-centro",
    "direccion": "Calle Centro 12",
    "codigo_postal": "37000",
    "ciudad": "León",
    "estado": "Guanajuato",
    "estado_id": "11",
    "municipio_id": "11020",
    "capacidad_total": null,
    "is_active": true,
    "has_layout": false,
    "layout_id": null,
    "venue_layout_id": null
  }
}
POST /admin/recintos Admin

Registrar recinto sin distribución

201. nombre (1–255), direccion (1–2000), estado_id y municipio_id obligatorios; valida pertenencia geográfica. slug opcional único, minúsculas/dígitos/guiones; si se omite se deriva del nombre. codigo_postal opcional/null (5 dígitos), capacidad_total opcional/null (entero positivo). UUID backend, tipo recinto, activo por defecto. Campos desconocidos rechazados; no acepta layout ni inventario.

Request

{
  "nombre": "Foro Centro",
  "estado_id": "11",
  "municipio_id": "11020",
  "direccion": "Calle Centro 12",
  "codigo_postal": "37000",
  "capacidad_total": 400
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "nombre": "Foro Centro",
    "slug": "foro-centro",
    "is_active": true,
    "capacidad_total": 400,
    "has_layout": false,
    "layout_id": null,
    "venue_layout_id": null
  }
}
PATCH /admin/recintos/:id Admin

Editar datos administrativos

Solo nombre, direccion, estado_id, municipio_id, codigo_postal y capacidad_total. Payload no vacío. Ubicación parcial se valida contra el valor almacenado. No permite cambiar slug, actividad ni referencias de layout. No sincroniza retrospectivamente snapshots de eventos.

Request

{
  "nombre": "Foro Centro Renovado",
  "capacidad_total": null
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "nombre": "Foro Centro Renovado",
    "capacidad_total": null
  }
}
PATCH /admin/recintos/:id/status Admin

Activar o desactivar recinto

Solo acepta is_active booleano. Los listados públicos /recintos y /venues ofrecen activos; crear eventos o cambiar a otro recinto exige actividad. Los detalles y mapas existentes continúan disponibles. Tener layout no implica publicación: esa validación pertenece a una fase posterior. Cambios auditados sin DELETE físico.

Request

{
  "is_active": false
}

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "is_active": false
  }
}

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

Administración de Taquilla

9 rutas

Alta global de operadores y administración multi-evento. Todas las rutas requieren rol admin.

GET /admin/taquilla/usuarios Admin

Listar operadores

Incluye roles, estado derivado de usuarios.is_active, assignments activos y cantidad de eventos.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "nombre": "Ana",
      "email": "ana@example.com",
      "status": "activo",
      "roles": [
        "taquilla"
      ],
      "eventos_asignados": 1,
      "assignments": [
        {
          "id": "00000000-0000-4000-8000-000000000000",
          "evento_id": "00000000-0000-4000-8000-000000000000",
          "evento_nombre": "Evento",
          "estado": "activo"
        }
      ]
    }
  ]
}
GET /admin/taquilla/eventos-asignables Admin

Listar eventos asignables

Solo devuelve eventos publicados.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "titulo": "Evento",
      "status": "publicado"
    }
  ]
}
GET /admin/taquilla/invitaciones Admin

Listar invitaciones globales

Incluye pendientes, aceptadas, expiradas y canceladas.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "email": "ana@example.com",
      "estado": "pendiente"
    }
  ]
}
POST /admin/taquilla/invitaciones Admin

Invitar operador

Crea el rol Taquilla sin asignar todavía un evento.

Request

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

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "ana@example.com",
    "estado": "pendiente"
  }
}
POST /admin/taquilla/invitaciones/:invitationId/reenviar Admin

Reenviar invitación

Rota el token y renueva la expiración.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "estado": "pendiente"
  }
}
DELETE /admin/taquilla/invitaciones/:invitationId Admin

Cancelar invitación

Cancela sin borrar historial.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "estado": "cancelada"
  }
}
POST /admin/taquilla/usuarios/:userId/asignaciones Admin

Asignar eventos

Asigna varios eventos publicados; conserva activos y reactiva inactivos.

Request

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

Response

{
  "data": {
    "usuario_id": "00000000-0000-4000-8000-000000000000",
    "assignments": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "evento_id": "00000000-0000-4000-8000-000000000000",
        "estado": "activo",
        "action": "created"
      }
    ]
  }
}
DELETE /admin/taquilla/asignaciones/:assignmentId Admin

Revocar asignación

Desactiva una asignación y registra removed_at.

Response

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "estado": "inactivo"
  }
}
DELETE /admin/taquilla/usuarios/:userId/asignaciones Admin

Revocar todo acceso

Desactiva transaccionalmente todos los assignments activos.

Response

{
  "data": {
    "usuario_id": "00000000-0000-4000-8000-000000000000",
    "revocadas": 2
  }
}

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@kustika.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@kustika.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@kustika.com",
  "password": "Password1!"
}

Response

{
  "data": {
    "user": {
      "id": "00000000-0000-4000-8000-000000000000",
      "email": "ana@kustika.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@kustika.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@kustika.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@kustika.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

14 rutas

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

GET /eventos/mis-eventos Bearer token

Listar mis eventos

Devuelve los eventos del organizador asociado al usuario autenticado, ordenados por creación descendente, sin paginación. fecha_inicio es el inicio de la primera función activa cronológicamente, incluso histórica, o null si no hay funciones activas.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "titulo": "Evento",
      "fecha_inicio": "2026-09-10T18:00:00.000Z"
    }
  ]
}
GET /eventos/customer-draft-context Bearer token

Contexto del borrador customer

Informativo y sin escrituras: no crea perfiles, solicitudes ni eventos. Lee de la BD el estado vigente de la cuenta (no confía en el rol del JWT). enabled: CUSTOMER_EVENT_DRAFTS_ENABLED=true. eligible: la cuenta puede usar el flujo customer (flag encendido, cuenta activa, email verificado, customer como único rol vigente y perfil de organizador inexistente o PROSPECTO). can_create: eligible y sin borrador activo. reason (null solo si can_create), en orden de prioridad: FEATURE_DISABLED, ACCOUNT_INACTIVE, ORGANIZER_ROLE (admin o event_manager: usar el alta normal), ROLE_NOT_ELIGIBLE (otro rol o varios roles), EMAIL_NOT_VERIFIED, PROFILE_INELIGIBLE (perfil existente no PROSPECTO), DRAFT_EXISTS. existing_event_id y existing_review_status (sin_solicitud, pendiente, aprobada, rechazada o retirada) describen solo el borrador activo del propio perfil PROSPECTO de una cuenta activa; nunca eventos de otros usuarios ni de perfiles ACTIVO. Se informan aunque el flag esté apagado. can_read_existing: el usuario puede abrir ese borrador hoy con GET /eventos/:id/editable (false con el flag apagado o la cuenta no apta). Es una instantánea: la elegibilidad puede cambiar entre esta lectura y POST /eventos, que revalida todo con el usuario bloqueado y puede responder 403 o 409 CUSTOMER_DRAFT_LIMIT.

Response

{
  "data": {
    "enabled": true,
    "eligible": true,
    "can_create": false,
    "reason": "DRAFT_EXISTS",
    "existing_event_id": "00000000-0000-4000-8000-000000000000",
    "existing_review_status": "pendiente",
    "can_read_existing": true
  }
}
GET /eventos/all-eventos?page=1&limit=20 Admin

Listar eventos administrativos

Acepta status?: string, categoria_id, busqueda, page y limit. status admite un valor o varios separados por coma (publicado,finalizado,cancelado); estados permitidos: publicado, borrador, finalizado, archivado y cancelado. Se recortan espacios y eliminan vacíos y duplicados; sin valores equivale a Todos. Valores inválidos devuelven 400. SELECT, filtro y total usan el mismo estado efectivo; búsqueda y categoría se combinan con AND. La persistencia de lifecycle no cambia: publicados sin funciones activas se muestran finalizados, pero el cron no persiste ese caso. fecha_inicio es nullable: el inicio de la primera función activa cronológicamente, incluso si ya pasó. Sin funciones activas devuelve null.

Response

{
  "data": {
    "eventos": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "titulo": "Evento",
        "fecha_inicio": "2026-09-10T18:00:00.000Z"
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20,
    "pages": 1
  }
}
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. Sin token solo responde estados públicos (publicado, finalizado, archivado, cancelado). Un borrador o evento abandonado responde 404 salvo token válido de admin, event_manager propietario o customer con su borrador (transición compatible con lecturas de gestión que ya envían token). Un token inválido se trata como anónimo.

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. Con CUSTOMER_EVENT_DRAFTS_ENABLED=true un customer verificado y activo (único rol vigente) crea un borrador privado: request_id obligatorio (400 CUSTOMER_DRAFT_REQUEST_ID_REQUIRED), perfil interno PROSPECTO sin validar, rol customer sin cambios y máximo un borrador activo (409 CUSTOMER_DRAFT_LIMIT con existing_event_id). Un perfil existente no PROSPECTO (ex-event_manager) responde 403 CUSTOMER_DRAFT_PROFILE_INELIGIBLE. Antes de mostrar el creador, GET /eventos/customer-draft-context informa eligible/can_create. El customer no publica: PUT /eventos/:id/publicar responde 409 EVENT_REVIEW_REQUIRED; cargo_servicio y cargo_servicio_porcentaje responden 403 SERVICE_FEE_FORBIDDEN. Un borrador con solicitud de publicación pendiente responde 409 EVENT_REVIEW_PENDING a cualquier cambio. request_id UUID opcional: mismo usuario y contenido normalizado recupera el mismo evento con 201; contenido incompatible devuelve 409 EVENT_CREATION_REQUEST_CONFLICT. Sin clave conserva el alta legacy. Instancias precargadas: inventario EVENT_SINGLE_FUNCTION, PERSON numerado con section_id efectivo obligatorio; general físico y TABLE/BOX permanecen bloqueados.

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. PERSON + is_numerado=false es acceso general. PERSON + is_numerado=true es declarativo: con cantidad_total, zona y label_format el backend resuelve o crea una sección lógica y genera sus asientos; ALPHANUMERIC exige units_per_row. section_id queda opcional para enlazar un seatmap visual numbered o mixed ya existente, cuya capacidad debe coincidir. En TABLE, label_format NUMERIC conserva Mesa 1..N; ALPHANUMERIC genera A1..A20, B1... Los UUID y unit_number son identidades estables; label es sólo presentación. Cambiar numeración, sección o distribución se bloquea si existen reservas, órdenes o ventas.

Request

{
  "nombre": "VIP Numerado",
  "precio": 1200,
  "cantidad_total": 200,
  "sale_mode": "PERSON",
  "is_numerado": true,
  "section_id": "uuid opcional para PERSON numerado con seatmap visual existente",
  "people_per_unit": 1,
  "label_format": "ALPHANUMERIC",
  "units_per_row": 20,
  "max_por_orden": 10,
  "min_por_orden": 1,
  "zona": "VIP",
  "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_mode": "PERSON",
    "people_per_unit": 1,
    "is_numerado": true,
    "section_id": "00000000-0000-4000-8000-000000000000",
    "section": {
      "id": "00000000-0000-4000-8000-000000000000",
      "code": "platea",
      "nombre": "Platea",
      "tipo": "numbered"
    },
    "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. Lectura pública solo si el evento padre está publicado, finalizado, archivado o cancelado; en otro estado responde 404 salvo Bearer token de admin, event_manager propietario o customer con su borrador (token inválido = anónimo).

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

Sincroniza atómicamente las fases activas de un tipo administrado por el usuario. Las históricas inactivas se preservan. Solo admin puede cambiar precio, nombre u orden con actividad comercial; al omitir una fase la retira mediante inactivación, sin modificar órdenes ni precios congelados de holds. Otros usuarios conservan las restricciones comerciales. Enviar sale_phases: [] restaura el modo de precio fijo si la operación está permitida. Los IDs de fases existentes deben conservarse.

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. Cada función activa debe tener fechas vigentes y al menos un tipo de boleto activo con inventario válido. PERSON numerado exige su sección y el total exacto de seat_map_seats habilitados; TABLE exige su proyección y el total exacto de commercial_units activas y consistentes. Admin: se registra en la misma transacción una solicitud ADMIN_DIRECTO aprobada y auditada y se publica por el núcleo único de publicación. Customer: nunca publica directamente (409 EVENT_REVIEW_REQUIRED). Event_manager: con la revisión obligatoria activa (política de la migración 086) responde 409 EVENT_REVIEW_REQUIRED y debe enviar el evento con POST /eventos/:id/solicitudes-publicacion; mientras la política esté inactiva conserva la publicación directa. Con una solicitud pendiente: 409 EVENT_REVIEW_PENDING para cualquier actor. El borrador de un perfil PROSPECTO se publica aprobando su solicitud. Un evento ya publicado responde 400. La base de datos rechaza cualquier transición a publicado sin aprobación de la misma transacción (409 PUBLICATION_APPROVAL_REQUIRED).

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

22 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. PERSON general usa quantity; TABLE usa commercial_unit_ids; PERSON numerado usa UUID canónicos en seats y sólo acepta secciones numbered/mixed asociadas a un tipo PERSON numerado activo del evento.

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. Si el total final validado por el backend (con cargos y descuentos) es 0, la orden se confirma en la misma transacción sin Stripe: responde status 'pagado', es_gratuita true y boletos_emitidos > 0 con los boletos ya generados; el correo se encola con el mecanismo habitual. Reenviar el mismo hold_token después de esa confirmación devuelve la misma orden gratuita (sin duplicar boletos). es_cortesia es un alias histórico de 'total 0'. GET /reservas/:id también expone boletos_emitidos y es_gratuita: el cliente sólo debe afirmar que los boletos están listos cuando boletos_emitidos > 0.

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,
    "es_gratuita": false,
    "status": "pendiente",
    "boletos_emitidos": 0
  }
}
POST /pagos/tarjeta Bearer token

Iniciar o recuperar el pago con tarjeta de una orden

No exige dirección del perfil. Crea el PaymentIntent de la orden pendiente o devuelve el mismo si ya existe (solicitudes repetidas y reintentos tras un rechazo usan el mismo client_secret). Un rechazo de tarjeta no invalida la orden: sigue pendiente, con su inventario, hasta expires_at. Errores: { error, code }. 400 INVALID_REQUEST (body ausente), INVALID_ORDER_ID (orden_id ausente o no UUID), ORDER_ALREADY_PROCESSED, ORDER_EXPIRED, ORDER_TOTAL_INVALID; 404 ORDER_NOT_FOUND (inexistente o ajena); 409 PAYMENT_METHOD_CONFLICT (la orden ya usa OXXO o tiene un pago HeyBanco en curso), PAYMENT_INTENT_CANCELED (crear una nueva orden), EVENT_FUNCTION_FINISHED/EVENT_*; 502/503/429 fallas de Stripe.

Request

{
  "orden_id": "00000000-0000-4000-8000-000000000000"
}

Response

{
  "data": {
    "client_secret": "pi_..._secret_...",
    "payment_intent_id": "pi_..."
  }
}
POST /pagos/oxxo Bearer token

Generar referencia OXXO de una orden

No exige dirección del perfil. Usa nombre y correo de la cuenta. Mismos errores que /pagos/tarjeta, más 400 BUYER_EMAIL_REQUIRED / BUYER_NAME_REQUIRED. Una orden que ya inició pago con tarjeta responde 409 PAYMENT_METHOD_CONFLICT.

Request

{
  "orden_id": "00000000-0000-4000-8000-000000000000"
}

Response

{
  "data": {
    "referencia": "12345678901234",
    "vence_at": "2026-07-04T18:00:00.000Z",
    "voucher_url": "https://payments.stripe.com/oxxo/...",
    "payment_intent_id": "pi_..."
  }
}
POST /reservas/:id/cancelar Bearer token

Cancelar una orden pendiente propia

Si la orden tiene PaymentIntent, primero lo cancela en Stripe y después libera inventario y descuento. 409 ORDER_ALREADY_PAID si Stripe ya cobró (la orden se confirma), 409 PAYMENT_IN_PROGRESS si el cobro se está procesando, 403 ORDER_FORBIDDEN, 404 ORDER_NOT_FOUND, 400 INVALID_ORDER_ID.

Response

{
  "success": true,
  "data": {
    "orden_id": "00000000-0000-4000-8000-000000000000",
    "status": "cancelada"
  },
  "message": "Orden cancelada exitosamente"
}
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. Lectura pública solo si el evento padre está publicado, finalizado, archivado o cancelado; en otro estado responde 404 salvo Bearer token de admin, event_manager propietario o customer con su borrador (token inválido = anónimo).

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. Lectura pública solo si el evento padre está publicado, finalizado, archivado o cancelado; en otro estado responde 404 salvo Bearer token de admin, event_manager propietario o customer con su borrador (token inválido = anónimo).

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. Lectura pública solo si el evento padre está publicado, finalizado, archivado o cancelado; en otro estado responde 404 salvo Bearer token de admin, event_manager propietario o customer con su borrador (token inválido = anónimo).

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. Lectura pública solo si el evento padre está publicado, finalizado, archivado o cancelado; en otro estado responde 404 salvo Bearer token de admin, event_manager propietario o customer con su borrador (token inválido = anónimo).

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": "kustika 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

Registro inicial por el contrato legacy (POST /organizadores/solicitar). Transacción única: exige admin vigente y customer activo con correo verificado, revalida datos empresariales y PDF registrado, asigna event_manager como rol único y activa el perfil PROSPECTO o crea un perfil ACTIVO si no existe. Conserva los datos de perfiles ACTIVO existentes. El perfil inicial utiliza documentación LEGACY respaldada por esta solicitud aprobada; no inventa una vigencia VIGENTE. Audita y conserva los mismos eventos y propietarios, sin publicar ni modificar funciones, precios o inventario. Los conflictos de calendario y cambios concurrentes de borradores responden 409 y revierten toda la aprobación. La sesión se actualiza mediante /auth/refresh; el usuario recupera sus borradores con /eventos/mis-eventos y /eventos/:id/editable. Publicar o solicitar publicación sigue siendo una operación posterior con las políticas vigentes. Las validaciones empresariales nuevas se deciden por su flujo independiente y no se promueven aquí. Errores: 400 identificador inválido o solicitud procesada; 403 admin no vigente; 404 solicitud inexistente; 409 documentación inválida, cuenta no elegible, roles incompatibles, conflicto de calendario o cambios concurrentes.

Response

{
  "message": "Solicitud aprobada. El usuario ahora es event_manager"
}
PUT /organizadores/solicitudes/:id/rechazar Admin

Rechazar solicitud

Transacción única: motivo obligatorio (10 a 2000 caracteres, se sanitiza), solo desde pendiente, conserva la evidencia y audita RECHAZAR_SOLICITUD_ORGANIZADOR. El rol del solicitante no cambia. Errores: 400 motivo inválido, solicitud_id inválido o solicitud ya procesada; 403 admin no vigente; 404 solicitud inexistente.

Request

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

Response

{
  "message": "Solicitud rechazada"
}

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

3 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. evento.status conserva el lifecycle del evento; is_usable y access_status son la clasificación autoritativa de acceso calculada por backend.

Response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "status": "activo",
      "funcion_id": "00000000-0000-4000-8000-000000000000",
      "funcion": {
        "id": "00000000-0000-4000-8000-000000000000",
        "is_active": true
      },
      "evento": {
        "id": "00000000-0000-4000-8000-000000000000",
        "titulo": "Concierto",
        "imagen_portada": "https://example.com/evento.jpg",
        "status": "publicado"
      },
      "is_usable": true,
      "access_status": "available",
      "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
      }
    }
  }
}
GET /eventos/:eventoId/accesos Bearer token

Monitor de accesos permitidos

Lista check-ins permitidos del evento, ordenados del mas reciente al mas antiguo. commercial_section.event_section_price_id es la clave estable para agrupar accesos por seccion comercial, tanto con seatmap como sin seatmap. section_configuration indica numeracion, modalidad y beneficios. Admin puede consultar cualquier evento; event_manager solo eventos propios. Soporta page, limit, search y tipo_boleto_id.

Response

{
  "data": {
    "items": [
      {
        "checkin_id": "00000000-0000-4000-8000-000000000000",
        "boleto_id": "00000000-0000-4000-8000-000000000000",
        "evento_id": "00000000-0000-4000-8000-000000000000",
        "funcion_id": "00000000-0000-4000-8000-000000000000",
        "nombre": "Ana Perez",
        "correo": "ana@correo.com",
        "tipo_boleto": {
          "id": "00000000-0000-4000-8000-000000000000",
          "nombre": "VIP"
        },
        "commercial_section": {
          "id": "00000000-0000-4000-8000-000000000000",
          "source": "SEAT_MAP_SECTION",
          "section_id": "00000000-0000-4000-8000-000000000000",
          "event_section_price_id": "00000000-0000-4000-8000-000000000000",
          "label": "VIP"
        },
        "section_configuration": {
          "is_numerado": true,
          "sale_mode": "PERSON",
          "people_per_unit": 1,
          "has_chairs": null,
          "has_individual_seats": null,
          "label_format": "NUMERIC",
          "units_per_row": null,
          "has_complimentary_items": true
        },
        "estado_acceso": "permitido",
        "fecha_hora_entrada": "2026-08-24T18:00:00.000Z",
        "ubicacion": {
          "tipo": "asiento",
          "zona": "VIP",
          "seccion": "VIP",
          "subseccion": null,
          "fila": "A",
          "asiento": "12",
          "mesa": null,
          "posicion": null
        },
        "commercial_details": {
          "commercial_units": [],
          "complimentary_items": [
            {
              "label": "Cortesia",
              "quantity": 1
            }
          ],
          "extras": [],
          "pricing_breakdown": {
            "base_subtotal": 1000,
            "service_fee": 150,
            "discount": 0,
            "extras_subtotal": 0,
            "total": 1150
          }
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 50,
      "total": 1,
      "total_pages": 1
    },
    "summary": {
      "ingresaron": 1,
      "total_boletos": 10,
      "pendientes": 9
    }
  }
}

Grupo

Staff

11 rutas

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

GET /staff/access-assignment Bearer token

Consultar asignación de acceso propia

Devuelve el evento asignado al staff. fecha_principal conserva el inicio de la primera función; acceso_desde es la apertura más temprana entre funciones activas, usando fecha_inicio cuando no existe fecha_apertura_puertas. acceso_desde orienta la interfaz y no sustituye la validación individual del boleto por función.

Response

{
  "data": {
    "assignment_id": "00000000-0000-4000-8000-000000000000",
    "evento": {
      "id": "00000000-0000-4000-8000-000000000000",
      "nombre": "Concierto",
      "venue": "Foro",
      "imagen": "https://example.com/evento.jpg",
      "fecha_principal": "2026-10-11T01:00:00.000Z",
      "acceso_desde": "2026-10-11T00:30:00.000Z"
    }
  }
}
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 /event-manager/eventos/:eventoId/staff-access Bearer token

Consultar Staff de acceso del evento

Solo lectura: Staff de acceso (event_user_assignments, responsibility='acceso', estado='activo') asignado por Kustika al evento propio del event_manager. No administra invitaciones ni asignaciones; eso sigue siendo exclusivo de /admin/staff-access.

Response

{
  "data": [
    {
      "assignment_id": "00000000-0000-4000-8000-000000000000",
      "usuario_id": "00000000-0000-4000-8000-000000000000",
      "evento_id": "00000000-0000-4000-8000-000000000000",
      "nombre": "Juan Pérez",
      "email": "juan@correo.com",
      "estado": "activo",
      "assigned_at": "2026-06-29T23:59:59.000Z"
    }
  ]
}
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

3 rutas

Vistas básicas sin identificadores persistentes y analítica avanzada con consentimiento.

POST /analytics/page-views Pública

Registrar una vista básica sin cookies de analítica

Enviar en cada carga de página pública, haya o no consentimiento analytics. Body estricto: solo event_id (UUID nuevo por vista, reutilizado únicamente al reintentar esa vista) y path sin datos personales. Normaliza path y elimina query/hash. No enviar cookies ni Authorization: usar credentials: omit. HTTP 202: accepted=true solo tras insertar; false si event_id ya existe. No crea visitantes ni sesiones. Límite de 600/min por IP resuelta; en Cloud Run sin proxy confiable se omite solo este límite para evitar un contador compartido. Body máximo 16 KiB. No se aplican filtros de User-Agent.

Request

{
  "event_id": "00000000-0000-4000-8000-000000000000",
  "path": "/eventos/concierto"
}

Response

{
  "data": {
    "accepted": true
  }
}
POST /analytics/events Pública

Registrar una vista de página

Analítica avanzada existente: el frontend debe enviarla únicamente con consentimiento analytics=true. Acepta page_view, deduplica por event_id y conserva visitor_id/session_id. No incrementa total_page_views; enviar también la vista básica cuando corresponda.

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

Summary y timeline incluyen total_page_views (solo tabla básica) y tracked_page_views (solo avanzada). pageviews se conserva como alias de tracked_page_views. Visitantes, sesiones, views_per_session y top_pages siguen usando solo avanzada; nunca sumar ambas capas. Timeline conserva period como etiqueta. Soporta period=custom&startDate=YYYY-MM-DD&endDate=YYYY-MM-DD&granularity=day|month; meses alineados y recortados al rango solicitado. Timezone America/Mexico_City. Los totales básicos comienzan con la nueva instrumentación, sin backfill histórico.

Response

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

Grupo

Admin

7 rutas

Acciones protegidas para usuarios administradores.

GET /admin/usuarios Admin

Listar usuarios

Query: page (entero >= 1, por defecto 1), limit (1–100, por defecto 20), search?: string (trim, máximo 200 caracteres; vacío equivale a omitirlo). Busca coincidencias parciales sin distinguir mayúsculas en nombre, apellidos, nombre completo, email y teléfono. Los comodines % y _ se buscan literalmente. roles?: CSV de admin,event_manager,staff,taquilla,customer; filtra por el mismo rol efectivo mostrado (prioridad admin > event_manager > staff > taquilla > customer). status?: CSV de activo (is_active=true), inactivo (is_active=false), sin_verificar (email_verified=false). Ambos normalizan espacios, vacíos y duplicados; CSV vacío omite el filtro; arrays, mayúsculas y valores desconocidos se rechazan con 400. OR dentro de cada grupo, AND entre search, roles y status; actividad y verificación son independientes. Filtra antes de paginar; total y pages corresponden al filtro. Orden fijo: created_at DESC, id DESC. Ejemplo combinado: /admin/usuarios?search=ana&roles=admin,staff&status=activo,sin_verificar&page=1&limit=20. Ejemplos: /admin/usuarios?page=1&limit=10&search=bryan, /admin/usuarios?search=cabrera%20sanchez, /admin/usuarios?search=gmail.com, /admin/usuarios?search=419122.

Response

{
  "data": {
    "usuarios": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "email": "ana@kustika.com",
        "nombre": "Ana",
        "apellido_paterno": "López",
        "apellido_materno": null,
        "telefono": null,
        "is_active": true,
        "email_verified": true,
        "created_at": "2026-09-01T12:00:00.000Z",
        "rol": "customer"
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20,
    "pages": 1
  }
}
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"
}