# 05 — API pública v1

Documentación completa de la API REST para integradores. Versión publicada en web: [API.md](API.md) (misma base).

## URLs base

| Recurso | URL |
|---------|-----|
| API | `https://n0xnahm83j.execute-api.sa-east-1.amazonaws.com` |
| OpenAPI JSON | `GET /v1/docs` |
| Health (sin auth) | `GET /v1/health` |
| Portal HTML | `https://d2s8ujt32olfq2.cloudfront.net/docs/index.html` |

---

## Código fuente (Lambda)

| Archivo | Función |
|---------|---------|
| `api/handler.py` | Router HTTP, rutas legacy y `/v1/*` |
| `api/reads.py` | Lecturas paginadas desde DynamoDB |
| `api/auth.py` | Creación/validación/revocación de API keys |
| `api/openapi.py` | Genera spec OpenAPI en `/v1/docs` |

El handler distingue rutas por prefijo:

- `path.startswith("/v1")` → API documentada con autenticación
- Rutas sin prefijo → legacy para el sitio web oficial

---

## Autenticación

### Header

```http
X-API-Key: vtb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Alternativa:

```http
Authorization: Bearer vtb_live_xxxxxxxx...
```

### Rutas sin autenticación

| Ruta | Descripción |
|------|-------------|
| `GET /v1/health` | Estado |
| `GET /v1/docs` | OpenAPI 3.0 |

### Permisos

| Permiso | Endpoints |
|---------|-----------|
| `read` | GET `/v1/metadata`, `/v1/stats`, `/v1/data-urls`, listados, detalle |
| `write` | POST, PATCH, `/v1/upload-url`, `/v1/export` |
| `admin` | Clave maestra `ADMIN_API_KEY` — gestión de keys |

### Formato de claves

| Tipo | Prefijo | Uso |
|------|---------|-----|
| Desarrollador | `vtb_live_` | Integraciones |
| Administrador | `vtb_admin_` | Solo operadores (`~/.vtb-admin-key`) |

Almacenamiento: solo `SHA-256(key)` en DynamoDB (`vtb-terremoto-api-keys`).

---

## Estrategia de lectura: S3 vs API

```
┌─────────────────────────────────────────────────────────┐
│  ALTO VOLUMEN (miles de usuarios, listados completos)   │
│  → CloudFront/S3: listing.json, listing-recent.json     │
│  → Sin API key, sin límite práctico (CDN)             │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│  INTEGRACIÓN (apps, filtros, sync incremental)          │
│  → GET /v1/desaparecidos?limit=&offset=&q=              │
│  → Requiere API key con permiso read                    │
└─────────────────────────────────────────────────────────┘
```

`GET /v1/data-urls` devuelve todas las URLs S3 actualizadas.

---

## Endpoints — Sistema

### `GET /v1/health`

```json
{ "ok": true, "ts": "2026-06-25T13:28:52Z", "version": "1.0.0" }
```

### `GET /v1/docs`

Retorna OpenAPI 3.0 JSON (generado por `api/openapi.py`).

### `GET /v1/metadata`

Copia de `data/metadata.json` desde S3 (o calculado desde DynamoDB si falla S3).

### `GET /v1/stats`

```json
{
  "totales": {
    "desaparecidos": 6540,
    "desaparecidos_activos": 6201,
    "encontrados": 339,
    "avisos": 0,
    "sos_activos": 0,
    "sos_total": 1,
    "edificios": 0
  },
  "updated_at": "2026-06-25T13:25:22Z"
}
```

### `GET /v1/data-urls`

URLs públicas de archivos JSON y prefijo de fotos.

---

## Endpoints — Desaparecidos

### `GET /v1/desaparecidos`

| Query | Tipo | Descripción |
|-------|------|-------------|
| `q` | string | Búsqueda en nombre, apellido, cédula, ubicación, descripción |
| `estado` | string | `desaparecido` \| `encontrado` |
| `limit` | int | 1–100, default 24 |
| `offset` | int | default 0 |

Respuesta:

```json
{
  "data": [ { "id": "...", "nombre": "...", ... } ],
  "pagination": {
    "total": 6201,
    "limit": 24,
    "offset": 0,
    "has_more": true
  }
}
```

### `GET /v1/desaparecidos/{id}`

Registro completo o `404`.

### `POST /v1/desaparecidos`

Cuerpo JSON — ver [04-modelo-de-datos.md](04-modelo-de-datos.md).

Respuesta `201` con el registro creado (incluye `id` generado).

### `PATCH /v1/desaparecidos/{id}`

Marcar encontrada:

```json
{ "estado": "encontrado", "notas": "Texto obligatorio en UI oficial" }
```

### `GET /v1/desaparecidos/{id}/avisos`

```json
{ "data": [ { "id": "...", "mensaje": "...", ... } ] }
```

### `POST /v1/avisos`

```json
{
  "desaparecido_id": "uuid",
  "nombre_aviso": "Pedro",
  "telefono_aviso": "+58...",
  "mensaje": "La vi en..."
}
```

---

## Endpoints — SOS

### `GET /v1/sos`

Query: `q`, `estado` (`activo` \| `resuelto`), `limit`, `offset`.

### `POST /v1/sos`

```json
{
  "descripcion": "Personas atrapadas en edificio",
  "ubicacion_texto": "Av. Principal, Cumaná",
  "lat": 10.45,
  "lng": -64.18,
  "telefono": "+58412...",
  "reportado_por_nombre": "Vecino",
  "fuente": "https://mi-app.org"
}
```

`descripcion` obligatoria.

### `PATCH /v1/sos/{id}`

```json
{ "estado": "resuelto", "notas": "Rescatados por bomberos" }
```

---

## Endpoints — Edificios

### `GET /v1/edificios`

Query: `q`, `estado`, `limit`, `offset`.

### `POST /v1/edificios`

```json
{
  "direccion": "Edif. Costa Brava, Piso 3",
  "descripcion": "Grietas en fachada, posibles atrapados",
  "severidad": "grave",
  "foto_url": "foto.webp",
  "reportado_por_nombre": "...",
  "reportado_por_telefono": "...",
  "fuente": "https://..."
}
```

### `PATCH /v1/edificios/{id}`

```json
{ "estado": "atendido", "notas": "..." }
```

Estados: `reportado` \| `verificado` \| `atendido`.

---

## Endpoints — Medios

### `POST /v1/upload-url`

```json
{ "filename": "foto.webp", "content_type": "image/webp" }
```

Flujo:

1. `POST /v1/upload-url` → `{ upload_url, foto_url, public_url }`
2. `PUT` binario a `upload_url` con `Content-Type: image/webp`
3. Incluir `foto_url` en POST del registro

---

## Endpoints — Admin

Requieren `X-API-Key: {ADMIN_API_KEY}`.

### `POST /v1/admin/keys`

```json
{
  "name": "Contacto",
  "organization": "ONG / Proyecto",
  "email": "dev@ejemplo.org",
  "fuente": "https://su-sitio.org",
  "permissions": ["read", "write"]
}
```

Respuesta `201`:

```json
{
  "message": "Guarda esta clave ahora. No se volverá a mostrar.",
  "api_key": "vtb_live_...",
  "key": { "key_id": "...", "prefix": "vtb_live_17a9...", ... }
}
```

### `GET /v1/admin/keys`

Lista metadatos sin secretos.

### `DELETE /v1/admin/keys/{key_id}`

Revoca (marca `active: false`).

---

## Rutas legacy (sin `/v1/`, sin API key)

Para el frontend en `web/`. **No usar en nuevas integraciones.**

| Método | Ruta |
|--------|------|
| GET | `/health` |
| POST | `/desaparecidos`, `/avisos`, `/sos`, `/edificios`, `/upload-url` |
| PATCH | `/desaparecidos/{id}`, `/sos/{id}`, `/edificios/{id}` |
| POST | `/export` |

---

## Códigos de error

| HTTP | Body | Causa |
|------|------|-------|
| 400 | `{ "error": "mensaje" }` | Validación fallida |
| 401 | `{ "error": "unauthorized", ... }` | Sin key o key inválida |
| 403 | `{ "error": "forbidden", ... }` | Sin permiso |
| 404 | `{ "error": "not_found" }` | Ruta o registro inexistente |
| 500 | `{ "error": "internal_error", "detail": "..." }` | Error Lambda |

---

## CORS

Todas las respuestas incluyen:

```
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type, X-API-Key, Authorization
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
```

---

## Export tras escritura

Cada POST/PATCH exitoso:

1. Escribe en DynamoDB
2. Invoca `vtb-terremoto-export` (async)
3. Export regenera JSON en S3

Latencia hasta que aparezca en S3: **~5–60 s** según tamaño de datos.

Forzar export manual (requiere write o legacy):

```http
POST /v1/export
```

---

## Ejemplos por lenguaje

### curl — listar

```bash
curl -s "https://n0xnahm83j.execute-api.sa-east-1.amazonaws.com/v1/desaparecidos?limit=5" \
  -H "X-API-Key: vtb_live_TU_CLAVE"
