StoryFeed Media
API v1 · lectura pública

API de StoryFeed Media

La API pública que alimenta al componente <story-feed>: feed vertical paginado, detalle por slug, feed agregado por tema y health. Sin autenticación, CORS abierto y respuestas JSON estables.

  • Base: https://api.storyfeedmedia.com/api/v1
  • Auth: ninguna
  • Formato: JSON (UTF-8)
  • Versión: v1

Base URL y acceso

Todas las rutas documentadas acá cuelgan de https://api.storyfeedmedia.com/api/v1. El host storyfeedmedia.com sirve la API con el mismo path, así que ambos prefijos son intercambiables.

  • No hay claves de API ni OAuth: la superficie es read-only y pública.
  • Todas las respuestas llevan Access-Control-Allow-Origin: *.
  • Cada respuesta incluye X-Request-Id (correlation ID) para soporte.
  • El error siempre usa el mismo envelope (ver Errores).
# Primer request: última página del feed de un cliente
curl -s "https://api.storyfeedmedia.com/api/v1/feed?client=demo"

Endpoints

MétodoRutaQué devuelve
GET/feedPágina del feed (proyección light, sin content)
GET/story/:slugStory completa, con content si existe
GET/theme/:theme/feedFeed agregado de varios clients bajo un tema
GET/theme/:theme/story/:slugDetalle dentro de un tema
GET/healthEstado del servicio

Feed

GET /api/v1/feed

Historias ordenadas por publishedAt descendente. Las que no tienen fecha van al final.

Query params

ParamTipoDefaultNotas
clientstring—Dataset del medio. Sin client se usa el dataset por defecto.
cursorstring—Cursor opaco de la página anterior. Ver Paginación.
tagstring—Filtro exacto sobre tags. Superficie enterprise.
themestring—Filtro exacto sobre theme. Superficie enterprise.
limitintegerignoradoDeprecado: se acepta y se ignora. El tamaño de página lo define pageSize de la fuente (1–50).

Respuesta 200

{
  "items": [
    {
      "id": "a1b2c3",
      "slug": "el-futuro-del-periodismo-movil",
      "type": "article",
      "title": "El futuro del periodismo móvil",
      "excerpt": "Una mirada a los formatos que retienen...",
      "image": { "src": "https://cdn.ejemplo.com/foto.jpg", "alt": "Redacción" },
      "tags": ["tecnologia"],
      "theme": "portada",
      "section": "tecnologia",
      "publishedAt": "2026-11-08T10:00:00.000Z",
      "contentUrl": "https://medio.ejemplo.com/nota/..."
    }
  ],
  "nextCursor": "eyJvZmZzZXQiOjEwfQ",
  "hasMore": true,
  "publisher": { "name": "Diario Ejemplo", "logo": "https://medio.ejemplo.com/logo.png" }
}

Proyección light. Los items del feed no incluyen content: el cuerpo HTML se sirve solo en el detalle para no inflar cada página. items.length puede superar el pageSize si hay ad cards (type: "ad") intercaladas; los ads no cuentan contra el tamaño de página ni contra hasMore.

Errores propios

CódigoStatusCuándo
INVALID_CURSOR400El cursor no se puede decodificar.
CLIENT_NOT_FOUND404El client no tiene ninguna fuente activa. Un client conocido sin artículos responde 200 con items: [].
QUOTA_EXCEEDED403El agregado mensual de vistas del dueño alcanzó el 130% de su plan.

Detalle por slug

GET /api/v1/story/:slug

Devuelve el objeto Story completo, incluido content cuando el artículo tiene body.

ParamDóndeNotas
slugpathIdentificador de la story (^[a-z0-9]+(-[a-z0-9]+)*$).
clientqueryMismo client que se usó en el feed, para resolver el dataset correcto.
curl -s "https://api.storyfeedmedia.com/api/v1/story/el-futuro-del-periodismo-movil?client=demo"

content es HTML del artículo con URLs relativas ya resueltas a absolutas. excerpt y subtitle son resúmenes de hasta 280 caracteres; si coinciden con el cuerpo completo, se omiten.

CódigoStatusCuándo
STORY_NOT_FOUND404El slug no existe en el dataset.
ALLY_NO_DETAIL403El Origin pertenece a un aliado sin derecho a detalle.
QUOTA_EXCEEDED403Cuota mensual del dueño agotada al 130%.

Orden de evaluación: cuota → gate de aliado → lookup del slug. Con un Origin aliado, un slug inexistente responde 403, no 404.

Feeds por tema

GET /api/v1/theme/:theme/feed

Agrupa los datasets de varios clients bajo una definición de tema y los sirve como un feed único.

