# Alartman — API pública de alertas (instrucciones para una IA)

Este documento está escrito para que un asistente de IA (Claude, ChatGPT, Copilot…) implemente
la integración de las alertas de Alartman en otro sitio web sin necesitar más contexto.
Si eres una persona: pega este archivo completo en tu asistente y dile qué quieres construir.

**URL base:** `https://alartman.com`
(si Alartman corre en otro dominio, sustituye la URL base en todos los ejemplos)

---

## 1. Qué ofrece esta API

- Lectura pública de las alertas publicadas en canales **públicos** de Alartman.
- **No requiere llave ni registro.** No hay OAuth, no hay tokens.
- CORS abierto (`Access-Control-Allow-Origin: *`): se puede llamar desde el navegador.
- Respuestas JSON con `Cache-Control: public, max-age=60`.
- Límite: **300 peticiones por hora por IP**. Si se supera: HTTP 429 con `{"ok":false,"error":"..."}`.
- Nunca expone alertas personales, grupos privados ni canales privados.

## 2. Endpoints

| Método y ruta | Devuelve |
|---|---|
| `GET /api/v1/alerts` | Lista de alertas (JSON) |
| `GET /api/v1/channels` | Canales públicos disponibles |
| `GET /api/v1/categories` | Categorías con número de canales |
| `GET /api/v1/feed.xml` | Las últimas 50 alertas en RSS 2.0 |
| `GET /api/v1/llms.txt` | Este mismo documento |

### 2.1 `GET /api/v1/alerts` — parámetros

| Parámetro | Valores | Por defecto | Notas |
|---|---|---|---|
| `kind` | `official`, `community`, `all` | `official` | `official` = canales verificados por el administrador |
| `channel` | slug de canal | — | p. ej. `sismos-mexico-ssn-unam` |
| `category` | slug de categoría | — | p. ej. `sismos`, `clima-local`, `incendios` |
| `priority` | `low`, `normal`, `high`, `critical` | — | actúa como **mínimo** (p. ej. `high` incluye `critical`) |
| `since_id` | entero | — | sólo alertas con `id` mayor; es el mecanismo correcto para sondeo |
| `since` | fecha ISO 8601 | — | p. ej. `2026-08-21T00:00:00Z` |
| `q` | texto | — | busca en título y cuerpo |
| `limit` | 1–100 | 20 | |
| `order` | `desc`, `asc` | `desc` | |

### 2.2 Forma de la respuesta

```json
{
  "ok": true,
  "count": 1,
  "latest_id": 1876,
  "generated_at": "2026-08-21T13:40:02-06:00",
  "alerts": [
    {
      "id": 1876,
      "title": "Sismo M5.3 · 22 km SSW de Pinotepa Nacional",
      "body": "Hora: 2026-08-21 13:22 · Profundidad: 18 km · Fuente: SSN",
      "priority": "high",
      "source": "module",
      "created_at": "2026-08-21T13:24:10-06:00",
      "link": "https://www.ssn.unam.mx/...",
      "image": null,
      "permalink": "https://alartman.com/a/1876",
      "channel": {
        "slug": "sismos-mexico-ssn-unam",
        "name": "Sismos México (SSN-UNAM)",
        "icon": "〰️",
        "official": true,
        "url": "https://alartman.com/c/sismos-mexico-ssn-unam"
      },
      "category": { "slug": "sismos", "name": "Sismos" }
    }
  ]
}
```

Notas de tipos:

- `body`, `link`, `image` y `category` pueden ser `null`. Trátalos como opcionales.
- `created_at` es ISO 8601 con zona horaria de México (`-06:00`/`-05:00`).
- `priority`: `low` < `normal` < `high` < `critical`. Úsalo para color y orden visual.
- `source`: de dónde salió la alerta (`module` = fuente oficial automática, `manual` = publicada por una persona, `api`, `webhook`, `rss`, `json`, `quake`).
- `id` es incremental global: sirve para deduplicar y para `since_id`.

## 3. Reglas que debes seguir al implementar