```

### Python — leer S3 masivo

```python
import json, gzip, urllib.request

url = "https://d2s8ujt32olfq2.cloudfront.net/data/desaparecidos/listing.json"
with urllib.request.urlopen(url) as r:
    raw = gzip.decompress(r.read()) if r.headers.get("Content-Encoding") == "gzip" else r.read()
data = json.loads(raw)
personas = [dict(zip(data["fields"], row)) for row in data["rows"]]
```

### JavaScript — registrar

```javascript
await fetch(`${API}/v1/desaparecidos`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-API-Key": KEY },
  body: JSON.stringify({
    nombre: "Ana", apellido: "López",
    ultima_ubicacion: "Caracas",
    reportado_por_nombre: "App X",
    reportado_por_telefono: "+58412...",
    fuente: "https://app-x.org",
  }),
});
```

---

## OpenAPI / Postman

1. `GET https://API/v1/docs`
2. Importar JSON en Postman: File → Import → Raw text
3. Configurar variable de colección `api_key`
4. Header de colección: `X-API-Key: {{api_key}}`

---

## Crear claves (operador)

```bash
python3 scripts/create_api_key.py \
  --name "Portal Vecinal" \
  --org "Comunidad X" \
  --email "dev@vecinal.org" \
  --fuente "https://vecinal.org" \
  --permissions read,write
```

Ver [07-scripts-y-operaciones.md](07-scripts-y-operaciones.md).

---

## Buenas prácticas para integradores

1. Lectura masiva → **S3**, no API.
2. Siempre incluir `fuente` al escribir.
3. Buscar antes de crear (evitar duplicados por cédula).
4. Cachear `metadata.json`; refrescar cuando cambie `updated_at`.
5. No exponer API keys en frontend público (usar backend propio).
6. Respetar que los datos son responsabilidad de quien los envía.

Ver también [API.md](API.md) (copia publicada en el sitio).