ParamDóndeDefaultNotas
themepath—^[a-z0-9-]+$ (minúsculas, números y guiones).
cursorquery—Cursor opaco, igual que en /feed.
limitquery10Se clampa entre 1 y 50; nunca devuelve error por rango.
tagquery—Filtro exacto sobre tags aplicado al merge.

La respuesta usa el mismo contrato que /feed. Los clients sin contenido se saltean; si ninguno tiene stories, responde 200 con items: []. El orden se define por el bloque sort del tema (recency, round-robin o alternating).

Detalle dentro del tema

GET /api/v1/theme/:theme/story/:slug

Busca el slug entre los clients del tema y devuelve la primera coincidencia, con content si existe.

CódigoStatusCuándo
INVALID_THEME400El tema tiene caracteres inválidos o falta.
THEME_NOT_FOUND404No existe la definición del tema.
STORY_NOT_FOUND404El slug no existe en ningún client del tema.
INVALID_CURSOR400Cursor no decodificable (solo en el feed del tema).

Health

GET /api/v1/health

Sin dependencias externas: no consulta la base de datos.

{ "status": "ok", "uptimeSeconds": 123 }

Modelo Story

Contrato en shared/schema/story.schema.json. Campos requeridos: id, slug, type.

CampoTipoNotas
idstringIdentificador interno.
slugstringClave pública para el detalle.
typestringarticle, reel o ad.
title / subtitle / excerptstringsubtitle y excerpt limitados a 280 caracteres.
captionstringTexto principal de un reel.
imageobject{ src, alt }.
videoobjectsrc, durationSeconds, videoProvider, videoId, isShort.
theme / section / tagsstring / arrayClasificación editorial.
authorstringFirma del artículo.
publishedAt / updatedAtstringISO-8601 UTC.
readingTimeintegerMinutos estimados de lectura.
ctaLabelstringTexto del call-to-action del detalle.
contentUrlstringURL original del artículo en el publisher.
contentstringHTML del cuerpo. Solo en el detalle.
active0 | 1Moderación. Las stories inactivas nunca se sirven.

Los campos _adOverlayId y _adDetailId son internos del componente y no forman parte del contrato público.

Paginación y cursor

La paginación es por cursor, no por offset numérico. Pedí la primera página sin cursor; para la siguiente pasá el nextCursor de la respuesta.

# Página 1
curl -s "https://api.storyfeedmedia.com/api/v1/feed?client=demo"

# Página 2 — usar el nextCursor devuelto
curl -s "https://api.storyfeedmedia.com/api/v1/feed?client=demo&cursor=eyJvZmZzZXQiOjEwfQ"
  • hasMore: false indica que no hay más páginas.
  • El cursor es opaco: no lo parsees ni lo construyas a mano.
  • El cursor codifica un offset sobre contenido real (Base64URL). Es una deuda técnica intencional: con escrituras concurrentes puede saltear o repetir items.
  • Un cursor inválido responde 400 INVALID_CURSOR; pedí la primera página de nuevo.

Errores

Todas las respuestas de error comparten el mismo envelope:

{
  "error": {
    "code": "STORY_NOT_FOUND",
    "message": "Story not found",
    "status": 404,
    "requestId": "req-8f3a..."
  }
}
  • requestId coincide con el header X-Request-Id.
  • Los errores no se cachean con el ETag del recurso.
  • Ante 429, respetá Retry-After y aplicá backoff.

Rate limits

Límites por IP + path, ventana de 15 minutos (salvo demos). Toda respuesta incluye RateLimit-Limit, RateLimit-Remaining y RateLimit-Reset.

RutasPresupuesto
/api/v1/feed, /story/:slug, /theme/*, /health300 req / 15 min
/.well-known/*, /openapi.json, /skills/*60 req / 15 min

El bloqueo responde 429 con Retry-After. En producción, Cloudflare es la capa que aplica el límite efectivo.

Cache y ETag

Las respuestas de feed y detalle incluyen ETag débil y Cache-Control con max-age corto y s-maxage largo. Para revalidar, mandá If-None-Match: si nada cambió, la API responde 304 sin body.

curl -s -H 'If-None-Match: W/"abc123"' \
  "https://api.storyfeedmedia.com/api/v1/feed?client=demo"

La invalidación en producción es activa por Cache-Tag en cada escritura, no solo por TTL.

Recursos para agentes

El contrato humano vive en docs/api.md del repo. La especificación openapi.json es la fuente máquina.

Embed

El componente consume esta API. Un snippet mínimo:

<script src="https://storyfeedmedia.com/js/story-feed.min.js" defer></script>
<story-feed endpoint="https://api.storyfeedmedia.com/api/v1/feed"
            client="tu-medio"></story-feed>

Guía de integración y personalización: Configurador · Feeds RSS · Métricas GA4.