1. **No consultes más de una vez por minuto.** Si necesitas tiempo real, muestra un aviso de "actualizado hace X" en vez de sondear más rápido.
2. **Usa `since_id`** guardando `latest_id` de la respuesta anterior; así no repites alertas ni gastas peticiones.
3. **Cachea en tu servidor** (60 s como mínimo). Si tu sitio tiene tráfico, llama a la API desde el backend y sirve tu propia copia a los visitantes, no desde cada navegador.
4. **Cita la fuente**: muestra `channel.name` junto a cada alerta y enlaza a `permalink` (o a `link` si existe). Las alertas oficiales reproducen información de SSN-UNAM, USGS, NOAA/NHC, CENAPRED, CISA y otros organismos.
5. **No presentes las alertas como propias** ni edites su contenido de forma que cambie el sentido.
6. **Deja claro que no es un sistema de alerta temprana.** Las alertas llegan minutos después del evento según publique cada organismo. En emergencias, el 911 y Protección Civil son la referencia.
7. **Maneja los errores**: si la respuesta no trae `ok: true`, si hay 429, o si la petición falla, muestra la última copia buena y no rompas la página.

## 4. Implementaciones de referencia

### 4.1 HTML + JavaScript (widget en el navegador)

```html
<div id="alartman" class="alartman-widget"></div>
<script>
(async () => {
  const BASE = 'https://alartman.com';
  const box = document.getElementById('alartman');
  const COLORS = { low: '#8A92AD', normal: '#6D4AE6', high: '#F39C12', critical: '#E5332A' };
  const rel = iso => {
    const s = (Date.now() - new Date(iso)) / 1000;
    if (s < 60) return 'hace un momento';
    if (s < 3600) return `hace ${Math.floor(s / 60)} min`;
    if (s < 86400) return `hace ${Math.floor(s / 3600)} h`;
    return new Date(iso).toLocaleDateString('es-MX');
  };
  const render = alerts => {
    box.innerHTML = alerts.map(a => `
      <article style="border-left:4px solid ${COLORS[a.priority]};padding:10px 14px;margin-bottom:8px;background:#fff;border-radius:10px">
        <p style="margin:0 0 4px;font-size:12px;color:#8A92AD">
          ${a.channel.icon} ${escapeHtml(a.channel.name)} · ${rel(a.created_at)}
        </p>
        <h3 style="margin:0 0 4px;font-size:15px">
          <a href="${a.permalink}" target="_blank" rel="noopener">${escapeHtml(a.title)}</a>
        </h3>
        ${a.body ? `<p style="margin:0;font-size:13px;color:#515B7A">${escapeHtml(a.body)}</p>` : ''}
      </article>`).join('') || '<p>Sin alertas por ahora.</p>';
  };
  const escapeHtml = s => s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));

  let latestId = 0, cache = [];
  const load = async () => {
    try {
      const url = new URL(BASE + '/api/v1/alerts');
      url.searchParams.set('kind', 'official');
      url.searchParams.set('limit', '10');
      if (latestId) url.searchParams.set('since_id', latestId);
      const res = await fetch(url);
      if (!res.ok) return;                       // 429 u otro: conserva lo que ya se muestra
      const data = await res.json();
      if (!data.ok) return;
      if (data.alerts.length) {
        cache = [...data.alerts, ...cache].slice(0, 10);
        latestId = data.latest_id;
        render(cache);
      }
    } catch (e) { /* sin red: no romper la página */ }
  };
  await load();
  setInterval(load, 60000);                      // una vez por minuto
})();
</script>
```

### 4.2 PHP (proxy con caché en el servidor — recomendado para sitios con tráfico)

```php
<?php
// alertas.php — sirve una copia cacheada a tus visitantes
function alartman_alerts(array $params = [], int $ttl = 60): array {
    $params += ['kind' => 'official', 'limit' => 10];
    $url  = 'https://alartman.com/api/v1/alerts?' . http_build_query($params);
    $file = sys_get_temp_dir() . '/alartman_' . md5($url) . '.json';

    if (is_file($file) && time() - filemtime($file) < $ttl) {
        return json_decode(file_get_contents($file), true);
    }
    $ctx  = stream_context_create(['http' => ['timeout' => 8, 'header' => "User-Agent: MiSitio/1.0\r\n"]]);
    $body = @file_get_contents($url, false, $ctx);
    if ($body === false) {   // sin red: última copia buena
        return is_file($file) ? json_decode(file_get_contents($file), true) : ['ok' => false, 'alerts' => []];
    }
    file_put_contents($file, $body);
    return json_decode($body, true) ?: ['ok' => false, 'alerts' => []];
}

$data = alartman_alerts(['category' => 'sismos', 'priority' => 'normal']);
foreach ($data['alerts'] ?? [] as $a) {
    printf(
        '<article class="alerta p-%s"><small>%s · %s</small><h3><a href="%s">%s</a></h3><p>%s</p></article>',
        htmlspecialchars($a['priority']),
        htmlspecialchars($a['channel']['name']),
        date('d/m/Y H:i', strtotime($a['created_at'])),
        htmlspecialchars($a['permalink']),
        htmlspecialchars($a['title']),
        htmlspecialchars($a['body'] ?? '')
    );
}
```

