Manual de uso del API de Blog
El API de blog expone endpoints para consultar artículos, autores y categorías, y para que tus visitantes comenten, valoren con estrellas y voten los artículos. Todas las rutas viven bajo /api/blog/* y devuelven JSON.
Autenticación
- Incluye
Authorization: Bearer <token>en cada solicitud. - El token debe estar activo y tener permisos sobre el recurso que consultas.
- Aunque los endpoints actuales no requieren cuerpo, mantén
Content-Type: application/jsonen caso de enviar parámetros serializados.
Permisos
read: permite ejecutar peticionesGET(incluida la lectura de comentarios).blog_comments(«Comentarios del blog» en API & Tokens): permite que tus visitantes publiquen comentarios y respuestas y voten. Va aparte dereada propósito: un token de solo lectura suele acabar en el JavaScript público de la web, y sin este permiso no sirve para escribir nada.
Si el token está vencido, inactivo o carece del permiso necesario, la respuesta será 401 o 403.
Códigos de respuesta comunes
200 OK: respuesta exitosa.400 Bad Request: parámetros inválidos.401 / 403: token ausente, inválido o sin permisos.404 Not Found: recurso inexistente o no publicado.405 Method Not Allowed: método no admitido por esa ruta.429 Too Many Requests: demasiados comentarios o votos seguidos desde la misma IP.500 Internal Server Error: error no controlado en el servidor.
Artículos — /api/blog/articles
GET listado
Devuelve todos los artículos publicados accesibles para el token.
Query params
limit(número, opcional): máximo de resultados (1–100).categoryId(string, opcional): devuelve solo artículos con esa categoría.authorId(string, opcional): devuelve solo artículos del autor indicado.excludeCategoryId(string, opcional): lista separada por comas de categorías a omitir (excludeCategoryId=marketing,eventos).excludeAuthorId(string, opcional): lista separada por comas de autores a omitir (excludeAuthorId=ana-garcia,pedro-lopez).sortBy(string, opcional): campo para ordenar resultados. Valores permitidos:publishedAt,title. Por defecto se usa orden descendente porpublishedAtsi se envía este parámetro sinsortDirection.sortDirection(string, opcional): dirección de orden. Valores permitidos:asc,desc. Si no se envía, se asumedesc.
Puedes combinar categoryId y authorId para filtrar por ambos criterios y, adicionalmente, aplicar exclusiones. Las exclusiones también se respetan cuando consultas un artículo puntual con slug.
Ejemplo listado básico
curl "https://cms.begraffic.com/api/blog/articles?limit=10&categoryId=marketing&authorId=ana-garcia" \
-H "Authorization: Bearer TU_TOKEN"Ejemplo con orden
curl "https://cms.begraffic.com/api/blog/articles?sortBy=title&sortDirection=asc&limit=5" \
-H "Authorization: Bearer TU_TOKEN"Ejemplos extra
- Orden por fecha descendente (default) con exclusión de categorías:
curl "https://cms.begraffic.com/api/blog/articles?sortBy=publishedAt&excludeCategoryId=marketing,eventos" \ -H "Authorization: Bearer TU_TOKEN" - Orden por fecha ascendente filtrando por autor:
curl "https://cms.begraffic.com/api/blog/articles?authorId=ana-garcia&sortBy=publishedAt&sortDirection=asc" \ -H "Authorization: Bearer TU_TOKEN" - Orden por título descendente con límite:
curl "https://cms.begraffic.com/api/blog/articles?sortBy=title&sortDirection=desc&limit=20" \ -H "Authorization: Bearer TU_TOKEN"
Notas de orden:
- Si envías
sortBysinsortDirection, se usadescpor defecto. - El orden aplica sobre todos los artículos publicados que cumplan tus filtros (
categoryId,authorId, exclusiones); no afecta al endpoint de detalle. - El orden por
publishedAtestá soportado de forma nativa.
Publicación programada
- Los artículos creados con
isScheduled = truey unscheduledForen el futuro permanecen ocultos hasta la fecha. - Los artículos programados se publican automáticamente al llegar su fecha; a partir de ese momento aparecen con
status: published.
Respuesta
{
"count": 2,
"articles": [
{
"id": "guia-crm",
"title": "Guía CRM",
"slug": "guia-crm",
"excerpt": "Resumen...",
"status": "published",
"tags": ["crm", "ventas"],
"categoryId": "marketing",
"categoryName": "Marketing",
"authorId": "ana-garcia",
"authorName": "Ana García",
"isScheduled": false,
"scheduledFor": null,
"metadata": null,
"publishedAt": "2024-05-01T10:00:00.000Z",
"createdAt": "2024-05-01T10:00:00.000Z",
"updatedAt": "2024-05-02T12:30:00.000Z",
"externalId": null,
"comments": { "count": 12, "ratingCount": 5, "ratingAverage": 4.4 }
}
]
}Cada artículo trae comments con sus cifras: count (comentarios visibles, respuestas incluidas), ratingCount (cuántos pusieron estrellas) y ratingAverage (media con un decimal, o null si nadie valoró). Si en ese momento no se pudieron leer, comments llega como null: no significa que no tenga comentarios.
Los artículos programados (isScheduled = true) solo se incluyen cuando scheduledFor es menor o igual a la hora actual.
El campo content se omite en el listado para reducir el tamaño de la respuesta; solo está disponible en el endpoint de detalle.
Ejemplo omitidos
curl "https://cms.begraffic.com/api/blog/articles?excludeCategoryId=marketing&excludeAuthorId=ana-garcia" \
-H "Authorization: Bearer TU_TOKEN"Respuesta
{
"count": 1,
"articles": [
{
"id": "caso-exito",
"title": "Caso de éxito",
"slug": "caso-exito",
"categoryId": "finanzas",
"authorId": "carlos-mendez",
"status": "published",
"isScheduled": false,
"scheduledFor": null,
"publishedAt": "2024-06-01T12:00:00.000Z",
"createdAt": "2024-05-15T08:00:00.000Z",
"updatedAt": "2024-05-20T09:30:00.000Z"
}
]
}GET detalle
GET /api/blog/articles?slug=<slug>
Devuelve un artículo publicado que coincida con el slug. Si existe pero no está en estado published, la respuesta será 404.
Este endpoint incluye el campo content con la información completa del artículo.
Respuesta
{
"article": {
"id": "guia-crm",
"title": "Guía CRM",
"slug": "guia-crm",
"excerpt": "Resumen...",
"categoryId": "marketing",
"categoryName": "Marketing",
"authorId": "ana-garcia",
"authorName": "Ana García",
"tags": ["crm", "ventas"],
"status": "published",
"isScheduled": false,
"scheduledFor": null,
"content": {
"html": "<h1>Hola</h1>",
"htmlPreview": "<h1>Hola</h1>"
},
"metadata": null,
"publishedAt": "2024-05-01T10:00:00.000Z",
"createdAt": "2024-05-01T10:00:00.000Z",
"updatedAt": "2024-05-02T12:30:00.000Z"
}
}El endpoint de detalle devuelve el contenido en content, con los campos html y htmlPreview.
También incluye comments con las mismas cifras que el listado.
Autores — /api/blog/authors
GET listado
Lista todos los autores con estado published accesibles para el token. Acepta limit como parámetro opcional.
curl "https://cms.begraffic.com/api/blog/authors" \
-H "Authorization: Bearer TU_TOKEN"Respuesta
{
"count": 2,
"authors": [
{
"id": "ana-garcia",
"name": "Ana García",
"slug": "ana-garcia",
"title": "Editora",
"bio": "Periodista especializada en marketing.",
"avatar": "https://cdn.example.com/avatars/ana.jpg",
"featured": true,
"status": "published",
"publishedAt": "2024-01-10T15:00:00.000Z",
"createdAt": "2024-01-10T15:00:00.000Z",
"updatedAt": "2024-03-05T09:30:00.000Z"
},
{
"id": "carlos-mendez",
"name": "Carlos Méndez",
"slug": "carlos-mendez",
"title": "Analista",
"featured": false,
"status": "published",
"publishedAt": "2024-02-01T09:00:00.000Z",
"createdAt": "2024-02-01T09:00:00.000Z",
"updatedAt": "2024-02-15T11:45:00.000Z"
}
]
}GET detalle
GET /api/blog/authors?slug=<slug> devuelve la información de un autor publicado. Responde 404 si el slug no existe o el autor no está publicado.
Respuesta
{
"author": {
"id": "ana-garcia",
"name": "Ana García",
"slug": "ana-garcia",
"title": "Editora",
"email": "ana@example.com",
"bio": "Periodista especializada en marketing.",
"avatar": "https://cdn.example.com/avatars/ana.jpg",
"website": "https://ana.blog",
"linkedin": "https://linkedin.com/in/anagarcia",
"twitter": "@anagarcia",
"featured": true,
"status": "published",
"publishedAt": "2024-01-10T15:00:00.000Z",
"createdAt": "2024-01-10T15:00:00.000Z",
"updatedAt": "2024-03-05T09:30:00.000Z"
}
}Categorías — /api/blog/categories
GET listado
Recupera las categorías publicadas. También admite el parámetro opcional limit.
Cada documento de categoría incluye articleCount, que refleja cuántos artículos están asignados actualmente a esa categoría.
Respuesta
{
"count": 2,
"categories": [
{
"id": "marketing",
"name": "Marketing",
"slug": "marketing",
"description": "Estrategias y tácticas de marketing.",
"accent": "#00AEEF",
"featured": true,
"articleCount": 12,
"status": "published",
"publishedAt": "2024-01-05T08:00:00.000Z",
"createdAt": "2024-01-05T08:00:00.000Z",
"updatedAt": "2024-03-20T10:15:00.000Z"
},
{
"id": "finanzas",
"name": "Finanzas",
"slug": "finanzas",
"description": "Noticias y consejos financieros.",
"featured": false,
"articleCount": 4,
"status": "published",
"publishedAt": "2024-02-12T14:00:00.000Z",
"createdAt": "2024-02-12T14:00:00.000Z",
"updatedAt": "2024-02-18T16:45:00.000Z"
}
]
}GET detalle
GET /api/blog/categories?slug=<slug> devuelve una categoría publicada. Si la categoría existe pero no está publicada, responde 404.
Respuesta
{
"category": {
"id": "marketing",
"name": "Marketing",
"slug": "marketing",
"description": "Estrategias y tácticas de marketing.",
"accent": "#00AEEF",
"featured": true,
"articleCount": 12,
"status": "published",
"publishedAt": "2024-01-05T08:00:00.000Z",
"createdAt": "2024-01-05T08:00:00.000Z",
"updatedAt": "2024-03-20T10:15:00.000Z"
}
}Comentarios — /api/blog/comments
Tus visitantes pueden comentar un artículo publicado, valorarlo con estrellas, responder a un comentario y votar comentarios con «me gusta» / «no me gusta».
Cómo funciona
- Se publica al instante. Desde el panel (
Blog → Comentarios) el negocio puede ocultar, volver a mostrar o borrar cualquier comentario, y responder como el negocio. Un comentario oculto desaparece de la API. - Un solo nivel de respuestas. Se responde a un comentario principal; una respuesta no admite respuestas (
400). - Estrellas opcionales, de 1 a 5, solo en el comentario principal. Una respuesta con estrellas se rechaza (
400). - Respuestas oficiales. Las que escribe el negocio desde el panel llegan con
official: true: píntalas distinto. - El correo de quien comenta nunca sale por la API. Solo lo ve el negocio en su panel.
- Texto plano.
bodyyauthorNameson texto, no HTML. Al guardar se quitan las etiquetas, pero escápalos al pintarlos (usatextContent, o el escapado por defecto de tu framework), nuncainnerHTML. Los saltos de línea llegan como\n. - Solo artículos publicados. Un artículo en borrador, oculto o programado a futuro responde
404, tanto al leer como al comentar.
GET listado
GET /api/blog/comments?article=<slug> · permiso read
Query params
article(obligatorio): slug del artículo (el mismo de/api/blog/articles).limit(opcional): comentarios principales por página, de 1 a 30. Por defecto 10.order(opcional):newest(por defecto) uoldest.cursor(opcional): elnextCursorde la página anterior.visitorId(opcional): si lo envías, cada comentario traemyVote("like","dislike"onull) para pintar el voto de ese visitante.
Cada comentario principal trae sus respuestas en replies, de la más antigua a la más reciente, hasta 50. replyCount siempre tiene el total real.
curl "https://cms.begraffic.com/api/blog/comments?article=guia-crm&limit=10&visitorId=v_8f2k1x9q" \
-H "Authorization: Bearer TU_TOKEN"Respuesta
{
"article": "guia-crm",
"stats": { "count": 3, "ratingCount": 1, "ratingAverage": 5 },
"count": 1,
"comments": [
{
"id": "Xb12…",
"parentId": null,
"authorName": "Ana García",
"body": "Muy útil, gracias.\nLo aplico mañana.",
"rating": 5,
"official": false,
"likes": 4,
"dislikes": 0,
"replyCount": 2,
"createdAt": "2026-09-27T10:00:00.000Z",
"myVote": "like",
"replies": [
{
"id": "Qa77…",
"parentId": "Xb12…",
"authorName": "Equipo Be",
"body": "¡Gracias, Ana!",
"rating": null,
"official": true,
"likes": 1,
"dislikes": 0,
"replyCount": 0,
"createdAt": "2026-09-27T11:30:00.000Z",
"myVote": null
}
]
}
],
"nextCursor": null
}nextCursor es null cuando no hay más páginas. stats son las cifras de todo el artículo, no solo de esta página.
POST publicar un comentario o una respuesta
POST /api/blog/comments · permiso blog_comments
Cuerpo
article(obligatorio): slug del artículo.authorName(obligatorio): hasta 80 caracteres.body(obligatorio): hasta 2000 caracteres.authorEmail(opcional): el negocio lo ve en su panel; nunca se publica.rating(opcional): entero de 1 a 5. Solo en comentarios principales.parentId(opcional): id del comentario principal al que se responde.
curl -X POST "https://cms.begraffic.com/api/blog/comments" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"article":"guia-crm","authorName":"Ana García","authorEmail":"ana@correo.com","body":"Muy útil, gracias.","rating":5}'Responde 201 con { "comment": { … } }, en la misma forma que el listado. El negocio recibe un aviso en su panel.
POST votar un comentario
POST /api/blog/comments/vote · permiso blog_comments
Cuerpo
commentId(obligatorio).visitorId(obligatorio, de 8 a 200 caracteres): quién vota. Usa el id de tu usuario si tu web tiene login; si no, genera uno aleatorio la primera vez (por ejemplo,crypto.randomUUID()) y guárdalo en el navegador. Guardamos solo su hash.vote(obligatorio):"like","dislike"o"none"para quitar el voto.
Hay un voto por visitante y comentario: repetir el mismo no suma, cambiarlo lo mueve de un contador al otro, y "none" lo quita. Se pueden votar comentarios y respuestas.
curl -X POST "https://cms.begraffic.com/api/blog/comments/vote" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"commentId":"Xb12…","visitorId":"v_8f2k1x9q","vote":"like"}'Respuesta
{ "commentId": "Xb12…", "likes": 5, "dislikes": 0, "myVote": "like" }El
visitorIdlo decide tu web, así que alguien que borre los datos de su navegador puede volver a votar. Sirve para evitar el voto repetido por accidente, no como identificación.
Errores y límites
400: faltan datos o no son válidos. El campoerrortrae un mensaje listo para enseñar («Escribe tu comentario.», «Las estrellas van de 1 a 5.»…).403: el token no tieneread(al leer) oblog_comments(al escribir o votar).404: el artículo no está publicado, o el comentario no existe o está oculto.429: más de 10 comentarios o 60 votos por minuto desde la misma IP en tu workspace.
¿Desde el servidor o desde el navegador?
- Desde el servidor de tu web (recomendado). El token no sale nunca al navegador: tu formulario llama a una ruta de tu propio servidor, y esa ruta llama a Be CMS. Ten en cuenta que entonces todas tus visitas comparten la IP de tu servidor y, con ella, el límite de 10 comentarios y 60 votos por minuto (de sobra para la mayoría de los blogs).
- Desde el navegador. Solo con un token dedicado que tenga únicamente
readyblog_comments: cualquiera puede copiar un token que viaja en el JavaScript de tu web, y así lo único que podría hacer con él es leer lo publicado y comentar como un visitante más. Nunca pongas en el navegador un token con otros permisos.
Ejemplo de integración (JavaScript)
const API = 'https://cms.begraffic.com/api/blog/comments'
const headers = { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }
// Un id estable por visitante: el de tu usuario si hay login; si no, uno aleatorio guardado.
function visitorId() {
let id = localStorage.getItem('becms_visitor')
if (!id) { id = crypto.randomUUID(); localStorage.setItem('becms_visitor', id) }
return id
}
// 1) Leer los comentarios de un artículo (con el voto de este visitante).
async function cargarComentarios(slug, cursor) {
const qs = new URLSearchParams({ article: slug, limit: '10', visitorId: visitorId() })
if (cursor) qs.set('cursor', cursor)
const res = await fetch(`${API}?${qs}`, { headers })
return res.json() // { stats, comments, nextCursor }
}
// 2) Publicar un comentario (con estrellas) o una respuesta (con parentId, sin estrellas).
async function comentar({ slug, nombre, correo, texto, estrellas, parentId }) {
const res = await fetch(API, {
method: 'POST',
headers,
body: JSON.stringify({ article: slug, authorName: nombre, authorEmail: correo, body: texto, rating: estrellas ?? null, parentId: parentId ?? null }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error) // mensaje listo para enseñar
return data.comment
}
// 3) Votar: 'like', 'dislike' o 'none' para quitar el voto.
async function votar(commentId, vote) {
const res = await fetch(`${API}/vote`, {
method: 'POST',
headers,
body: JSON.stringify({ commentId, visitorId: visitorId(), vote }),
})
return res.json() // { likes, dislikes, myVote }
}
// Al pintar: SIEMPRE como texto.
parrafo.textContent = comentario.bodyBuenas prácticas
- Normaliza slugs a minúsculas antes de consultarlos.
- Envía fechas en formato ISO 8601 al compararlas con
scheduledForopublishedAt. - Maneja específicamente los códigos
404(recurso no publicado) y405(método no permitido) en tus integraciones. - Pinta
bodyyauthorNamede los comentarios como texto, nunca como HTML. - Si llegas a necesitar otras operaciones de escritura, deberás coordinar con el equipo para habilitar endpoints adicionales.