Manual de uso del API de Calendario
Este API permite gestionar categorías y eventos del calendario desde integraciones externas.
Las rutas viven bajo /api/calendar/* y devuelven JSON.
Autenticación
- Envía
Authorization: Bearer <token>en cada solicitud. - El token debe estar activo y tener permisos sobre el recurso que consultas.
- Mantén
Content-Type: application/jsoncuando el endpoint acepte cuerpo.
Permisos
read: requerido para todas las solicitudes (GET).update: permite usarincludeDrafts=trueen/eventspara ver borradores/eventos programados.
Si el token está vencido, inactivo o no cuenta con los permisos necesarios, la respuesta será 401 o 403.
Respuestas comunes
200 OK: solicitud exitosa.400 Bad Request: parámetros o cuerpo inválidos.401 / 403: token inválido, expirado o sin permisos.404 Not Found: recurso inexistente o no visible para el modo actual.405 Method Not Allowed: se usó un método no soportado.500 Internal Server Error: error inesperado en el servidor.
Categorías — /api/calendar/categories
GET listado
Lista las categorías disponibles para la página asociada al token.
Parámetros opcionales
limit(número, 1‑100): restringe la cantidad de resultados.id/slug(string): devuelve una única categoría que coincida con el identificador.
curl "https://cms.begraffic.com/api/calendar/categories?limit=50" \
-H "Authorization: Bearer TU_TOKEN"Respuesta
{
"count": 2,
"categories": [
{
"id": "lanzamientos",
"name": "Lanzamientos",
"description": "Actividades de lanzamiento",
"color": "#f97316",
"createdAt": "2024-07-12T15:00:00.000Z",
"updatedAt": "2024-07-12T15:00:00.000Z"
}
]
}Si envías id/slug, la respuesta tendrá la forma:
{ "category": { ... } }Eventos — /api/calendar/events
GET listado
Devuelve eventos futuros o ya publicados para la página del token.
Parámetros opcionales
start(ISO): obtiene eventos cuyo inicio sea igual o posterior a la fecha.end(ISO): obtiene eventos cuyo inicio sea igual o anterior a la fecha indicada.categoryId(string): filtra por categoría.limit(1‑200): máximo de registros.includeDrafts(booleano): requiere permisoupdatey devuelve además borradores/programados.includeImages(booleano): incluye la galería completa (images) de cada evento. Sin él, el listado trae solo la portada (coverImage) yimageCount, para no inflar la respuesta cuando pides muchos eventos.includeContent(booleano): incluye el contenido enriquecido (content.html) de cada evento. Mismo criterio: sin él el listado solo dicehasContent.
Con
includeImagesoincludeContentel listado se acota a 25 eventos aunque pidas más: son respuestas mucho más pesadas y el ancho de banda se factura a tu workspace. Para recorrer muchos eventos, lístalos sin esos parámetros y pide el detalle solo de los que vayas a mostrar.
curl "https://cms.begraffic.com/api/calendar/events?start=2024-07-01&end=2024-07-31" \
-H "Authorization: Bearer TU_TOKEN"Respuesta
{
"count": 1,
"events": [
{
"id": "launch-demo",
"title": "Demo de lanzamiento",
"description": "Presentación del nuevo producto.",
"start": "2024-07-08T15:00:00.000Z",
"end": "2024-07-08T16:30:00.000Z",
"location": "Google Meet",
"visibility": "public",
"categoryId": "lanzamientos",
"categoryName": "Lanzamientos",
"categoryColor": "#f97316",
"publish": true,
"publishAt": null,
"isPublished": true,
"isScheduled": false,
"isAllDay": false,
"coverImageUrl": "https://cms.begraffic.com/api/storage/proxy?path=...",
"coverImage": { "id": "...", "url": "...", "alt": "Escenario principal", "variants": { "...": {} } },
"imageCount": 3,
"images": null,
"slug": "demo-de-lanzamiento",
"details": {
"subtitle": "Una tarde para conocer el producto",
"organizer": "Equipo de producto",
"contactEmail": "eventos@tudominio.com",
"tags": ["producto", "gratuito"],
"modality": "hybrid",
"onlineUrl": "https://meet.google.com/abc-defg-hij",
"place": { "address": "Calle 1 #2-3, Bogotá", "lat": 4.65, "lng": -74.05 },
"capacity": 120,
"registrationUrl": "https://tudominio.com/inscripcion",
"registrationLabel": "Reservar mi cupo",
"price": { "isFree": true, "amount": null, "currency": "cop" }
},
"hasContent": true,
"content": null,
"createdAt": "2024-07-01T10:00:00.000Z",
"updatedAt": "2024-07-01T10:00:00.000Z"
}
]
}Los eventos programados (publish = false y publishAt futuro) solo se devuelven cuando el token incluye includeDrafts=true y cuenta con permiso update.
GET detalle
GET /api/calendar/events?id=<id> o GET /api/calendar/events?slug=<slug>
Devuelve un único evento publicado (o borrador, si includeDrafts=true y el token tiene update).
El detalle siempre incluye la galería completa (images) y el contenido (content): no hace falta includeImages ni includeContent.
Si envías id y slug a la vez, manda id. Al guardar, el dashboard comprueba que el slug esté
libre y le añade un sufijo (-2, -3…) si chocara con otro evento. No es una restricción de la
base de datos: si dos personas guardan el mismo slug exactamente a la vez, ?slug= devolverá uno
de los dos — usa id cuando necesites una referencia estable para siempre.
Detalles del evento
Todo lo que se edita en la página interna del evento viaja en details. Los campos vacíos
llegan como "", [] o null — nunca desaparecen, así que puedes leerlos sin comprobar.
| Campo | Tipo | Notas |
|---|---|---|
slug | string | null | Va en la raíz del evento, no dentro de details. Sirve como clave de búsqueda. |
details.subtitle | string | Bajada corta para la cabecera. |
details.organizer | string | Quién organiza. |
details.contactEmail | string | Correo de contacto ya validado (o ""). |
details.tags | string[] | Hasta 12, sin repetidos. |
details.modality | "in_person" | "online" | "hybrid" | Cómo se asiste. |
details.onlineUrl | string | Sala virtual. Siempre "" si la modalidad es presencial. |
details.place | { address, lat, lng } | null | Dirección. Siempre null si la modalidad es online. lat/lng son null cuando la dirección se escribió a mano y no se geocodificó — no pintes un marcador en ese caso. |
details.capacity | number | null | Aforo. null = sin límite declarado. |
details.registrationUrl | string | CTA de inscripción, ya normalizada a http(s). |
details.registrationLabel | string | Texto del botón (p. ej. «Reservar mi cupo»). |
details.price | { isFree, amount, currency } | amount en unidades mayores (25.5 = 25,50) y currency en minúsculas. |
Sobre price: isFree: true significa gratis de forma explícita. isFree: false con
amount: null significa precio sin definir — no lo pintes como «0».
Las URLs (onlineUrl, registrationUrl) se guardan ya normalizadas y percent-codificadas: si no
son http(s) válidas llegan como "". Nunca recibirás un javascript:, un enlace con credenciales
(https://sitio.com@otro-host) ni caracteres sin escapar — son seguras de interpolar en un href.
Contenido del evento
Cada evento puede tener una página completa montada con el editor visual de Be CMS, igual que un artículo del blog.
hasContent(booleano): viene siempre, también en los listados.content:{ "html": "...", "htmlPreview": "..." }onull.
curl "https://cms.begraffic.com/api/calendar/events?slug=demo-de-lanzamiento" \
-H "Authorization: Bearer TU_TOKEN"- El HTML se sanitiza al servir (se eliminan scripts, manejadores
on*y esquemas peligrosos), así que puedes inyectarlo con seguridad en tu página. - Nunca se expone el formato interno del editor (
designJson/projectDataJson). htmlPreviewes un recorte de ~1000 caracteres para listados y tarjetas.
Imágenes de los eventos
Cada evento puede llevar hasta 12 imágenes ordenadas. La primera es la portada.
coverImage: la primera imagen, onullsi el evento no tiene ninguna. Viene siempre, también en los listados.coverImageUrl: atajo con la URL lista para usar en un<img src>.imageCount: cuántas imágenes tiene el evento (aunqueimagesvenga ennull).images: array ordenado con TODAS las imágenes. Viene en el detalle y en el listado conincludeImages=true; en caso contrario esnull(no[], para que puedas distinguir «no las pediste» de «no tiene»).
Cada imagen tiene esta forma:
{
"id": "0b0a…",
"url": "https://cms.begraffic.com/api/storage/proxy?path=...",
"previewUrl": "https://cms.begraffic.com/api/storage/proxy?path=...",
"alt": "Escenario principal",
"width": 3000,
"height": 2000,
"variants": {
"original": { "url": "...", "width": 3000, "height": 2000, "size": 850123, "contentType": "image/jpeg", "storagePath": "tables/…" },
"medium": { "url": "...", "width": 1920, "height": 1280, "size": 210344, "contentType": "image/webp", "storagePath": "tables/…" },
"small": { "url": "...", "width": 1280, "height": 853, "size": 98211, "contentType": "image/webp", "storagePath": "tables/…" },
"thumbnail": { "url": "...", "width": 480, "height": 320, "size": 21044, "contentType": "image/webp", "storagePath": "tables/…" }
}
}Recomendaciones de uso:
- Usa
variants.thumbnailen listados yvariants.mediumen la vista de detalle:originalpuede pesar varios MB y el ancho de banda se factura a tu workspace. altes el texto alternativo que escribió quien creó el evento; úsalo tal cual en el atributoalt(accesibilidad y SEO). Puede venir vacío.- Las URLs se sirven por el proxy de assets de Be CMS, no por Firebase Storage: no las guardes en caché de forma indefinida ni las reescribas.
Visibilidad
Cada evento tiene visibility, que sí filtra lo que sirve la API:
| Valor | Listado | ?id= / ?slug= |
|---|---|---|
public | Sí | Sí |
unlisted | No | Sí — para enlaces que compartes tú (no aparece en índices) |
private | No | No (404) |
Es independiente de la publicación: un evento private no se sirve aunque esté publicado.
Notas generales
- Todas las fechas se esperan y devuelven en formato ISO 8601.
- Los errores se devuelven en la forma
{ "error": "mensaje descriptivo" }. - El API está pensado para integraciones de confianza; aplica límites razonables (
limit, rangos de fechas) para evitar lecturas muy grandes.