### 4.3 Python

```python
import requests, time

BASE = "https://alartman.com"
latest_id = 0

def poll():
    global latest_id
    params = {"kind": "official", "limit": 50}
    if latest_id:
        params["since_id"] = latest_id
    r = requests.get(f"{BASE}/api/v1/alerts", params=params, timeout=10)
    r.raise_for_status()
    data = r.json()
    for a in reversed(data["alerts"]):          # de la más antigua a la más nueva
        print(f"[{a['priority']}] {a['channel']['name']}: {a['title']} — {a['permalink']}")
    latest_id = data["latest_id"] or latest_id

while True:
    try:
        poll()
    except Exception as e:
        print("error:", e)
    time.sleep(60)
```

### 4.4 WordPress (shortcode `[alartman]`)

```php
// functions.php de tu tema
add_shortcode('alartman', function ($atts) {
    $a = shortcode_atts(['kind' => 'official', 'category' => '', 'limit' => 5], $atts);
    $key = 'alartman_' . md5(serialize($a));
    if (false === ($html = get_transient($key))) {
        $url = add_query_arg(array_filter($a), 'https://alartman.com/api/v1/alerts');
        $res = wp_remote_get($url, ['timeout' => 8]);
        $out = '';
        if (!is_wp_error($res)) {
            $data = json_decode(wp_remote_retrieve_body($res), true);
            foreach ($data['alerts'] ?? [] as $al) {
                $out .= sprintf(
                    '<li class="alartman-%s"><a href="%s" target="_blank" rel="noopener">%s</a> <small>%s</small></li>',
                    esc_attr($al['priority']), esc_url($al['permalink']),
                    esc_html($al['title']), esc_html($al['channel']['name'])
                );
            }
        }
        $html = $out ? "<ul class=\"alartman\">$out</ul>" : '<p>Sin alertas.</p>';
        set_transient($key, $html, 60);
    }
    return $html;
});
```

### 4.5 RSS (sin escribir código)

```
https://alartman.com/api/v1/feed.xml?kind=official
https://alartman.com/api/v1/feed.xml?channel=sismos-mexico-ssn-unam
```
Sirve para lectores de RSS, Zapier/n8n/Make, widgets de RSS de cualquier CMS y bots de Telegram/Discord.

## 5. Descubrir qué hay disponible

```bash
curl -s "https://alartman.com/api/v1/channels?kind=official" | jq '.channels[] | {slug, name, category: .category.slug}'
curl -s "https://alartman.com/api/v1/categories" | jq '.categories[] | select(.channels > 0)'
```

Ejemplos de slugs habituales: `sismos-mexico-ssn-unam`, `sismos-fuertes-en-el-mundo-usgs`,
`huracanes-y-tormentas-tropicales`, `alertas-de-tsunami-pacifico`, `desastres-globales-gdacs`,
`clima-espacial-y-tormentas-solares`, `actividad-volcanica`.
Los módulos por ciudad añaden la zona al slug, por ejemplo `clima-severo-guadalajara-jal`.
**No los escribas de memoria: consulta `/api/v1/channels` y usa los que existan.**

## 6. Prompt sugerido para la IA

> Usando el documento anterior, implementa en mi sitio (indica aquí tu tecnología: HTML estático,
> WordPress, Laravel, Next.js…) una sección "Alertas" que muestre las últimas N alertas oficiales
> de Alartman filtradas por (categoría/canal). Requisitos: consultar como máximo una vez por
> minuto usando `since_id`, cachear en el servidor 60 segundos, mostrar prioridad con color,
> mostrar el nombre del canal como fuente, enlazar al `permalink`, degradar sin romper la página
> si la API falla, y añadir la nota de que no es un sistema de alerta temprana.

## 7. Preguntas frecuentes

- **¿Necesito permiso?** No para leer canales públicos. Si vas a redistribuir masivamente o quieres
  un límite mayor, escribe a contacto@alartman.com.
- **¿Puedo publicar alertas desde mi sistema?** Sí, pero eso es otra API: crea un canal en Alartman
  y usa su token con `POST /api/emit` (ver `/docs`).
- **¿Hay webhooks salientes?** Todavía no; usa sondeo con `since_id` o el RSS.
- **¿Cambiará esta API?** `v1` es estable: sólo se añadirán campos nuevos. Si algo cambia de forma
  incompatible, será bajo `/api/v2/`.
