Academia Derecho Virtual
API de administración · documentación pública
Ésta es la documentación completa y pública de la API de administración de la Academia Derecho Virtual. No hace falta clave para leerla, ni cabeceras, ni permiso de nadie: si estás leyendo esto ya tienes todo lo necesario para escribir un cliente correcto. Para EJECUTAR cualquier operación sí hace falta una credencial, que te habrá dado el dueño.
Qué es esta academia. La academia online de Derecho Virtual: el catálogo de cursos jurídicos, el aula donde estudian los alumnos, las páginas de pago y la facturación.
- Dirección base
https://academia.derechovirtual.org/api/admin/v1- Autenticación
- Manda la credencial en la cabecera `Authorization: Bearer <credencial>`. Valen las dos formas: con el sufijo `@academia.derechovirtual.org` y sin él. Nunca va en la URL, ni en un parámetro, ni en el cuerpo.
- Ojo: la sesión del navegador NO sirve
- La API NO acepta la sesión del navegador. Aunque hayas entrado en la academia como administrador, esa cookie aquí no vale: la ÚNICA forma de autenticarse es la cabecera `Authorization`. Por eso pegar una ruta de la API en la barra de direcciones devuelve 401 aunque tengas la sesión abierta — no está rota, está haciendo su trabajo. Y es a propósito: si la API aceptara la cookie, cualquier web que visitaras podría operar la Academia en tu nombre sin más que llamarla desde tu navegador, que es la sesión de quien ya está dentro. Una credencial hay que mandarla a conciencia; una cookie la manda el navegador solo. Consecuencia práctica: para leer la API hace falta algo que sepa poner cabeceras (`curl`, un guion, un cliente HTTP). Lo que SÍ se lee sin nada de eso es esta documentación, que por eso está publicada en abierto.
- Contrato OpenAPI (público, sin clave)
- https://academia.derechovirtual.org/openapi.json91 operaciones · 53 rutas · 28 ámbitos. Sirve para generar un cliente sin escribir una línea.
- Esta misma página, en texto plano
- https://academia.derechovirtual.org/llms.txt
Por dónde se empieza
Con una credencial delante, la primera llamada es siempre a la raíz: contesta en castellano qué autoriza esa clave EXACTAMENTE, qué no autoriza y qué ámbito le falta para cada cosa. Eso no se puede deducir leyendo esta página, porque depende de la clave.
curl -s https://academia.derechovirtual.org/api/admin/v1 -H "Authorization: Bearer $ACADEMIA_API_KEY"Lo PRIMERO, siempre: preguntarle a la raíz qué autoriza TU clave. Contesta en castellano, con lo que puede y lo que no, y el ámbito que le falta para cada cosa.
En una variable de entorno, por ejemplo `ACADEMIA_API_KEY`. No la escribas dentro de un guion, ni la pegues en un chat, ni la dejes en el historial del terminal.
- Empieza SIEMPRE leyendo esta raíz: aquí está todo lo que necesitas saber antes de operar.
- Antes de cambiar algo, léelo. La lectura te da el `ETag` que la escritura necesita.
- Si una llamada falla, lee el mensaje antes de reintentar: casi todos dicen qué hacer.
- No inventes rutas. Las que existen están en el contrato que sirve esta misma API.
- Los precios y el IVA se tocan con las cabeceras de confirmación puestas, nunca sin ellas.
- Si te falta un permiso, se pide; no se rodea.
Las cabeceras, y cuándo son obligatorias
No son adorno: sin la que toque, la llamada se rechaza con un 428 antes de tocar nada. Son el freno contra el reintento que cobra dos veces, contra pisar el trabajo de otro y contra el dedo que resbala en un precio.
Authorization
SIEMPRE, en todas las llamadas.
Bearer <tu credencial>. Sin ella todo responde 401.
Idempotency-Key
En TODA escritura (cualquier cosa que no sea una lectura).
Un identificador único que generas tú, uno por operación. Si repites la llamada con la misma clave y el mismo cuerpo, no se ejecuta dos veces: te devuelve el resultado de la primera durante 24 horas.
If-Match
En toda escritura que cambie algo que YA existe. Las altas no la llevan.
El `ETag` que te devolvió la lectura de ese mismo recurso, entre comillas. Si falta: 428. Si alguien lo cambió mientras tanto: 412, y entonces relees y decides — no lo pises.
X-Confirm-Amount-Change
Sólo al cambiar el importe de un producto.
El importe NUEVO, literal y en céntimos. Es el freno contra el dedo que resbala: hay que escribir la cifra dos veces.
X-Confirm-Value-Change
Sólo al cambiar la cuantía de un cupón.
La cuantía NUEVA, literal (puntos de porcentaje o céntimos, según el tipo del cupón).
X-Confirm-Tax-Rate
SIEMPRE que se toque el IVA, aunque el tipo no cambie.
El tipo NUEVO en puntos básicos enteros (2100 = 21 %).
Formato. Todo entra y sale en JSON (UTF-8). Cada respuesta trae `X-Request-Id`, que localiza la petición en la auditoría sin citar a nadie.
Paginación y caudal
- Las listas aceptan `?limit=` (1 a 100, por defecto 50) y `?cursor=`.
- La respuesta trae `page.next_cursor`. Si no es nulo, repite la MISMA consulta con los MISMOS filtros añadiendo ese cursor.
- Se ordena por identificador. El orden editorial vive en el campo `position` de cada elemento.
- Lecturas: 60 peticiones por minuto y por clave.
- Escrituras: 20 por minuto y por clave.
- Al pasarse: 429 con la cabecera `Retry-After` en segundos. Espera ese tiempo; no reintentes en bucle.
- Tamaño: Máximo 100 elementos por página y 2 MB por respuesta.
Los errores, y qué hacer con cada uno
Ninguno se arregla reintentando en bucle. Cada línea dice qué significa y cuál es el siguiente paso correcto; si un cliente hace eso, no hay ningún error de esta API que lo deje atascado.
401
La credencial no vale: es falsa, ha caducado o la han revocado.
Qué hacer: Para. Pide una credencial nueva; reintentar no arregla esto.
403
La credencial es buena, pero le falta el permiso para esta operación.
Qué hacer: El mensaje del error dice exactamente qué ámbito falta. Pide una clave que lo lleve; no busques otra ruta para colarte.
404
Eso no existe… o la API está apagada del todo.
Qué hacer: Si TODO responde 404 con una credencial que sabes buena, es el interruptor general, no tu llamada.
409
Conflicto: el estado actual no admite lo que pides (por ejemplo, borrar un curso que alguien compró).
Qué hacer: Lee el mensaje: dice qué lo impide. No insistas con la misma llamada.
412
Mandaste `If-Match` con una versión vieja: alguien tocó el recurso entre tu lectura y tu escritura.
Qué hacer: Vuelve a leer, mira si tu cambio sigue teniendo sentido y repite con el `ETag` nuevo.
415
El cuerpo no viene como `application/json`.
Qué hacer: Pon la cabecera `Content-Type: application/json`.
422
El cuerpo o los parámetros no cuadran con lo que pide el contrato.
Qué hacer: Mira el contrato filtrado a tus ámbitos y corrige el cuerpo.
428
Falta una cabecera obligatoria de las de arriba.
Qué hacer: El mensaje dice cuál falta y con qué valor. Ponla y repite.
429
Demasiadas peticiones.
Qué hacer: Espera los segundos que diga `Retry-After` y sigue.
503
Una dependencia no está disponible ahora mismo.
Qué hacer: Reintenta más tarde, espaciando los intentos.
Qué se puede hacer, por áreas de negocio
Las 91 operaciones, agrupadas por el área a la que pertenece el permiso que exige cada una. El ámbito escrito al lado es el que tu clave tiene que llevar; si no lo lleva, esa operación responde 403 diciendo cuál le falta.
El servicio: por dónde se empieza
GET /
Qué es esta API, a qué proyecto pertenece y qué puede hacer EXACTAMENTE la clave que pregunta: capacidades en castellano por área de negocio, lo que NO puede con el ámbito que le falta, las cabeceras obligatorias, qué hacer ante cada error y órdenes ejecutables para comprobarlo. Nunca habla de otra clave ni de ningún alumno.
exige catalog:read
GET /openapi.json
Este mismo contrato, FILTRADO a los ámbitos de la clave que pregunta (sólo las operaciones que puede usar, con los esquemas que citan). Con ?completo=1 llega entero.
exige catalog:read
Catálogo: cursos, secciones y lecciones
catalog:readVer el catálogo entero
- Ver todos los cursos de la academia, incluidos los que todavía están en borrador
- Ver el temario de un curso: sus secciones, sus lecciones y el orden en que van
- Saber si un curso o una lección está publicado o aún no se ve
catalog:writeCrear y editar cursos
- Crear un curso nuevo, que nace en borrador y no se vende ni se ve hasta que se publica
- Cambiar el título, la descripción o la imagen de portada de un curso
- Crear secciones y lecciones dentro de un curso, y cambiarles el nombre
- Subir una portada nueva para un curso
catalog:publishPublicar y despublicar
- Publicar un curso para que aparezca en la academia, o retirarlo de la circulación
- Publicar o retirar una lección suelta sin tocar el resto del curso
- Marcar una lección como muestra gratuita, para que la vea quien todavía no ha comprado
catalog:orderOrdenar el temario
- Cambiar el orden en que se presentan las secciones de un curso
- Cambiar el orden de las lecciones dentro de una sección
- Mover una lección de una sección a otra del mismo curso
catalog:deleteBorrar cursos, secciones y lecciones (no tiene vuelta atrás)
- Borrar una lección con todo lo que cuelga de ella
- Borrar una sección entera, con sus lecciones y su contenido
- Borrar un curso completo, siempre que nadie lo haya comprado ni esté matriculado en él
- Arrastra consigo
catalog:write: no se concede sin él.
GET /courses
Lista los cursos, borradores incluidos.
exige catalog:read
POST /courses
Crea un curso en borrador.
exige catalog:write
GET /courses/{courseId}
Devuelve un curso con su descripción.
exige catalog:read
PATCH /courses/{courseId}
Edita título, descripción o portada.
exige catalog:write
DELETE /courses/{courseId}
Borra el curso entero. Exige ?confirmar=N; con matrículas, pedidos, producto, pack o progreso: 409.
exige catalog:delete
POST /courses/{courseId}/status
Publica o despublica un curso.
exige catalog:publish
GET /courses/{courseId}/modules
Lista las secciones de un curso.
exige catalog:read
POST /courses/{courseId}/modules
Crea una sección al final del temario.
exige catalog:write
GET /courses/{courseId}/modules/order
Orden actual de las secciones y su versión.
exige catalog:read
POST /courses/{courseId}/modules/order
Reordena las secciones. Exige la lista completa.
exige catalog:order
GET /modules/{moduleId}
Devuelve una sección.
exige catalog:read
PATCH /modules/{moduleId}
Edita el título o la descripción de una sección.
exige catalog:write
DELETE /modules/{moduleId}
Borra la sección con sus lecciones y bloques. Exige ?confirmar=N con el recuento real de bloques.
exige catalog:delete
GET /modules/{moduleId}/lessons
Lista las lecciones de una sección.
exige catalog:read
POST /modules/{moduleId}/lessons
Crea una lección al final de la sección.
exige catalog:write
GET /modules/{moduleId}/lessons/order
Orden actual de las lecciones y su versión.
exige catalog:read
POST /modules/{moduleId}/lessons/order
Reordena las lecciones. Exige la lista completa.
exige catalog:order
GET /lessons/{lessonId}
Metadatos de una lección.
exige catalog:read
PATCH /lessons/{lessonId}
Renombra una lección.
exige catalog:write
DELETE /lessons/{lessonId}
Borra la lección y sus bloques. Exige ?confirmar=N con el recuento real de bloques.
exige catalog:delete
PATCH /lessons/{lessonId}/move
Mueve la lección a otra sección del MISMO curso. If-Match: la versión del orden de la sección de ORIGEN.
exige catalog:order
POST /courses/{courseId}/image/uploads
Firma la subida de una portada (bucket público course-covers) y devuelve la URL pública para PATCH image_url.
exige catalog:write
POST /lessons/{lessonId}/status
Publica o despublica una lección.
exige catalog:publish
POST /lessons/{lessonId}/preview
Marca la lección como muestra gratuita.
exige catalog:publish
Contenido: lo que el alumno lee y se descarga
content:readLeer lo que hay escrito en las lecciones
- Leer el texto y el código de cualquier lección, tal como lo ve el alumno
content:writeEscribir y corregir las lecciones
- Añadir texto o fragmentos de código a una lección
- Corregir o reescribir por completo lo que ya estaba escrito
- Quitar un trozo de una lección que sobra o que está mal
- Arrastra consigo
content:read: no se concede sin él.
media:readVer los documentos y adjuntos
- Ver qué apuntes, esquemas e imágenes se puede descargar el alumno en cada lección
media:writeSubir y reemplazar documentos
- Subir un documento, un esquema o una imagen a una lección
- Reemplazar el fichero de un documento por una versión corregida, sin cambiarlo de sitio
- Retirar un documento que se subió por esta misma vía
- Arrastra consigo
media:read: no se concede sin él.
media:purgeRetirar también el material heredado de Teachable
- Retirar apuntes e imágenes que vinieron de Teachable, que son casi todo el material vivo
- Arrastra consigo
media:write: no se concede sin él.
GET /blocks/{blockId}
Devuelve un bloque nativo con su versión.
exige content:read
PATCH /blocks/{blockId}
Edita el contenido de un bloque nativo.
exige content:write
DELETE /blocks/{blockId}
Borra un bloque de texto, código o embed. Vídeo de Bunny: 403 (desenlazar primero). Heredado: 409.
exige content:write
GET /lessons/{lessonId}/blocks
Lista los bloques de una lección.
exige content:read
POST /lessons/{lessonId}/blocks
Añade un bloque de texto, de código o embebido.
exige content:write
GET /lessons/{lessonId}/documents
Lista los documentos de una lección.
exige media:read
POST /lessons/{lessonId}/documents
PASO 3 de 3: registra el documento y lo cuelga de la lección. `source.storage_path` es el que devolvió el ticket del paso 1, y el objeto tiene que estar YA subido (paso 2) o responde 422 storage_object_missing. También admite `source.url` para enlazar uno externo, y entonces no hacen falta los pasos 1 y 2.
exige media:write
POST /lessons/{lessonId}/documents/uploads
PASO 1 de 3: firma un permiso de subida. NO registra nada todavía — devuelve un ticket con `upload_url` (paso 2: PUT de los bytes con el `content_type` del ticket) y `storage_path` (paso 3: POST /lessons/{lessonId}/documents con `source.storage_path`). Sólo `filename` es obligatorio; `content_type` se deduce de la extensión y `size_bytes` sólo sirve para que te avisen antes de subir.
exige media:write
GET /documents/{documentId}
Devuelve un documento y su versión.
exige media:read
PUT /documents/{documentId}
Reemplaza el fichero de un documento.
exige media:write
DELETE /documents/{documentId}
Descuelga el documento. Lo heredado exige media:purge.
exige media:write
Cuestionarios: los tests de cada lección
quizzes:readVer los cuestionarios con el solucionario
- Ver el test de una lección con sus preguntas, sus opciones y cuál es la respuesta buena
quizzes:writeCrear y cambiar cuestionarios
- Crear el test de una lección, o sustituirlo entero por otro
- Cambiar la nota que hace falta para aprobar y cuántos intentos tiene el alumno
- Decidir si el alumno ve en qué ha fallado al terminar
- Borrar un test, siempre que ningún alumno lo haya contestado todavía
- Arrastra consigo
quizzes:read: no se concede sin él.
GET /lessons/{lessonId}/quiz
El cuestionario de la lección CON las respuestas correctas (quiz:null si no hay). Su ETag es la preimagen de PUT, PATCH y DELETE.
exige quizzes:read
PUT /lessons/{lessonId}/quiz
Crea (201) o reemplaza (200) el cuestionario COMPLETO. Cada pregunta exige al menos una correcta y sin opciones duplicadas. Los intentos de alumnos SOBREVIVEN al reemplazo.
exige quizzes:write
PATCH /lessons/{lessonId}/quiz
Edita sólo los ajustes: título, si puntúa, nota de corte, reintentos y feedback.
exige quizzes:write
DELETE /lessons/{lessonId}/quiz
Borra el cuestionario. Con intentos de alumnos: 403 attempts_exist (histórico académico; se decide en el panel). Preimagen en la auditoría.
exige quizzes:write
Vídeo: las clases grabadas
video:readVer qué clases grabadas hay
- Ver qué vídeos tiene cada lección y cuánto duran
video:writeColgar y reemplazar clases grabadas
- Colgar en una lección un vídeo que ya esté subido al servidor de vídeo
- Sustituir el vídeo de una lección por una grabación mejor
- Descolgar un vídeo que se colgó por esta misma vía
- Arrastra consigo
video:read: no se concede sin él.
video:purgeDescolgar también los vídeos heredados de Teachable
- Descolgar clases grabadas que vinieron de Teachable, que son casi todas las que hay
- Arrastra consigo
video:write: no se concede sin él.
video:ingestProponer subidas de vídeo, nunca ejecutarlas
- Ver la cola de vídeos pendientes de subir y en qué estado está cada uno
- Proponer que se suba un vídeo, con el coste mensual que calcula el servidor. La subida la aprueba y la lanza una persona, jamás esta clave
- Arrastra consigo
video:read: no se concede sin él.
GET /lessons/{lessonId}/videos
Lista los vídeos de una lección.
exige video:read
POST /lessons/{lessonId}/videos
Cuelga un vídeo de Bunny de la lección.
exige video:write
GET /videos/{videoId}
Devuelve un vídeo y su versión.
exige video:read
PUT /videos/{videoId}
Reemplaza el vídeo de un bloque.
exige video:write
DELETE /videos/{videoId}
Descuelga el vídeo. Lo heredado exige video:purge.
exige video:write
GET /video/ingest-requests
Lista la cola de ingesta de vídeo (filtro ?status=).
exige video:ingest
POST /video/ingest-requests
Propone una ingesta con su coste mensual estimado. NUNCA la ejecuta.
exige video:ingest
Comercio: precios, cupones, packs y ofertas
commerce:readConsultar precios, cupones y packs
- Ver todo lo que está a la venta y lo que está apagado, con su precio
- Ver las formas de pago de un producto y cuál es la que cobra la página de pago
- Ver los cupones de descuento y en qué estado están: activo, caducado o agotado
- Ver los packs, qué cursos lleva cada uno, y las ofertas y recomendaciones publicadas
commerce:writeCambiar precios y lo que se vende
- Crear un producto nuevo y, con él, su página de pago
- Cambiar el precio de un producto. Obliga a escribir el importe nuevo dos veces
- Encender o apagar la venta de algo sin borrarlo
- Crear y editar formas de pago: pago único, a plazos o suscripción, y archivarlas
- Fijar el precio tachado que se enseña al lado del precio de venta, con su procedencia
- Montar un pack y decidir exactamente qué cursos lleva
- Publicar ofertas y las recomendaciones que se le enseñan a quien está comprando
- Arrastra consigo
commerce:read: no se concede sin él.
commerce:orderReordenar las formas de pago (cambia lo que se cobra)
- Cambiar el orden de las formas de pago de un producto. Ojo, porque la página de pago cobra la primera, así que esto cambia lo que paga quien entra por el enlace de siempre
- Arrastra consigo
commerce:write: no se concede sin él.
coupons:writeEmitir y apagar cupones de descuento
- Emitir un cupón, para un curso concreto o para todo el catálogo
- Cambiar la cuantía, la caducidad o el tope de usos de un cupón. Obliga a escribir la cuantía nueva dos veces
- Desactivar o reactivar un cupón. Un cupón nunca se borra, porque su código está impreso en campañas
- Arrastra consigo
commerce:read: no se concede sin él.
GET /products
Lista los productos a la venta y los apagados.
exige commerce:read
POST /products
Crea un producto SIN llamar a Stripe (el Price recurrente lo crea el primer cobro). Crear el producto ES crear su checkout: la respuesta añade checkout_url. 409 duplicate_checkout si el destino ya tiene otro producto activo.
exige commerce:write
GET /products/{productId}
Producto, su plan por defecto y si el escaparate cuadra.
exige commerce:read
PATCH /products/{productId}
Edita título, importe anunciado o el interruptor de venta. kind, mode, divisa y destino son inmutables. Cambiar amount_cents exige X-Confirm-Amount-Change (sin ella 428). 409 strikethrough_conflict con tachado publicado; 409 showcase_mismatch si descuadra. Sin DELETE: deliberado.
exige commerce:write
GET /products/{productId}/strikethrough
El precio tachado publicado (o null), SIEMPRE con el precio de venta contra el que se mide. Su ETag es la preimagen del PUT y del DELETE: cambiar el precio del producto la invalida.
exige commerce:read
PUT /products/{productId}/strikethrough
Fija o reemplaza el tachado (UNO por producto), con procedencia obligatoria. precio_venta_cents y currency los pone el servidor. 409 strikethrough_below_price si no SUPERA el precio. 201 al crear, 200 al reemplazar.
exige commerce:write
DELETE /products/{productId}/strikethrough
Quita el tachado (único borrado físico de la familia: decoración que ningún pedido cita). La fila entera queda en admin_api_audit.detail. Responde 200 con el documento (strikethrough:null). 404 si no hay tachado.
exige commerce:write
POST /products/{productId}/active
Enciende o apaga un producto. 409 si el escaparate no cuadraría.
exige commerce:write
GET /products/{productId}/plans
Lista los planes de precio de un producto.
exige commerce:read
POST /products/{productId}/plans
Crea un plan de precio, siempre el último por posición.
exige commerce:write
GET /products/{productId}/plans/order
Orden actual de los planes y su versión.
exige commerce:read
POST /products/{productId}/plans/order
Reordena los planes. CAMBIA EL PRECIO POR DEFECTO.
exige commerce:order
GET /plans/{planId}
Devuelve un plan de precio y su versión.
exige commerce:read
PATCH /plans/{planId}
Edita nombre, importe, tope o visibilidad. Tipo y cuotas son inmutables.
exige commerce:write
POST /plans/{planId}/archived
Archiva o restaura un plan. No lo borra nunca.
exige commerce:write
GET /coupons
Lista los cupones de descuento.
exige commerce:read
POST /coupons
Emite un cupón. Sin course_id vale para todo el catálogo.
exige coupons:write
GET /coupons/{couponId}
Cupón con su estado real (caducado, agotado, desactivado).
exige commerce:read
PATCH /coupons/{couponId}
Edición SEGURA: valor, caduca_en, usos_maximos y activo — la lista literal del dueño. El CÓDIGO no se edita jamás (identidad impresa en URLs de venta); tampoco tipo, nombre ni course_id. Cambiar valor exige X-Confirm-Value-Change (sin ella 428). Los pedidos cobrados conservan su descuento_cents congelado; el trigger sube config_revision y la fila previa queda en admin_api_audit.detail. Aviso `warnings` si la cuantía nueva dejaría cobros bajo 0,50 €. Sin DELETE: un cupón se apaga, nunca se borra.
exige coupons:write
POST /coupons/{couponId}/active
Desactiva o reactiva un cupón. No lo borra nunca.
exige coupons:write
GET /bundles
Lista los packs.
exige commerce:read
POST /bundles
Crea un pack VACÍO, despublicado y sin listar, en última posición. Crear no es vender.
exige commerce:write
GET /bundles/{bundleId}
Pack con su composición congelada.
exige commerce:read
PUT /bundles/{bundleId}/composition
Fija la composición COMPLETA y ordenada del pack. Pack contenido en otro (SUMMER⊂BONUS): warning, no bloqueo. Lo vendido está congelado en order_bundle_courses.
exige commerce:write
GET /offers
Lista las ofertas (bump y upsell). Filtro ?active=.
exige commerce:read
POST /offers
Publica una oferta nueva envolviendo admin_publicar_oferta_v1. Descuento>0 exige el interruptor encendido (409 not_chargeable).
exige commerce:write
GET /offers/{offerId}
Devuelve una oferta y su versión (sin source_reference).
exige commerce:read
PUT /offers/{offerId}
Reemplaza la configuración COMPLETA de la oferta. Despublicar = activa:false. No hay DELETE: deliberado.
exige commerce:write
GET /courses/{courseId}/recommendations
El conjunto de recomendaciones del curso, en orden. Su ETag es la huella oficial: este GET ES la preimagen del PUT.
exige commerce:read
PUT /courses/{courseId}/recommendations
Reemplaza la lista COMPLETA (máx. 6). activa=true exige ambos cursos publicados (409 target_not_published). >4: aviso, no bloqueo.
exige commerce:write
Fiscal: el IVA de las facturas
fiscal:readConsultar el IVA vigente
- Ver con qué tipo de IVA se están emitiendo las facturas ahora mismo
fiscal:writeCambiar el IVA de las facturas futuras
- Cambiar el tipo de IVA de todas las facturas que se emitan a partir de ese momento. Obliga a escribir el tipo nuevo dos veces, y las facturas ya emitidas no cambian jamás
- Arrastra consigo
fiscal:read: no se concede sin él.
GET /fiscal/iva
El tipo de IVA vigente en puntos básicos enteros (2100 = 21 %), con el precepto de exención si el tipo es 0 y las facturas ya emitidas. El documento existe SIEMPRE (configurado:false si aún manda la env INVOICE_IVA_RATE); su ETag es la preimagen del PUT y NO incluye facturas_emitidas (facturar no cambia el tipo).
exige fiscal:read
PUT /fiscal/iva
Fija el tipo de IVA de TODAS las facturas futuras. Exige If-Match Y X-Confirm-Tax-Rate con los puntos básicos nuevos, literales (sin ella 428; se exige SIEMPRE). Tipo 0 sin precepto: 422 (art. 6.1.j RD 1619/2012); precepto con tipo distinto de 0: 422. La respuesta lleva `aviso`: las facturas ya emitidas no cambian (tax_rate congelado fila a fila). Antes/después en admin_api_audit.detail. 201 la primera vez, 200 después.
exige fiscal:write
Alumnos: quién estudia, qué ha pagado y por dónde va
students:readBuscar alumnos
- Buscar y listar a las personas con cuenta en la academia: nombre, correo y fecha de alta
enrollments:readVer a qué cursos tiene acceso alguien
- Ver las matrículas de una persona: a qué cursos entra y hasta cuándo
progress:readVer por dónde va un alumno
- Ver cuánto lleva hecho un alumno de un curso
- Ver el progreso de un alumno lección a lección, una por una
payments:readVer pagos y facturas de una persona
- Ver lo que ha pagado alguien, con su factura y el cupón de descuento que usó
- Arrastra consigo
students:read: no se concede sin él.
students:piiVer la ficha personal completa (datos personales)
- Ver los datos de facturación y la ficha personal de un alumno
- Ver si llegó a activar su cuenta y cuál ha sido su actividad en la academia
- Arrastra consigo
students:read: no se concede sin él.
enrollments:writeMATRICULAR y revocar matrículas (entrega el producto: es regalar dinero)
- Dar acceso a un curso o a un pack a partir de un correo, exista ya la cuenta o no
- Entregarlo como una compra normal: el alumno recibe su código de 6 dígitos y entra por /activar
- Darlo en silencio y en el acto, para migraciones, cuando la persona ya tiene cuenta
- Quitarle el acceso a un curso a alguien, y devolvérselo, siempre con un motivo escrito
- Arrastra consigo
enrollments:read: no se concede sin él.
GET /students
Lista el alumnado con cuenta (id, correo, nombre y alta).
exige students:read
GET /students/{studentId}
Datos básicos de un alumno.
exige students:read
GET /students/{studentId}/profile
FICHA COMPLETA: facturación y datos heredados. Datos personales.
exige students:pii
GET /students/{studentId}/account
Si reclamó su cuenta, su actividad y lo que ha pagado.
exige students:pii
GET /students/{studentId}/payments
Pedidos con su factura y el cupón que usaron.
exige payments:read
GET /students/{studentId}/enrollments
Matrículas de un alumno.
exige enrollments:read
GET /students/{studentId}/enrollments/{courseId}
Una matrícula concreta, con su versión.
exige enrollments:read
GET /students/{studentId}/courses/{courseId}/progress
Resumen de progreso en un curso.
exige progress:read
GET /students/{studentId}/courses/{courseId}/progress/lessons
Lecciones completadas, una a una.
exige progress:read
POST /enrollments
MATRICULA a un correo en un curso o en un pack. Enrola y matricula en un paso: si el correo ya tiene cuenta se localiza (nunca se duplica) y si no, la cuenta nace cuando el alumno canjee el código. Entrega por la vía estándar de la Academia (pedido → código de 6 dígitos → correo → /activar): `notificar` es TRUE por defecto. Con `notificar:false` concede el acceso en el acto y sin correo, y entonces exige que el alumno ya tenga cuenta (409 alumno_sin_cuenta). `termino` y `motivo` son OBLIGATORIOS; `termino:'hasta_fecha'` responde 409 termino_no_ejecutable (enrollments no tiene caducidad). Exige X-Confirm-Enrollment con el destino literal (sin ella 428). Idempotente por estado: repetir devuelve 200 con creada:false y NO manda un segundo correo. Tope diario por clave que falla cerrado (409 tope_diario). Sin If-Match: no hay documento previo que versionar.
exige enrollments:write
POST /students/{studentId}/enrollments/{courseId}/active
REVOCA (active:false) o reactiva (active:true) el acceso de un alumno a un curso. Apaga la matrícula, nunca borra la fila. Exige If-Match con la versión que devolvió getStudentEnrollment, X-Confirm-Enrollment-Change con el `active` NUEVO literal (cabecera DISTINTA de la de matricular, a propósito) y `motivo`. Reactivar cuenta contra el tope diario; revocar no. La preimagen queda en admin_api_audit.detail y en admin_audit_log.
exige enrollments:write
Ejemplos que funcionan
Copiables tal cual. La credencial entra por la variable de entorno ACADEMIA_API_KEY y no se escribe dentro de ninguna orden. Los identificadores entre <…> son huecos: los de verdad te los da la lectura que hagas tú.
Lo PRIMERO, siempre: preguntarle a la raíz qué autoriza TU clave. Contesta en castellano, con lo que puede y lo que no, y el ámbito que le falta para cada cosa.
curl -s https://academia.derechovirtual.org/api/admin/v1 -H "Authorization: Bearer $ACADEMIA_API_KEY"Leer el catálogo, borradores incluidos. Toda clave puede hacerlo, así que sirve para confirmar que la credencial vale.
curl -s "https://academia.derechovirtual.org/api/admin/v1/courses?limit=5" -H "Authorization: Bearer $ACADEMIA_API_KEY"Seguir la paginación: repetir la MISMA consulta con los MISMOS filtros, añadiendo el cursor que vino en page.next_cursor.
curl -s "https://academia.derechovirtual.org/api/admin/v1/courses?limit=50&cursor=<el-cursor-que-te-devolvio-la-pagina-anterior>" -H "Authorization: Bearer $ACADEMIA_API_KEY"Una ESCRITURA con las tres cautelas puestas: idempotencia, versión leída y tipo de contenido. Léelo antes para tener el ETag; sin If-Match responde 428.
curl -s -X PATCH https://academia.derechovirtual.org/api/admin/v1/lessons/<el-id-que-te-dio-la-lectura> \
-H "Authorization: Bearer $ACADEMIA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'If-Match: "<el-etag-que-te-devolvio-la-lectura>"' \
-H "Content-Type: application/json" \
-d '{"title":"El título nuevo"}'Descargar el contrato completo. Esta orden NO lleva credencial: el contrato es público. Con una clave, /api/admin/v1/openapi.json te lo sirve ya recortado a tus permisos.
curl -s https://academia.derechovirtual.org/openapi.jsonLo que NO vas a encontrar en esta página
- Ninguna credencial, ni siquiera de ejemplo. Las claves las emite el dueño desde el panel con su sesión de administrador, el secreto se enseña una sola vez y viaja SIEMPRE en una variable de entorno, nunca escrito dentro de un guion ni pegado en un chat.
- Ningún dato de la academia: ni un alumno, ni un curso, ni un precio, ni un pedido, ni un identificador real. Los identificadores de los ejemplos son huecos; los de verdad te los da la lectura que hagas tú.
- Ninguna ruta que no sea de esta API. El panel de administración, los webhooks de cobro, las tareas programadas y las descargas firmadas no se documentan aquí y no son parte de este contrato.
- Ningún atajo. Todo lo que hay debajo responde 401 sin credencial y 403 si la credencial no lleva el ámbito; esta página no cambia eso, sólo lo explica.
Esta página se genera del mismo contrato y del mismo código que atiende las llamadas, así que no puede quedarse desfasada: una operación o un permiso nuevos aparecen aquí solos, y un centinela de la batería de pruebas se pone rojo si alguna vez dejaran de aparecer.