La API es el complemento para crear y actualizar artículos desde otro sistema. Puedes usarla para:
- Migrar un sitio hacia Medios CMS.
- Integrar una agencia de noticias.
- Conectar el medio con otra herramienta que ya uses.
Consulta con un programador de tu confianza para usarla. Los ejemplos están en Python 3, con la librería requests.
TABLA DE CONTENIDOS
- Activar el complemento
- Generar una clave
- Dirección y autenticación
- Errores
- Autores, categorías y etiquetas
- Subir multimedia
- Listar artículos
- Crear un artículo
- Leer un artículo
- Actualizar un artículo
- Eliminar un artículo
- Campos del artículo
- Redirección de URLs
Activar el complemento
Entra a Panel Principal > Complementos > API y pulsa Activar.
Si el plan no incluye la API, el complemento no se puede activar. En ese caso verás el aviso de que no está disponible en el plan actual.
Con el complemento desactivado, todas las llamadas responden 403 y el mensaje «La API no está activada». Desactivarlo no borra las claves: deja de aceptar pedidos hasta que lo vuelvas a activar.
Generar una clave
En la misma pantalla, con el complemento activo, pulsa Generar clave.
- Nombre. Sirve para reconocer la clave en el panel (por ejemplo, «Migración» o «Agencia»).
- Usuario. Una persona del plantel con permiso de administración de artículos y sin moderador. La actividad de la API queda asociada a esa persona y publica directo, sin pasar por moderación.
Al generar, el secreto se muestra una sola vez. Cópialo en ese momento: al cerrar el diálogo no se vuelve a mostrar. Empieza por medios_. En el listado solo queda un prefijo, la fecha de alta y el último uso.
Puedes tener hasta 5 claves. Para crear otra cuando ya hay cinco, revoca una. Revocar corta el acceso de los sistemas que la usaban.
Si no aparece nadie en el selector de usuario, falta una persona del plantel con administración de artículos y sin moderador.
Dirección y autenticación
La base es la dirección pública del sitio:
https://tusitio.com/api/v1Cada pedido lleva la clave en el encabezado Authorization, con el esquema Bearer. No uses usuario y contraseña, ni la clave dentro de la URL.
Los cuerpos de artículos, y también el de eliminar, son JSON (Content-Type: application/json). La subida de multimedia es la excepción: ahí el archivo va en multipart/form-data.
Instala la librería y deja este cliente a mano. Los ejemplos de más abajo lo reutilizan.
pip install requestsimport requests
BASE = "https://tusitio.com/api/v1"
CLAVE = "medios_..."
def api(metodo, ruta, **kwargs):
headers = {"Authorization": f"Bearer {CLAVE}"}
if "files" not in kwargs:
headers["Content-Type"] = "application/json"
respuesta = requests.request(
metodo, BASE + ruta, headers=headers, timeout=60, **kwargs)
return respuesta.status_code, respuesta.json()Los booleanos van como true o false de JSON, y los ids como número, no como texto.
Errores
Cuando un pedido no se puede cumplir, el cuerpo tiene esta forma:
{
"error": {
"codigo": "hay_borrador",
"mensaje": "Hay un borrador abierto de este artículo."
}
}| HTTP | codigo | Cuándo |
|---|---|---|
| 400 | parametro_invalido | Falta un dato, el JSON no se puede leer o un filtro no tiene el formato esperado. |
| 403 | no_autorizado | La API está apagada, falta el Bearer, la clave no es válida o el usuario de la clave ya no puede usarla. |
| 403 | limite_plan | No se puede publicar porque el sitio llegó al máximo de artículos del plan. |
| 403 | sin_espacio | No se puede subir multimedia porque el sitio se quedó sin espacio. |
| 404 | no_encontrado | Esa ruta o ese artículo no existe. |
| 405 | metodo_no_permitido | Ese método HTTP no corresponde a la ruta. La respuesta incluye el encabezado Allow. |
| 409 | conflicto | El artículo cambió desde que lo leíste, o el external_id está repetido o no es de un artículo. |
| 409 | hay_borrador | Alguien tiene un borrador de ese artículo abierto en el panel. |
| 415 | tipo_no_admitido | El archivo no es una imagen, un audio ni un video MP4, WebM u OGG. |
| 422 | validacion | Un campo no cierra: categoría, autor, schema, importancia o un {{multimedia:ID}} que no existe. |
Un alta nueva responde 201. El resto de los aciertos responde 200.
Autores, categorías y etiquetas
Son lecturas. Sirven para elegir ids antes de crear o actualizar una nota. No aceptan POST, PATCH ni DELETE.
GET /autores
Personas del plantel que pueden firmar una nota. No incluye al usuario root.
codigo, datos = api("GET", "/autores")
# datos["autores"] es una lista de {"id": 8, "nombre": "Ana Pérez"}GET /categorias
codigo, datos = api("GET", "/categorias")
# datos["categorias"] es una lista de {"id": 12, "nombre": "Ciudad"}GET /etiquetas
Devuelve hasta 50 etiquetas, ordenadas por nombre. Con q filtra por el texto del nombre.
codigo, datos = api("GET", "/etiquetas", params={"q": "clima"})Subir multimedia
POST /multimedia sube un archivo al centro multimedia. El campo del formulario se llama file. No se acepta una URL ni el archivo en base64.
Admitidos: imágenes, audios y videos MP4, WebM u OGG.
with open("foto.jpg", "rb") as archivo:
codigo, media = api(
"POST",
"/multimedia",
files={"file": ("foto.jpg", archivo)},
)
media_id = media["id"]La respuesta trae id, tipo (imagen, video o audio), titulo, url y html.
Para poner ese archivo dentro del cuerpo, escribe el marcador {{multimedia:ID}} en el HTML. Al guardar el artículo, la API lo reemplaza por el HTML del archivo. Si el id no existe, o no es imagen, video ni audio, el artículo no se guarda.
cuerpo = "<p>El temporal llegó durante la madrugada.</p>{{multimedia:%d}}" % media_idLa imagen principal de la nota y la de redes no se insertan así: se envían como imagen y ogimage, con el id numérico. Lo ves en Crear un artículo.
Listar artículos
GET /articulos devuelve páginas de 50, de la más nueva a la más vieja. La lista no incluye el cuerpo. Tampoco incluye el borrador que alguien tiene abierto en el editor sobre una nota ya publicada.
codigo, datos = api("GET", "/articulos", params={
"estado": "publicado",
"categoria": 12,
"q": "temporal",
"desde": "2026-10-01",
"hasta": "2026-10-16",
"pagina": 1,
})
for articulo in datos["articulos"]:
print(articulo["id"], articulo["titulo"], articulo["url"])
if datos["hay_mas"]:
# pedir pagina 2
passFiltros, todos opcionales:
q: texto. Busca en el contenido del artículo.estado:borrador,publicado,archivadoopapelera. Si lo omites, entran todos.categoria: id de categoría.desdeyhasta: fechaYYYY-MM-DD.hastaincluye ese día completo.external_id: el id de tu sistema, si lo enviaste al crear.pagina: desde 1. Sihay_masestrue, hay otra página.
Crear un artículo
POST /articulos crea una nota. Son obligatorios titulo, fecha_hora y cuerpo.
Si no envías estado, queda en borrador. Para publicarla en el mismo pedido, envía "estado": "publicado".
fecha_hora es ISO 8601. Sin zona horaria, se toma como la hora local del sitio. Con Z o un desplazamiento (-03:00), se convierte a esa hora local.
codigo, articulo = api("POST", "/articulos", json={
"titulo": "Lluvia en la ciudad",
"fecha_hora": "2026-10-16T09:30:00",
"detalle": "El temporal llegó durante la madrugada.",
"cuerpo": "<p>El temporal llegó durante la madrugada.</p>{{multimedia:%d}}" % media_id,
"estado": "publicado",
"categoria_id": 12,
"etiquetas": [4, 9],
"autor_id": 8,
"imagen": media_id,
"schema": "NewsArticle",
"importancia": 2,
"external_id": 4821,
})
print(codigo, articulo["id"], articulo["url"])Un alta nueva responde 201 y el mismo objeto que GET /articulos/{id}, con cuerpo incluido.
El mismo id de tu sistema
external_id es el identificador del artículo en tu base, un entero. Si vuelves a hacer POST con el mismo external_id, la API actualiza esa nota en lugar de crear otra. En ese caso responde 200.
Esa actualización por external_id no pide actualizado, pero titulo, fecha_hora y cuerpo siguen siendo obligatorios: es el mismo POST de alta.
Si hay más de un contenido con ese external_id, o si no es un artículo, la respuesta es 409.
Crear categoría o etiquetas por nombre
Si todavía no existen, envía el nombre y "crear_si_falta": true. Sin ese valor, la API no las crea.
codigo, articulo = api("POST", "/articulos", json={
"titulo": "Lluvia en la ciudad",
"fecha_hora": "2026-10-16T09:30:00",
"cuerpo": "<p>El temporal llegó durante la madrugada.</p>",
"estado": "borrador",
"crear_si_falta": True,
"categoria_nombre": "Ciudad",
"etiquetas_nombres": ["clima", "municipalidad"],
})Una nota acepta como máximo 10 etiquetas, sumando ids y nombres.
Leer un artículo
GET /articulos/{id} devuelve una nota, con el cuerpo. id es el de Medios CMS, el que viene en articulo["id"], no el external_id.
codigo, articulo = api("GET", "/articulos/3465")
print(articulo["titulo"], articulo["actualizado"])Guarda actualizado: lo vas a necesitar para modificar o eliminar.
La forma del objeto es esta. En el listado no viene cuerpo.
{
"id": 3465,
"external_id": 4821,
"estado": "publicado",
"programado": false,
"titulo": "Lluvia en la ciudad",
"slug": "lluvia-en-la-ciudad",
"url": "https://tusitio.com/contenido/3465/lluvia-en-la-ciudad",
"detalle": "El temporal llegó durante la madrugada.",
"cuerpo": "<p>...</p>",
"fecha_hora": "2026-10-16T09:30:00-03:00",
"actualizado": "2026-10-16T09:31:02-03:00",
"categoria": {"id": 12, "nombre": "Ciudad"},
"etiquetas": [{"id": 4, "nombre": "clima"}],
"autor_id": 8,
"autor": null,
"imagen": 91,
"ogimage": null,
"meta_title": "",
"meta_description": "",
"schema": "NewsArticle",
"index": true,
"importancia": 2,
"comentarios": true
}programado es true cuando el estado es publicado y fecha_hora todavía no llegó.
Si pides el id de una copia que el editor tiene abierta, la lectura la devuelve. Crear, actualizar y borrar operan sobre el artículo original: esa copia responde 404 en la escritura.
Actualizar un artículo
PATCH /articulos/{id} cambia solo los campos que envías. actualizado es obligatorio y tiene que ser el de la última lectura. La comparación es al segundo: no hace falta arrastrar microsegundos.
codigo, actual = api("GET", f"/articulos/{articulo_id}")
codigo, articulo = api("PATCH", f"/articulos/{articulo_id}", json={
"actualizado": actual["actualizado"],
"titulo": "Lluvia en la ciudad: el temporal sigue",
"estado": "publicado",
})Si alguien editó la nota después de tu GET, la respuesta es 409 y el mensaje pide volver a leerla. Lee de nuevo, toma el actualizado nuevo y reintenta.
Si hay un borrador abierto en el panel, también responde 409, con codigo hay_borrador. Para escribir igual, envía "forzar": true junto con el resto.
codigo, articulo = api("PATCH", f"/articulos/{articulo_id}", json={
"actualizado": actual["actualizado"],
"detalle": "Sigue lloviendo en el centro.",
"forzar": True,
})Si envías etiquetas o etiquetas_nombres, esa lista reemplaza a las etiquetas anteriores. Para dejar la nota sin etiquetas, envía "etiquetas": []. Para sumar una, envía la lista completa.
external_id se define en el POST. Un PATCH no lo cambia.
Eliminar un artículo
DELETE /articulos/{id} también exige actualizado. El resultado depende del estado:
- Publicado o archivado: pasa a la papelera.
eliminadovale"papelera". - Borrador o papelera: se borra definitivo.
eliminadovale"definitivo". No se puede deshacer.
No se puede mandar un artículo a la papelera poniendo "estado": "papelera" en un POST o PATCH. Ese paso es este DELETE.
codigo, actual = api("GET", f"/articulos/{articulo_id}")
codigo, resultado = api("DELETE", f"/articulos/{articulo_id}", json={
"actualizado": actual["actualizado"],
})
print(resultado["eliminado"])Si hay un borrador abierto en el panel, vale lo mismo que al actualizar: 409 hay_borrador, o "forzar": true para seguir.
Un segundo DELETE, cuando la nota ya está en la papelera, la borra definitivo. Vuelve a leer actualizado antes de ese segundo pedido.
Campos del artículo
En un POST, lo que no envías toma el valor por defecto de un alta. En un PATCH, lo que no envías no se toca.
| Campo | Detalle |
|---|---|
titulo | Texto. Obligatorio al crear. Hasta 512 caracteres. |
fecha_hora | ISO 8601. Obligatoria al crear. Sin zona, es la hora local del sitio. |
cuerpo | HTML. Obligatorio al crear. Ahí va {{multimedia:ID}}. |
estado | borrador, publicado o archivado. Al crear, si falta, queda en borrador. papelera solo se alcanza con DELETE. |
detalle | Copete, en texto. Al crear, si falta, queda vacío. |
slug | La parte final de la URL. Al crear, si falta, se arma desde el título. Cambiarlo cambia la URL pública. |
categoria_id | Id existente. null quita la categoría. |
categoria_nombre | Nombre. Solo crea la categoría si envías crear_si_falta: true. |
etiquetas | Lista de ids. Reemplaza el conjunto. Máximo 10 en total. |
etiquetas_nombres | Lista de nombres. Crea las que falten si envías crear_si_falta: true. |
autor_id | Id de GET /autores. Si lo envías, el texto de autor queda vacío. |
autor | Nombre en texto, cuando la firma no es un usuario del plantel. Hasta 50 caracteres. |
imagen | Id de multimedia para la imagen principal. null la quita. |
ogimage | Id de multimedia para la imagen de redes. null la quita. |
meta_title | Título para buscadores. Hasta 512 caracteres. |
meta_description | Descripción para buscadores. Hasta 512 caracteres. |
schema | Tipo de schema.org. Por defecto Article. También: NewsArticle, OpinionNewsArticle, AnalysisNewsArticle, BlogPosting, AdvertiserContentArticle, SatiricalArticle. |
importancia | Orden en portada: 1 máxima, 2 superior, 3 media, 4 inferior. |
index | true o false. Si los buscadores indexan la nota. Al crear, por defecto true. |
comentarios | true o false. Al crear, por defecto true. |
external_id | Entero de tu sistema. Solo en el POST. Sirve para crear o para actualizar esa misma nota. |
crear_si_falta | true para crear categoría o etiquetas por nombre. |
forzar | true para escribir aunque haya un borrador abierto en el panel. |
actualizado | Obligatorio en PATCH y DELETE. Es el valor que devolvió la última lectura. |
Al crear, si no envías autor ni autor_id, la nota queda asociada al usuario de la clave.
Publicar una nota que no estaba publicada puede responder 403 limite_plan si el sitio llegó al tope del plan. Volver a publicar una nota archivada no pasa por ese control.
La API no abre un borrador de editor ni envía mail: escribe directo sobre el artículo.
Redirección de URLs
En una migración, las URLs viejas dejan de existir y las nuevas las devuelve la API en el campo url. Si quieres que las viejas lleguen a las nuevas, tienes que construir un mapa de redirecciones.
Es un archivo de texto. Cada línea tiene dos rutas separadas por un espacio y termina con punto y coma. La primera es la URL vieja. La segunda es la ruta nueva, sin el dominio: si la API devolvió https://tusitio.com/contenido/3465/la-redaccion-de-el-faro, en el mapa va /contenido/3465/la-redaccion-de-el-faro.
/2022-01-13/la-redaccion-de-el-faro.html /contenido/3465/la-redaccion-de-el-faro;
/2022-01-12/inundaciones.html /contenido/3468/inundaciones;
/category/economia /categoria/86/economia;Ese formato es una coincidencia exacta: si la ruta de origen coincide, se redirige a la de destino.
A veces la URL vieja llega con argumentos, por ejemplo cuando el enlace se compartió en redes:
/2022-01-13/la-redaccion-de-el-faro.html?fb=39ql389Para esos casos la coincidencia exacta no alcanza y hace falta una expresión regular de Nginx. El mapeo de la URL de origen se hace sobre la variable $request_uri. Este artículo no enseña expresiones regulares: consulta la documentación de Nginx o pídeselo a quien arme el archivo.
Consideraciones
- Las rutas no llevan dominio, solo el camino.
- El mapa no debe repetir una URL de origen.
- Envía el archivo a contacto@medios.io para validarlo y activarlo.
- El límite es de 200.000 URLs.
- El mapa permanece activo durante 12 meses mientras se mantenga un plan que incluye la API.
- Una vez activado el mapa, no se aceptan modificaciones.
¿Le fue útil este artículo?
¡Qué bueno!
Gracias por sus comentarios
¡Sentimos mucho no haber sido de ayuda!
Gracias por sus comentarios
Comentarios enviados
Agradecemos su iniciativa, e intentaremos corregir el artículo