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.json

Lo 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.