API Reference — Vital4Female

Documentación técnica de todos los endpoints REST expuestos por la aplicación (routes/api.php), pensada para publicarse en la documentación oficial del sistema y servir de referencia a integraciones externas (Vue.js, mobile, ElevenLabs Agent, etc.).

Para el flujo detallado de registro de estudiantes (incluyendo integración con PayPro, colas y ejemplos Vue), ver student-registration-api.md. Este documento resume ese mismo endpoint y añade el resto de la API.


Convenciones generales

Base URL

https://admin.vital4female.nl/api

Headers por defecto

Content-Type: application/json
Accept: application/json

Formato de respuesta

La mayoría de los endpoints heredan de BaseController y devuelven un envelope consistente:

// Éxito
{
  "success": true,
  "data": { ... },
  "message": "Descripción legible"
}
// Error
{
  "success": false,
  "message": "Descripción del error",
  "data": { "campo": ["mensaje de validación"] }
}

Un pequeño número de endpoints "legacy" (course_grid, questions, agent/*, surveyanswers, checkdiscounts) no usan este envelope y devuelven JSON plano — se indica explícitamente en cada sección.

Autenticación

La API usa Laravel Sanctum (tokens personales tipo Bearer). Los endpoints protegidos requieren:

Authorization: Bearer {token}

El token se obtiene vía POST /api/login (ver más abajo) y solo se emite a usuarios con rol super-admin, admin o api-user.

Un grupo separado de endpoints (/api/agent/*) usa un esquema de autenticación distinto basado en API key por header (ver sección Agente ElevenLabs).


Índice de endpoints

Método Endpoint Auth Descripción
POST /login Pública Login y emisión de token Sanctum
GET /user Sanctum Usuario autenticado actual
GET/POST/PUT/DELETE /coaches /coaches/{id} Sanctum CRUD de coaches (parcial)
GET/POST/PUT/DELETE /students /students/{id} Sanctum (index/show)¹ Registro y consulta de estudiantes
PATCH /membership_change Sanctum Cambia la membresía de un estudiante
GET/POST/PUT/DELETE /categories /categories/{id} Pública Catálogo de categorías (solo index)
GET/POST/PUT/DELETE /category/{category}/subcategories Pública Subcategorías por categoría (solo index)
GET/POST/PUT/DELETE /courses /courses/{id} Pública Catálogo de cursos (solo index)
GET /coursegrid Pública Cursos + categorías + subcategorías + membresías activas
GET/POST/PUT/DELETE /lessons /lessons/{id} Pública Catálogo de lecciones (solo index)
GET/POST/PUT/DELETE /levels /levels/{id} Pública CRUD completo de niveles
GET/POST/PUT/DELETE /statuses /statuses/{id} Pública CRUD completo de estados
GET /memberships Pública Membresías activas
GET /checkdiscounts/{code} Pública Valida un código de descuento
GET/POST/PUT/DELETE /courses/{course}/lessons /courses/{course}/lessons/{id} Pública Lecciones dentro de un curso (solo index)
PUT /addLessonToCourse/{id} Pública Asocia una lección a un curso
GET /deleteLessonInCourse/{id} Pública Desasocia una lección de un curso
POST /surveyanswers Pública² Guarda respuestas de una encuesta
GET /surveyanswers/{id} Pública Respuestas de un usuario
GET /questions Pública Preguntas de una encuesta (?survey_id=)
GET /questions/{id} Pública ⚠️ Ruta registrada, controlador sin método show() (ver notas)
GET /questions/index/{id} Pública Preguntas de la encuesta {id}
POST /agent/rag API key (x-agent-key) Búsqueda semántica (RAG) para el agente ElevenLabs
GET /agent/student/{id} API key (x-agent-key) Perfil del estudiante para el agente
POST /agent/interaction API key (x-agent-key) Guarda una interacción del agente

¹ POST /students es pública (registro de nuevos estudiantes); index/show/update/destroy requieren Sanctum. ² No exige token, pero usa auth()->id() internamente — ver advertencia en su sección.


Autenticación

POST /api/login

Autentica un usuario administrativo y emite un token Sanctum.

Rate limiting: 5 intentos por minuto, por combinación email + IP.

Request body

Campo Tipo Requerido Descripción
email string Email del usuario
password string Contraseña
{
  "email": "admin@vital4female.nl",
  "password": "secret"
}

Respuestas

200 — Login exitoso

{
  "success": true,
  "data": {
    "token": "1|AbCdEfGhIjKlMnOpQrStUvWxYz",
    "username": "admin"
  },
  "message": "User logged in successfully."
}

401 — Credenciales inválidas

{
  "success": false,
  "message": "Unauthorised.",
  "data": { "error": "Invalid credentials" }
}

403 — Usuario sin acceso API

Solo los roles super-admin, admin y api-user pueden generar tokens; cualquier otro rol recibe:

{
  "success": false,
  "message": "Forbidden.",
  "data": { "error": "This user has no API access" }
}

422 — Rate limit excedido

Laravel responde con el mensaje de throttle estándar de auth.throttle (vía ValidationException).


Endpoints protegidos (Sanctum)

Requieren header Authorization: Bearer {token} obtenido en /login.

GET /api/user

Devuelve el modelo User autenticado (según el token enviado).

{ "id": 1, "username": "admin", "email": "admin@vital4female.nl", "...": "..." }

Coaches — /api/coaches

Registrado como apiResource, pero el controlador solo implementa index y show; store, update y destroy no están implementados (llamarlos produce un error de método no definido).

Método Endpoint Descripción
GET /coaches Lista todos los coaches
GET /coaches/{id} Detalle de un coach (404 si no existe)
{ "success": true, "data": [ { "id": 1, "coach_social": {...}, "coach_schedule": {...} } ], "message": "Coaches list" }

Students — /api/students

Método Endpoint Auth Descripción
POST /students Pública (fuera del grupo Sanctum) Registro de un nuevo estudiante — ver student-registration-api.md
GET /students Sanctum Lista todos los estudiantes
GET /students/{id} Sanctum Detalle de un estudiante

update y destroy están declarados por apiResource pero no implementados en el controlador.

Resumen de POST /students

POST /api/students
Campo Tipo Requerido Descripción
firstname string Máx. 100 caracteres
lastname string Máx. 100 caracteres
email string Único en users.email
product_id integer Debe existir en products.id
membership string Nombre de la membresía
payment_method string ideal, ideal-qr, etc.
birthday date YYYY-MM-DD
discount_code string Código de descuento

Respuesta 201:

{
  "success": true,
  "data": {
    "payment_url": "...",
    "payment_hash": "...",
    "order_id": 42,
    "user_id": 88
  },
  "message": "Student registered successfully."
}

Ver el documento dedicado para el detalle completo de errores (422, 429, 500) y el flujo de pago con PayPro.

PATCH /api/membership_change

Cambia la membresía activa de un estudiante, actualiza su disponibilidad de coaching y envía notificación por email + notificación interna.

Campo Tipo Requerido Descripción
user_id integer ID del usuario/estudiante
membership_id integer Nuevo ID de membresía
membership_name string Nombre de la nueva membresía

Efectos secundarios:

  • Actualiza students.membership_id y students.student_membership.
  • Actualiza availabilities.minutes_available según membership_coaching_time.
  • Envía el correo MembershipUpgrade al usuario.
  • Dispara la notificación MembershipChanged.
{ "success": true, "data": "Changed", "message": "Membership changed" }

⚠️ No valida que user_id o membership_id existan antes de operar sobre ellos (firstOrFail sobre Student, pero Membership::find sin orFail); un membership_id inexistente puede provocar un error 500 al acceder a membership_coaching_time de null.


Catálogo público

Estos endpoints no requieren autenticación — se usan desde los componentes Vue del sitio público y del checkout.

Categorías y subcategorías

Método Endpoint Descripción
GET /api/categories Lista todas las categorías (category_tags como array)
GET /api/category/{category}/subcategories Subcategorías de la categoría indicada

Solo index está implementado en ambos controladores, aunque las rutas se registran como apiResource completo.

{
  "success": true,
  "data": [
    {
      "id": 1,
      "category_name": "Fitness",
      "category_tags": ["cardio", "fuerza"]
    }
  ],
  "message": "Category list"
}

Cursos

Método Endpoint Descripción
GET /api/courses Lista todos los cursos (sin filtrar por estado)
GET /api/coursegrid Cursos activos (status_id = 1) + catálogos relacionados

GET /api/coursegrid es el endpoint usado por la grilla pública de cursos. No usa el envelope success/data/message:

{
  "courses": [ { "id": 5, "course_title": "...", "category": {...}, "subcategory": {...} } ],
  "categories": [ ... ],
  "subcategories": [ ... ],
  "memberships": [ ... ]
}

Lecciones

Método Endpoint Descripción
GET /api/lessons Lista todas las lecciones (solo index implementado)
GET /api/courses/{course}/lessons Lecciones de un curso específico (solo index)
PUT /api/addLessonToCourse/{id} Reasigna una lección (id) al curso indicado en course_id
GET /api/deleteLessonInCourse/{id} Desasocia la lección {id} de su curso (pone course_id = 0)
// PUT /api/addLessonToCourse/12   body: { "course_id": 7 }
{
  "success": true,
  "data": { "id": 12, "course_id": 7, "...": "..." },
  "message": "lessonincourse updated"
}

⚠️ deleteLessonInCourse usa GET, no DELETE — es una convención heredada del sistema, no un error de este documento. Además, no elimina la lección, solo la desvincula del curso (course_id = 0).

Niveles — /api/levels

CRUD completo y funcional:

Método Endpoint Body Descripción
GET /levels Lista todos
POST /levels { "level_name": "..." } Crea (level_name requerido)
GET /levels/{id} Detalle (404 si no existe)
PUT/PATCH /levels/{id} { "level_name": "..." } Actualiza
DELETE /levels/{id} Elimina

Estados — /api/statuses

Mismo patrón CRUD que levels, con el campo status_name:

Método Endpoint Body
GET /statuses
POST /statuses { "status_name": "..." }
GET /statuses/{id}
PUT/PATCH /statuses/{id} { "status_name": "..." }
DELETE /statuses/{id}

Membresías — /api/memberships

GET /api/memberships

Devuelve solo las membresías con isActive = true, ordenadas por membership_order.

{
  "success": true,
  "data": [{ "id": 1, "membership_slug": "founder", "membership_order": 1 }],
  "message": "Membership list"
}

Descuentos

GET /api/checkdiscounts/{code}

Valida un código de descuento antes del checkout. No usa el envelope estándar.

Respuesta Código HTTP Body
Válido 200 { "valid": true, "message": "Discount code is valid", "amount": 40 }
No encontrado 404 { "valid": false, "message": "Discount not found" }
Agotado (uses_count >= max_uses) 400 { "valid": false, "message": "Discount code has been used up" }

amount es el porcentaje de descuento (discounts.percentage), redondeado a entero.


Encuestas y preguntas

POST /api/surveyanswers

Guarda las respuestas de un usuario a una encuesta.

Campo Tipo Descripción
surveyId integer ID de la encuesta
answers object Mapa { question_id: answer_value }
{ "surveyId": 3, "answers": { "10": "opcion_a", "11": "opcion_b" } }
// 200
{ "message": "Successfully saved responses" }

⚠️ El controlador usa auth()->id() para asociar las respuestas al usuario, pero la ruta no está dentro del grupo auth:sanctum. Si se llama sin sesión/token activo, user_id se guarda como null. Si se integra desde un cliente externo, considerar exigir autenticación o enviar el user_id explícitamente.

GET /api/surveyanswers/{id}

Devuelve todas las respuestas guardadas para el usuario {id}.

[
  {
    "id": 1,
    "user_id": 5,
    "survey_id": 3,
    "question_id": 10,
    "value": "opcion_a"
  }
]

Preguntas — /api/questions

Método Endpoint Descripción
GET /api/questions?survey_id={id} Preguntas de una encuesta, con title/description de la encuesta (join) — devuelve un array plano, sin envelope
GET /api/questions/{id} ⚠️ Registrado por Route::resource(...)->only(['index','show']), pero QuestionController no define show() — invocarlo produce un error del framework. Usar questions/index/{id} en su lugar
GET /api/questions/index/{id} Alias explícito equivalente a index filtrando por survey_id = {id}

Los métodos store, update y destroy existen en el controlador pero no están registrados en routes/api.php (no son accesibles vía API pública actualmente).


Agente ElevenLabs

Grupo de endpoints bajo el prefijo /api/agent, protegidos por el middleware agent.key (no usa Sanctum).

Autenticación

x-agent-key: {AGENT_API_KEY}

Configurado en config/services.phpservices.agent.key (variable de entorno correspondiente). Una key ausente o incorrecta responde:

// 401
{ "error": "Unauthorized" }

POST /api/agent/rag

Búsqueda semántica (RAG) sobre la base de conocimiento (knowledge_base, pgvector) usando embeddings de OpenAI (text-embedding-3-small). Pensado para ser invocado por el agente conversacional de ElevenLabs antes de generar una respuesta.

Campo Tipo Requerido Descripción
query string Máx. 1000 caracteres — texto a buscar
user_id integer Si se envía, filtra resultados por la membresía del estudiante (exists:users,id)
limit integer 1–10, por defecto 5
{ "query": "¿Cómo cancelo mi membresía?", "user_id": 88, "limit": 3 }

Respuesta (sin envelope):

{
  "query": "¿Cómo cancelo mi membresía?",
  "context": [
    {
      "source_type": "faq",
      "source_id": 12,
      "content": "Para cancelar tu membresía...",
      "similarity": 0.8734,
      "metadata": { "membership": "founder" }
    }
  ]
}

Requiere OPENAI_API_KEY configurada (services.openai.key) y la extensión pgvector en PostgreSQL.

GET /api/agent/student/{id}

Perfil resumido de un estudiante, usado por el agente para personalizar sus respuestas.

{
  "user_id": 88,
  "name": "Anna Jansen",
  "language": "nl",
  "status": "active",
  "membership": "founder",
  "courses": [{ "id": 5, "course_title": "...", "course_description": "..." }]
}

courses solo incluye cursos activos (status_id = 1) cuyo campo JSON course_membership contiene el slug de la membresía del estudiante. Si el usuario no existe, responde 404 (findOrFail).

POST /api/agent/interaction

Registra una interacción del agente (para historial/analítica) en la tabla agent_interactions.

Campo Tipo Requerido Descripción
session_id string ID de sesión de la conversación
query string Pregunta del usuario
response string Respuesta generada por el agente
user_id integer exists:users,id
context array Contexto RAG usado para generar la respuesta
// 200
{ "status": "ok" }

Notas y limitaciones conocidas

  • Rutas apiResource parcialmente implementadas: categories, category.subcategories, courses, lessons, coaches y courses.lessons se registran como recursos REST completos, pero sus controladores solo implementan index (y show en coaches). Invocar store/update/destroy sobre estos recursos falla con un error de método no definido en el controlador.
  • GET /api/questions/{id} no funciona — falta el método show() en QuestionController. Usar GET /api/questions/index/{id} como alternativa ya soportada.
  • Endpoints sin envelope estándar: coursegrid, checkdiscounts, surveyanswers, questions y agent/* devuelven JSON plano en lugar de { success, data, message }. Tenerlo en cuenta al escribir clientes genéricos.
  • deleteLessonInCourse usa GET en vez de DELETE, y no elimina la lección, solo la desvincula del curso.
  • surveyanswers no está protegida por Sanctum pese a depender de auth()->id() internamente.
  • Los endpoints marcados como "pública" en el índice están disponibles sin token porque alimentan componentes Vue del sitio público (landing, checkout, grilla de cursos); no exponen datos sensibles de estudiantes individuales salvo agent/student/{id}, que sí está protegido por API key.

Referencias relacionadas

  • student-registration-api.md — flujo completo de registro + pago (PayPro), ejemplos Vue.js, colas y webhook.
  • routes/api.php — fuente de verdad de las rutas registradas.
  • app/Http/Controllers/API/V1/ — controladores de la API versión 1.
  • app/Http/Middleware/AgentKeyMiddleware.php — validación de x-agent-key.