# Academia Derecho Virtual — API de administración v1 > É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. 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. - Web: https://academia.derechovirtual.org - Base de la API: https://academia.derechovirtual.org/api/admin/v1 - Contrato OpenAPI completo, público y sin clave: https://academia.derechovirtual.org/openapi.json - Documentación en HTML: https://academia.derechovirtual.org/api - Tamaño: 91 operaciones en 53 rutas, 28 ámbitos de permiso. ## Autenticación Manda la credencial en la cabecera `Authorization: Bearer `. 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. 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. 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. 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. EMPIEZA AQUÍ: con la clave delante, la primera llamada es siempre a la raíz. Cuenta qué autoriza ESA clave exactamente, qué no autoriza y qué ámbito le falta para cada cosa. Eso no se puede deducir de este documento, porque depende de la clave. ```bash curl -s https://academia.derechovirtual.org/api/admin/v1 -H "Authorization: Bearer $ACADEMIA_API_KEY" ``` ## Reglas de la casa - 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. ## Cabeceras obligatorias ### Authorization - Cuándo: SIEMPRE, en todas las llamadas. - Valor: Bearer . Sin ella todo responde 401. ### Idempotency-Key - Cuándo: En TODA escritura (cualquier cosa que no sea una lectura). - Valor: 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 - Cuándo: En toda escritura que cambie algo que YA existe. Las altas no la llevan. - Valor: 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 - Cuándo: Sólo al cambiar el importe de un producto. - Valor: 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 - Cuándo: Sólo al cambiar la cuantía de un cupón. - Valor: La cuantía NUEVA, literal (puntos de porcentaje o céntimos, según el tipo del cupón). ### X-Confirm-Tax-Rate - Cuándo: SIEMPRE que se toque el IVA, aunque el tipo no cambie. - Valor: El tipo NUEVO en puntos básicos enteros (2100 = 21 %). ## Paginación - 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. ## Caudal - 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. ## Errores: qué significan y qué hacer - 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. ## Operaciones por área de negocio Cada operación lleva el ámbito que exige. Sin ese ámbito responde 403 diciendo cuál falta. ### El servicio: por dónde se empieza - `GET /` [catalog:read] — 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. - `GET /openapi.json` [catalog:read] — 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. ### Catálogo: cursos, secciones y lecciones Ámbito `catalog:read` — Ver 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 Ámbito `catalog:write` — Crear 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 Ámbito `catalog:publish` — Publicar 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 Ámbito `catalog:order` — Ordenar 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 Ámbito `catalog:delete` — Borrar cursos, secciones y lecciones (no tiene vuelta atrás) (arrastra `catalog:write`) - 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 - `GET /courses` [catalog:read] — Lista los cursos, borradores incluidos. - `POST /courses` [catalog:write] — Crea un curso en borrador. - `GET /courses/{courseId}` [catalog:read] — Devuelve un curso con su descripción. - `PATCH /courses/{courseId}` [catalog:write] — Edita título, descripción o portada. - `DELETE /courses/{courseId}` [catalog:delete] — Borra el curso entero. Exige ?confirmar=N; con matrículas, pedidos, producto, pack o progreso: 409. - `POST /courses/{courseId}/status` [catalog:publish] — Publica o despublica un curso. - `GET /courses/{courseId}/modules` [catalog:read] — Lista las secciones de un curso. - `POST /courses/{courseId}/modules` [catalog:write] — Crea una sección al final del temario. - `GET /courses/{courseId}/modules/order` [catalog:read] — Orden actual de las secciones y su versión. - `POST /courses/{courseId}/modules/order` [catalog:order] — Reordena las secciones. Exige la lista completa. - `GET /modules/{moduleId}` [catalog:read] — Devuelve una sección. - `PATCH /modules/{moduleId}` [catalog:write] — Edita el título o la descripción de una sección. - `DELETE /modules/{moduleId}` [catalog:delete] — Borra la sección con sus lecciones y bloques. Exige ?confirmar=N con el recuento real de bloques. - `GET /modules/{moduleId}/lessons` [catalog:read] — Lista las lecciones de una sección. - `POST /modules/{moduleId}/lessons` [catalog:write] — Crea una lección al final de la sección. - `GET /modules/{moduleId}/lessons/order` [catalog:read] — Orden actual de las lecciones y su versión. - `POST /modules/{moduleId}/lessons/order` [catalog:order] — Reordena las lecciones. Exige la lista completa. - `GET /lessons/{lessonId}` [catalog:read] — Metadatos de una lección. - `PATCH /lessons/{lessonId}` [catalog:write] — Renombra una lección. - `DELETE /lessons/{lessonId}` [catalog:delete] — Borra la lección y sus bloques. Exige ?confirmar=N con el recuento real de bloques. - `PATCH /lessons/{lessonId}/move` [catalog:order] — Mueve la lección a otra sección del MISMO curso. If-Match: la versión del orden de la sección de ORIGEN. - `POST /courses/{courseId}/image/uploads` [catalog:write] — Firma la subida de una portada (bucket público course-covers) y devuelve la URL pública para PATCH image_url. - `POST /lessons/{lessonId}/status` [catalog:publish] — Publica o despublica una lección. - `POST /lessons/{lessonId}/preview` [catalog:publish] — Marca la lección como muestra gratuita. ### Contenido: lo que el alumno lee y se descarga Ámbito `content:read` — Leer lo que hay escrito en las lecciones - Leer el texto y el código de cualquier lección, tal como lo ve el alumno Ámbito `content:write` — Escribir y corregir las lecciones (arrastra `content:read`) - 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 Ámbito `media:read` — Ver los documentos y adjuntos - Ver qué apuntes, esquemas e imágenes se puede descargar el alumno en cada lección Ámbito `media:write` — Subir y reemplazar documentos (arrastra `media:read`) - 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 Ámbito `media:purge` — Retirar también el material heredado de Teachable (arrastra `media:write`) - Retirar apuntes e imágenes que vinieron de Teachable, que son casi todo el material vivo - `GET /blocks/{blockId}` [content:read] — Devuelve un bloque nativo con su versión. - `PATCH /blocks/{blockId}` [content:write] — Edita el contenido de un bloque nativo. - `DELETE /blocks/{blockId}` [content:write] — Borra un bloque de texto, código o embed. Vídeo de Bunny: 403 (desenlazar primero). Heredado: 409. - `GET /lessons/{lessonId}/blocks` [content:read] — Lista los bloques de una lección. - `POST /lessons/{lessonId}/blocks` [content:write] — Añade un bloque de texto, de código o embebido. - `GET /lessons/{lessonId}/documents` [media:read] — Lista los documentos de una lección. - `POST /lessons/{lessonId}/documents` [media:write] — 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. - `POST /lessons/{lessonId}/documents/uploads` [media:write] — 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. - `GET /documents/{documentId}` [media:read] — Devuelve un documento y su versión. - `PUT /documents/{documentId}` [media:write] — Reemplaza el fichero de un documento. - `DELETE /documents/{documentId}` [media:write] — Descuelga el documento. Lo heredado exige media:purge. ### Cuestionarios: los tests de cada lección Ámbito `quizzes:read` — Ver los cuestionarios con el solucionario - Ver el test de una lección con sus preguntas, sus opciones y cuál es la respuesta buena Ámbito `quizzes:write` — Crear y cambiar cuestionarios (arrastra `quizzes:read`) - 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 - `GET /lessons/{lessonId}/quiz` [quizzes:read] — 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. - `PUT /lessons/{lessonId}/quiz` [quizzes:write] — 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. - `PATCH /lessons/{lessonId}/quiz` [quizzes:write] — Edita sólo los ajustes: título, si puntúa, nota de corte, reintentos y feedback. - `DELETE /lessons/{lessonId}/quiz` [quizzes:write] — Borra el cuestionario. Con intentos de alumnos: 403 attempts_exist (histórico académico; se decide en el panel). Preimagen en la auditoría. ### Vídeo: las clases grabadas Ámbito `video:read` — Ver qué clases grabadas hay - Ver qué vídeos tiene cada lección y cuánto duran Ámbito `video:write` — Colgar y reemplazar clases grabadas (arrastra `video:read`) - 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 Ámbito `video:purge` — Descolgar también los vídeos heredados de Teachable (arrastra `video:write`) - Descolgar clases grabadas que vinieron de Teachable, que son casi todas las que hay Ámbito `video:ingest` — Proponer subidas de vídeo, nunca ejecutarlas (arrastra `video:read`) - 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 - `GET /lessons/{lessonId}/videos` [video:read] — Lista los vídeos de una lección. - `POST /lessons/{lessonId}/videos` [video:write] — Cuelga un vídeo de Bunny de la lección. - `GET /videos/{videoId}` [video:read] — Devuelve un vídeo y su versión. - `PUT /videos/{videoId}` [video:write] — Reemplaza el vídeo de un bloque. - `DELETE /videos/{videoId}` [video:write] — Descuelga el vídeo. Lo heredado exige video:purge. - `GET /video/ingest-requests` [video:ingest] — Lista la cola de ingesta de vídeo (filtro ?status=). - `POST /video/ingest-requests` [video:ingest] — Propone una ingesta con su coste mensual estimado. NUNCA la ejecuta. ### Comercio: precios, cupones, packs y ofertas Ámbito `commerce:read` — Consultar 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 Ámbito `commerce:write` — Cambiar precios y lo que se vende (arrastra `commerce:read`) - 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 Ámbito `commerce:order` — Reordenar las formas de pago (cambia lo que se cobra) (arrastra `commerce:write`) - 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 Ámbito `coupons:write` — Emitir y apagar cupones de descuento (arrastra `commerce:read`) - 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 - `GET /products` [commerce:read] — Lista los productos a la venta y los apagados. - `POST /products` [commerce:write] — 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. - `GET /products/{productId}` [commerce:read] — Producto, su plan por defecto y si el escaparate cuadra. - `PATCH /products/{productId}` [commerce:write] — 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. - `GET /products/{productId}/strikethrough` [commerce:read] — 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. - `PUT /products/{productId}/strikethrough` [commerce:write] — 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. - `DELETE /products/{productId}/strikethrough` [commerce:write] — 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. - `POST /products/{productId}/active` [commerce:write] — Enciende o apaga un producto. 409 si el escaparate no cuadraría. - `GET /products/{productId}/plans` [commerce:read] — Lista los planes de precio de un producto. - `POST /products/{productId}/plans` [commerce:write] — Crea un plan de precio, siempre el último por posición. - `GET /products/{productId}/plans/order` [commerce:read] — Orden actual de los planes y su versión. - `POST /products/{productId}/plans/order` [commerce:order] — Reordena los planes. CAMBIA EL PRECIO POR DEFECTO. - `GET /plans/{planId}` [commerce:read] — Devuelve un plan de precio y su versión. - `PATCH /plans/{planId}` [commerce:write] — Edita nombre, importe, tope o visibilidad. Tipo y cuotas son inmutables. - `POST /plans/{planId}/archived` [commerce:write] — Archiva o restaura un plan. No lo borra nunca. - `GET /coupons` [commerce:read] — Lista los cupones de descuento. - `POST /coupons` [coupons:write] — Emite un cupón. Sin course_id vale para todo el catálogo. - `GET /coupons/{couponId}` [commerce:read] — Cupón con su estado real (caducado, agotado, desactivado). - `PATCH /coupons/{couponId}` [coupons:write] — 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. - `POST /coupons/{couponId}/active` [coupons:write] — Desactiva o reactiva un cupón. No lo borra nunca. - `GET /bundles` [commerce:read] — Lista los packs. - `POST /bundles` [commerce:write] — Crea un pack VACÍO, despublicado y sin listar, en última posición. Crear no es vender. - `GET /bundles/{bundleId}` [commerce:read] — Pack con su composición congelada. - `PUT /bundles/{bundleId}/composition` [commerce:write] — 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. - `GET /offers` [commerce:read] — Lista las ofertas (bump y upsell). Filtro ?active=. - `POST /offers` [commerce:write] — Publica una oferta nueva envolviendo admin_publicar_oferta_v1. Descuento>0 exige el interruptor encendido (409 not_chargeable). - `GET /offers/{offerId}` [commerce:read] — Devuelve una oferta y su versión (sin source_reference). - `PUT /offers/{offerId}` [commerce:write] — Reemplaza la configuración COMPLETA de la oferta. Despublicar = activa:false. No hay DELETE: deliberado. - `GET /courses/{courseId}/recommendations` [commerce:read] — El conjunto de recomendaciones del curso, en orden. Su ETag es la huella oficial: este GET ES la preimagen del PUT. - `PUT /courses/{courseId}/recommendations` [commerce:write] — Reemplaza la lista COMPLETA (máx. 6). activa=true exige ambos cursos publicados (409 target_not_published). >4: aviso, no bloqueo. ### Fiscal: el IVA de las facturas Ámbito `fiscal:read` — Consultar el IVA vigente - Ver con qué tipo de IVA se están emitiendo las facturas ahora mismo Ámbito `fiscal:write` — Cambiar el IVA de las facturas futuras (arrastra `fiscal:read`) - 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 - `GET /fiscal/iva` [fiscal:read] — 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). - `PUT /fiscal/iva` [fiscal:write] — 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. ### Alumnos: quién estudia, qué ha pagado y por dónde va Ámbito `students:read` — Buscar alumnos - Buscar y listar a las personas con cuenta en la academia: nombre, correo y fecha de alta Ámbito `enrollments:read` — Ver a qué cursos tiene acceso alguien - Ver las matrículas de una persona: a qué cursos entra y hasta cuándo Ámbito `progress:read` — Ver 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 Ámbito `payments:read` — Ver pagos y facturas de una persona (arrastra `students:read`) - Ver lo que ha pagado alguien, con su factura y el cupón de descuento que usó Ámbito `students:pii` — Ver la ficha personal completa (datos personales) (arrastra `students:read`) - 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 Ámbito `enrollments:write` — MATRICULAR y revocar matrículas (entrega el producto: es regalar dinero) (arrastra `enrollments:read`) - 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 - `GET /students` [students:read] — Lista el alumnado con cuenta (id, correo, nombre y alta). - `GET /students/{studentId}` [students:read] — Datos básicos de un alumno. - `GET /students/{studentId}/profile` [students:pii] — FICHA COMPLETA: facturación y datos heredados. Datos personales. - `GET /students/{studentId}/account` [students:pii] — Si reclamó su cuenta, su actividad y lo que ha pagado. - `GET /students/{studentId}/payments` [payments:read] — Pedidos con su factura y el cupón que usaron. - `GET /students/{studentId}/enrollments` [enrollments:read] — Matrículas de un alumno. - `GET /students/{studentId}/enrollments/{courseId}` [enrollments:read] — Una matrícula concreta, con su versión. - `GET /students/{studentId}/courses/{courseId}/progress` [progress:read] — Resumen de progreso en un curso. - `GET /students/{studentId}/courses/{courseId}/progress/lessons` [progress:read] — Lecciones completadas, una a una. - `POST /enrollments` [enrollments:write] — 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. - `POST /students/{studentId}/enrollments/{courseId}/active` [enrollments:write] — 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. ## Ejemplos 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. ```bash 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. ```bash 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. ```bash curl -s "https://academia.derechovirtual.org/api/admin/v1/courses?limit=50&cursor=" -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. ```bash curl -s -X PATCH https://academia.derechovirtual.org/api/admin/v1/lessons/ \ -H "Authorization: Bearer $ACADEMIA_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H 'If-Match: ""' \ -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. ```bash curl -s https://academia.derechovirtual.org/openapi.json ``` ## Lo que NO está en este documento - 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.