# Analítica

> Visitas de tus blogs y artículos por la API de Blogenia: series por día, semana o mes, ranking de artículos y total de la cuenta con el permiso stats:read.

Versión web: https://blogelia.com/docs/analitica

Cuatro rutas devuelven las visitas de tu cuenta, de un blog o de un artículo, como las APIs de analítica de cualquier herramienta web.

> **Nota:** Estas rutas exigen una clave con el permiso `stats:read`, que **no** da acceso a artículos ni a datos de la cuenta. Es el permiso pensado para un dashboard. Ver [Permisos](https://blogelia.com/docs/autenticacion#scopes) y [Construir tu dashboard](https://blogelia.com/docs/construir-dashboard).

## Reglas comunes

Las rutas de analítica comparten estas reglas:

- **Fechas.** `from` y `to` son días `YYYY-MM-DD` en la zona Europe/Madrid, ambos incluidos. Por defecto, los últimos 30 días; el máximo son 400 días por petición.
- **Frescura.** Los datos los vuelca un proceso que se ejecuta cada 10 minutos. `updatedAt` es la hora de la última pasada (ISO 8601 en UTC) y puede ser `null` si todavía no se conoce.
- **Día en curso.** El grupo de la serie que contiene hoy aún no ha terminado y lleva `partial: true`.
- **Únicos aproximados.** `uniquesApprox` se calcula con un identificador anónimo que cambia cada día y sin cookies, así que es una aproximación. No se pueden sumar días como si fueran personas distintas: por eso el total se llama `uniquesApproxSum`.
- **Series densas.** Los días sin visitas aparecen con `0`; no hay huecos.
- **Caché.** Blogenia guarda cada respuesta 60 segundos por organización y parámetros.

**Parámetros de consulta comunes**

| Nombre | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `from` | string | No | Primer día, `YYYY-MM-DD` en Europe/Madrid, incluido. Por defecto, 29 días antes de `to` (los últimos 30 días). |
| `to` | string | No | Último día, `YYYY-MM-DD` en Europe/Madrid, incluido. Por defecto, hoy. No puede ser futuro. |
| `interval` | string | No | `day` (por defecto), `week` (semanas ISO, empiezan en lunes) o `month` (meses naturales). Siempre en Europe/Madrid. |

> **Agrupar por semana y mes:** `date` es el primer día del grupo: el propio día con `interval=day`, el lunes con `week` y el día 1 con `month`. En el primer grupo puede ser anterior a `from`. `uniquesApprox` de una semana o un mes es la suma de sus días.

## Visitas de toda la cuenta

**`GET /account/stats`**: Total y serie de visitas de todos los blogs de la organización.

Permiso necesario: `stats:read`. Admite `from`, `to` e `interval`.

**Campos de la respuesta**

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `from` | string | Primer día del rango aplicado. |
| `to` | string | Último día del rango aplicado. |
| `interval` | string | `day`, `week` o `month`. |
| `totals` | object | `{ views, uniquesApproxSum, articlesPublished }`. `articlesPublished` cuenta los artículos cuya publicación cae en el rango. |
| `series` | array | `[{ date, views, uniquesApprox, partial? }]`. `partial: true` solo aparece en el grupo que contiene hoy. |
| `blogs` | array | Visitas por blog en el rango: `[{ blogId, name, views }]`. |
| `updatedAt` | string \| null | Hora del último volcado de estadísticas, ISO 8601 en UTC. |

**Petición**

*curl*

```bash
curl "https://api.blogelia.com/v1/account/stats?interval=week" \
  -H "Authorization: Bearer $BLOGENIA_API_KEY"
```

*Node.js*

```js
const res = await fetch('https://api.blogelia.com/v1/account/stats?interval=week', {
  headers: { Authorization: 'Bearer ' + process.env.BLOGENIA_API_KEY },
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const body = await res.json();
console.log(body);
```

*Python*

```python
import os
import requests

res = requests.get(
    "https://api.blogelia.com/v1/account/stats",
    params={"interval": "week"},
    headers={"Authorization": "Bearer " + os.environ["BLOGENIA_API_KEY"]},
    timeout=10,
)
res.raise_for_status()
print(res.json())
```

**Respuesta**

```json
{
  "from": "2026-10-06",
  "to": "2026-11-04",
  "interval": "week",
  "totals": { "views": 1840, "uniquesApproxSum": 1315, "articlesPublished": 9 },
  "series": [
    { "date": "2026-10-05", "views": 310, "uniquesApprox": 232 },
    { "date": "2026-10-12", "views": 402, "uniquesApprox": 290 },
    { "date": "2026-10-19", "views": 455, "uniquesApprox": 318 },
    { "date": "2026-10-26", "views": 520, "uniquesApprox": 365 },
    { "date": "2026-11-02", "views": 153, "uniquesApprox": 110, "partial": true }
  ],
  "blogs": [{ "blogId": "66f1c0a2b3c4d5e6f7a8b9c0", "name": "Reformas Ejemplo", "views": 1840 }],
  "updatedAt": "2026-11-04T14:50:00.000Z"
}
```

**Respuestas**

| Código | Cuándo |
| --- | --- |
| `200` | Respuesta devuelta. |
| `400` | Fechas o parámetros no válidos. Con `code`: `invalid_date` (no es una fecha `YYYY-MM-DD` real), `invalid_range` (`from` posterior a `to`), `range_in_future` (`to` es futuro) o `range_too_long` (más de 400 días). Un parámetro que no existe también da `400`. |
| `401` | Clave ausente, no válida o revocada. |
| `403` | La clave no tiene el permiso `stats:read` (`code: "insufficient_scope"`), tu plan es Free (`plan_required`) o la organización está suspendida (`ORGANIZATION_SUSPENDED`). |
| `429` | Límite del plan superado. Ver [Límites de uso](https://blogelia.com/docs/limites). |

## Visitas de un blog

**`GET /blogs/{blogId}/stats`**: Total y serie de visitas de un blog.

Permiso necesario: `stats:read`. Admite `from`, `to` e `interval`. La respuesta tiene los mismos campos que la de la cuenta, sin `blogs`.

**Parámetros de ruta**

| Nombre | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `blogId` | string | Sí | Identificador del blog. |

**Petición**

*curl*

```bash
curl "https://api.blogelia.com/v1/blogs/66f1c0a2b3c4d5e6f7a8b9c0/stats?from=2026-10-01&to=2026-10-31" \
  -H "Authorization: Bearer $BLOGENIA_API_KEY"
```

*Node.js*

```js
const res = await fetch('https://api.blogelia.com/v1/blogs/66f1c0a2b3c4d5e6f7a8b9c0/stats?from=2026-10-01&to=2026-10-31', {
  headers: { Authorization: 'Bearer ' + process.env.BLOGENIA_API_KEY },
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const body = await res.json();
console.log(body);
```

*Python*

```python
import os
import requests

res = requests.get(
    "https://api.blogelia.com/v1/blogs/66f1c0a2b3c4d5e6f7a8b9c0/stats",
    params={"from": "2026-10-01", "to": "2026-10-31"},
    headers={"Authorization": "Bearer " + os.environ["BLOGENIA_API_KEY"]},
    timeout=10,
)
res.raise_for_status()
print(res.json())
```

**Respuesta**

```json
{
  "from": "2026-10-01",
  "to": "2026-10-31",
  "interval": "day",
  "totals": { "views": 1420, "uniquesApproxSum": 1010, "articlesPublished": 7 },
  "series": [
    { "date": "2026-10-01", "views": 38, "uniquesApprox": 29 },
    { "date": "2026-10-02", "views": 51, "uniquesApprox": 40 }
  ],
  "updatedAt": "2026-11-04T14:50:00.000Z"
}
```

La lista `series` real trae un punto por cada día del rango; aquí se acorta.

**Respuestas**

| Código | Cuándo |
| --- | --- |
| `200` | Respuesta devuelta. |
| `400` | Fechas o parámetros no válidos. Con `code`: `invalid_date` (no es una fecha `YYYY-MM-DD` real), `invalid_range` (`from` posterior a `to`), `range_in_future` (`to` es futuro) o `range_too_long` (más de 400 días). Un parámetro que no existe también da `400`. |
| `404` | El blog no existe o es de otra organización. |
| `401` | Clave ausente, no válida o revocada. |
| `403` | La clave no tiene el permiso `stats:read` (`code: "insufficient_scope"`), tu plan es Free (`plan_required`) o la organización está suspendida (`ORGANIZATION_SUSPENDED`). |
| `429` | Límite del plan superado. Ver [Límites de uso](https://blogelia.com/docs/limites). |

## Ranking de artículos

**`GET /blogs/{blogId}/stats/articles`**: Los artículos de un blog ordenados por visitas en el rango.

Permiso necesario: `stats:read`.

**Parámetros**

| Nombre | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `blogId` | string | Sí | Identificador del blog (en la ruta). |
| `from` | string | No | Primer día, `YYYY-MM-DD` en Europe/Madrid, incluido. Por defecto, 29 días antes de `to` (los últimos 30 días). |
| `to` | string | No | Último día, `YYYY-MM-DD` en Europe/Madrid, incluido. Por defecto, hoy. No puede ser futuro. |
| `sort` | string | No | `-views` (por defecto, de más a menos visitas) o `views` (de menos a más). |
| `limit` | integer | No | Artículos a devolver, de 1 a 100 (por defecto 20). |

**Campos de la respuesta**

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `from` | string | Primer día del rango aplicado. |
| `to` | string | Último día del rango aplicado. |
| `data` | array | `[{ articleId, title, slug, url, publishedAt, views }]`. `publishedAt` es `null` si el artículo ya no está publicado. |
| `updatedAt` | string \| null | Hora del último volcado de estadísticas. |

> **Nota:** Solo aparecen los artículos con visitas en el rango. Los artículos borrados no se listan.

**Petición**

*curl*

```bash
curl "https://api.blogelia.com/v1/blogs/66f1c0a2b3c4d5e6f7a8b9c0/stats/articles?limit=5" \
  -H "Authorization: Bearer $BLOGENIA_API_KEY"
```

*Node.js*

```js
const res = await fetch('https://api.blogelia.com/v1/blogs/66f1c0a2b3c4d5e6f7a8b9c0/stats/articles?limit=5', {
  headers: { Authorization: 'Bearer ' + process.env.BLOGENIA_API_KEY },
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const body = await res.json();
console.log(body);
```

*Python*

```python
import os
import requests

res = requests.get(
    "https://api.blogelia.com/v1/blogs/66f1c0a2b3c4d5e6f7a8b9c0/stats/articles",
    params={"limit": 5},
    headers={"Authorization": "Bearer " + os.environ["BLOGENIA_API_KEY"]},
    timeout=10,
)
res.raise_for_status()
print(res.json())
```

**Respuesta**

```json
{
  "from": "2026-10-06",
  "to": "2026-11-04",
  "data": [
    {
      "articleId": "66f1c0a2b3c4d5e6f7a8b9c1",
      "title": "¿Cuánto cuesta reformar una cocina?",
      "slug": "cuanto-cuesta-reformar-una-cocina",
      "url": "https://reformas-ejemplo.blogelia.com/cuanto-cuesta-reformar-una-cocina",
      "publishedAt": "2026-10-01T09:00:00.000Z",
      "views": 612
    }
  ],
  "updatedAt": "2026-11-04T14:50:00.000Z"
}
```

**Respuestas**

| Código | Cuándo |
| --- | --- |
| `200` | Respuesta devuelta. |
| `400` | Fechas o parámetros no válidos. Con `code`: `invalid_date` (no es una fecha `YYYY-MM-DD` real), `invalid_range` (`from` posterior a `to`), `range_in_future` (`to` es futuro) o `range_too_long` (más de 400 días). Un parámetro que no existe también da `400`. |
| `404` | El blog no existe o es de otra organización. |
| `401` | Clave ausente, no válida o revocada. |
| `403` | La clave no tiene el permiso `stats:read` (`code: "insufficient_scope"`), tu plan es Free (`plan_required`) o la organización está suspendida (`ORGANIZATION_SUSPENDED`). |
| `429` | Límite del plan superado. Ver [Límites de uso](https://blogelia.com/docs/limites). |

## Visitas de un artículo

**`GET /articles/{id}/stats`**: Total y serie de visitas de un artículo.

Permiso necesario: `stats:read`. Admite `from`, `to` e `interval`. Los puntos de la serie no llevan `uniquesApprox`, y `totals` solo tiene `views`.

**Parámetros de ruta**

| Nombre | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `id` | string | Sí | Identificador del artículo. |

**Petición**

*curl*

```bash
curl "https://api.blogelia.com/v1/articles/66f1c0a2b3c4d5e6f7a8b9c1/stats?interval=month" \
  -H "Authorization: Bearer $BLOGENIA_API_KEY"
```

*Node.js*

```js
const res = await fetch('https://api.blogelia.com/v1/articles/66f1c0a2b3c4d5e6f7a8b9c1/stats?interval=month', {
  headers: { Authorization: 'Bearer ' + process.env.BLOGENIA_API_KEY },
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const body = await res.json();
console.log(body);
```

*Python*

```python
import os
import requests

res = requests.get(
    "https://api.blogelia.com/v1/articles/66f1c0a2b3c4d5e6f7a8b9c1/stats",
    params={"interval": "month"},
    headers={"Authorization": "Bearer " + os.environ["BLOGENIA_API_KEY"]},
    timeout=10,
)
res.raise_for_status()
print(res.json())
```

**Respuesta**

```json
{
  "from": "2026-10-06",
  "to": "2026-11-04",
  "interval": "month",
  "totals": { "views": 612 },
  "series": [
    { "date": "2026-10-01", "views": 580 },
    { "date": "2026-11-01", "views": 32, "partial": true }
  ],
  "updatedAt": "2026-11-04T14:50:00.000Z"
}
```

**Respuestas**

| Código | Cuándo |
| --- | --- |
| `200` | Respuesta devuelta. |
| `400` | Fechas o parámetros no válidos. Con `code`: `invalid_date` (no es una fecha `YYYY-MM-DD` real), `invalid_range` (`from` posterior a `to`), `range_in_future` (`to` es futuro) o `range_too_long` (más de 400 días). Un parámetro que no existe también da `400`. |
| `404` | El artículo no existe o es de otra organización. |
| `401` | Clave ausente, no válida o revocada. |
| `403` | La clave no tiene el permiso `stats:read` (`code: "insufficient_scope"`), tu plan es Free (`plan_required`) o la organización está suspendida (`ORGANIZATION_SUSPENDED`). |
| `429` | Límite del plan superado. Ver [Límites de uso](https://blogelia.com/docs/limites). |
