{
  "openapi": "3.1.0",
  "info": {
    "title": "API de datos del terremoto de Colombia — crisiscolombia.org",
    "version": "2.0.0",
    "summary": "API pública de solo lectura del conjunto de datos agregado del terremoto.",
    "description": "API REST de solo lectura sobre reportes normalizados de fuentes abiertas tras el terremoto M7.4 de Colombia del 10 de agosto de 2026. Incluye daño estructural, puntos de acopio, necesidades, información general y evaluaciones declaradas por fuentes (pendiente, habitable o inconsistente). La categoría evaluación_habitable significa que una fuente reporta ocupación sin restricciones; puede coexistir con daño leve o moderado y no constituye una verificación independiente.\n\nAlcance: se excluyen conteos de fallecidos/heridos, listas de víctimas y búsquedas de personas desaparecidas. Las referencias a personas afectadas solo se conservan cuando forman parte de un registro accionable de ayuda, necesidad o rescate. Solo daño, acopio y necesidad se renderizan como marcadores; información general y evaluaciones neutrales nunca se convierten en daño rojo.\n\nCómo se construyen los datos: (1) agregamos fuentes públicas conservando URL e identificador del ítem; (2) un clasificador debe emitir un veredicto exitoso para cada registro o la publicación se bloquea; (3) las coordenadas se marcan como explícitas o geocodificadas y las contradicciones materiales con el departamento o municipio reportado se retienen fuera de los feeds operativos; (4) entidades, fechas y fotos solo heredan evidencia que respalda la estructura mostrada; las variantes inequívocas se unifican y los nombres casi idénticos que no pueden resolverse con seguridad se retienen para revisión; (5) todos los volcados pasan una auditoría cruzada antes de publicarse.\n\nConfianza fail-closed: más registros o nombres de fuente no demuestran independencia. independent_origins solo cuenta linajes de origen explícitamente establecidos mediante origin_id, y Verificado también exige una revisión completada de procedencia, fuente, fecha y ubicación. Si falta cualquiera de esas evidencias, el registro permanece Reportado.\n\nNaturaleza y límites: es un agregado de fuentes abiertas en emergencia y NO reemplaza a las autoridades oficiales. Verifique siempre la procedencia en fuentes[].\n\nUso justo: para descargas masivas use /facts.json o /colapsos.csv.",
    "license": {
      "name": "CC-BY-4.0",
      "url": "https://creativecommons.org/licenses/by/4.0/"
    },
    "contact": {
      "name": "crisiscolombia.org",
      "url": "https://crisiscolombia.org/api"
    }
  },
  "servers": [
    {
      "url": "https://crisiscolombia.org",
      "description": "Producción"
    }
  ],
  "paths": {
    "/api/v1/facts": {
      "get": {
        "operationId": "listarHechos",
        "summary": "Lista los reportes normalizados, con filtros combinables.",
        "description": "Devuelve todos los hechos dentro del alcance, incluidos estados neutrales de evaluación, ordenados del más reciente al más antiguo. Solo daño, acopio y necesidad pertenecen a la capa de marcadores.",
        "parameters": [
          {
            "name": "categoria",
            "in": "query",
            "description": "Filtra por categoría. Lista separada por comas. Un valor inválido devuelve 400.",
            "required": false,
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "daño",
                  "acopio",
                  "necesidad",
                  "información",
                  "evaluación_pendiente",
                  "evaluación_habitable",
                  "evaluación_inconsistente"
                ]
              }
            },
            "example": "acopio,necesidad"
          },
          {
            "name": "estado",
            "in": "query",
            "description": "Filtra por departamento colombiano. El campo conserva el nombre legado estado. Lista separada por comas, sin distinción de mayúsculas/minúsculas y con coincidencia exacta.",
            "required": false,
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "Chocó,Caldas"
          },
          {
            "name": "municipio",
            "in": "query",
            "description": "Filtra por municipio. Lista separada por comas, sin distinción de mayúsculas/minúsculas.",
            "required": false,
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "San José del Palmar,Manizales"
          },
          {
            "name": "nivel",
            "in": "query",
            "description": "Filtra por nivel de daño (solo aplica a registros de categoría daño). Lista separada por comas. Valores típicos: colapso_total, severo, parcial, dano.",
            "required": false,
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "colapso_total",
                  "severo",
                  "parcial",
                  "dano"
                ]
              }
            },
            "example": "colapso_total,severo"
          },
          {
            "name": "tipo_necesidad",
            "in": "query",
            "description": "Filtra por tipo de necesidad (solo aplica a registros de categoría necesidad). Lista separada por comas. Ej: agua, comida, medicinas, insumos, voluntarios, sangre, rescate, refugio, ropa, otro.",
            "required": false,
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "agua,medicinas"
          },
          {
            "name": "coord_origen",
            "in": "query",
            "description": "Filtra por procedencia de las coordenadas. Lista separada por comas. explicita = las coordenadas vinieron con el dato (metadatos de la fuente original, p. ej. eltiempo.com / sgc.gov.co, o indicadas en el post); su precisión no está verificada de forma independiente y pueden ser aproximadas o de relleno. geocodificada = nuestro pipeline las derivó de un nombre de lugar mediante Nominatim y son aproximadas. Ningún valor garantiza una dirección exacta.",
            "required": false,
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "explicita",
                  "geocodificada"
                ]
              }
            },
            "example": "explicita"
          },
          {
            "name": "fuente",
            "in": "query",
            "description": "Conserva solo los registros que tengan al menos una fuente en la lista. Lista separada por comas. Coincide sin distinción de mayúsculas por nombre de la fuente y por subcadena del dominio (host), de modo que 'eltiempo.com' coincide con ese medio.",
            "required": false,
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "eltiempo.com"
          },
          {
            "name": "excluir_fuente",
            "in": "query",
            "description": "Anti-circular: descarta los registros cuyas fuentes estén TODAS en la lista de exclusión. Un registro sigue apareciendo si además tiene otra fuente no excluida. Pensado para que un sitio de origen consuma la API sin re-ingerir sus propios datos. Mismo criterio de coincidencia que 'fuente'.",
            "required": false,
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "eltiempo.com"
          },
          {
            "name": "bbox",
            "in": "query",
            "description": "Caja delimitadora geográfica: minLon,minLat,maxLon,maxLat (4 números). Conserva solo registros con coordenadas dentro de la caja. Un valor inválido devuelve 400.",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?$"
            },
            "example": "-77.5,4.0,-75.0,6.5"
          },
          {
            "name": "desde",
            "in": "query",
            "description": "Marca de tiempo ISO 8601. Conserva solo registros con fecha (última vez visto) mayor o igual a este instante. Un valor inválido devuelve 400.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2026-08-10T00:00:00Z"
          },
          {
            "name": "min_fuentes",
            "in": "query",
            "description": "Número mínimo de registros de fuente distintos (n_fuentes) que debe tener el registro. Mide volumen de apoyo, no independencia de origen.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "example": 2
          },
          {
            "name": "limite",
            "in": "query",
            "description": "Cantidad máxima de registros a devolver. Por defecto 1000; máximo 5000 (se recorta).",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1000,
              "minimum": 0,
              "maximum": 5000
            },
            "example": 100
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Número de registros a saltar (paginación). Por defecto 0.",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            },
            "example": 0
          },
          {
            "name": "formato",
            "in": "query",
            "description": "Formato de salida. json (por defecto): sobre con metadatos + datos. geojson: FeatureCollection (solo registros con coordenadas). csv: tabla con cabecera (las fuentes se aplanan a una columna 'nombre(url);nombre(url)').",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "geojson",
                "csv"
              ],
              "default": "json"
            },
            "example": "geojson"
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de hechos en el formato solicitado.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "public, max-age=60, s-maxage=120"
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string"
                },
                "description": "*"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RespuestaJSON"
                }
              },
              "application/geo+json": {
                "schema": {
                  "$ref": "#/components/schemas/ColeccionGeoJSON"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Parámetro inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Método no permitido (solo se admite GET).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Conjunto de datos no disponible temporalmente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Fuente": {
        "type": "object",
        "description": "Fuente canónica que respalda el hecho mostrado. La lista se deduplica por URL o, si falta, por autor.",
        "properties": {
          "fuente": {
            "type": "string",
            "description": "Nombre/handle de la fuente, ya limpiado (sin '@' inicial; para direcciones puente 'x@dominio' se conserva solo 'x')."
          },
          "url": {
            "type": "string",
            "description": "Enlace a la publicación original (puede ser cadena vacía)."
          },
          "kind": {
            "type": "string",
            "description": "Tipo interno de fuente usado por el modelo de confianza."
          },
          "origin_id": {
            "type": "string",
            "description": "Identificador opcional del origen subyacente, asignado solo por un adaptador con linaje explícito o una revisión humana documentada. Dos fuentes con el mismo origin_id cuentan como un solo origen; la ausencia no se interpreta como independencia."
          },
          "post_id": {
            "type": "string",
            "description": "Identificador estable del ítem de procedencia."
          },
          "observado_en": {
            "type": "string",
            "format": "date-time",
            "description": "Fecha de esta evidencia. Las fechas del registro se calculan solo con las fuentes mostradas."
          },
          "entity_support": {
            "type": "string",
            "enum": ["source_record", "explicit_name", "not_applicable"],
            "description": "Cómo respalda esta fuente la entidad mostrada."
          },
          "media_entity_support": {
            "type": "string",
            "enum": ["source_record", "single_entity", "not_applicable"],
            "description": "Nivel requerido para que una foto de esta fuente pueda mostrarse con la entidad."
          }
        }
      },
      "Hecho": {
        "type": "object",
        "description": "Un reporte normalizado con procedencia trazable; no implica verificación.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador determinista con prefijo cv-, derivado de la identidad de la entidad, categoría y evidencia fuente. Es único dentro de cada versión del conjunto.",
            "example": "cv-219c7ab54981438cbe05e2fb"
          },
          "categoria": {
            "type": "string",
            "enum": [
              "daño",
              "acopio",
              "necesidad",
              "información",
              "evaluación_pendiente",
              "evaluación_habitable",
              "evaluación_inconsistente"
            ],
            "description": "Tipo de hecho. Las cuatro categorías neutrales no son marcadores de daño."
          },
          "nivel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nivel de daño, presente solo cuando la categoría es 'daño'. Es null en todas las demás categorías."
          },
          "conflicto_nivel": {
            "type": "boolean",
            "description": "true cuando la evidencia agrupada contiene niveles específicos de daño incompatibles. En ese caso nivel='dano' y descripcion muestra un aviso neutral hasta revisión humana."
          },
          "estructura": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nombre específico del edificio o estructura cuando se logró identificar (p. ej. 'Edificio San Judas Tadeo'). Es null cuando el reporte es a nivel de zona y no nombra una estructura concreta."
          },
          "municipio": {
            "type": "string",
            "description": "Municipio del hecho (nombre legible para mostrar). Puede venir vacío en hechos que no tienen municipio asignado."
          },
          "estado": {
            "type": "string",
            "description": "Departamento colombiano del hecho. Se conserva el nombre de campo legado estado; si el reporte no lo trae, se completa con el departamento del municipio según el gazetteer."
          },
          "zona": {
            "type": "string",
            "description": "Zona, sector o referencia local (barrio, urbanización, avenida). Texto libre; puede estar vacío."
          },
          "lat": {
            "type": [
              "number",
              "null"
            ],
            "description": "Latitud en WGS84, o null si el hecho no está geolocalizado. La precisión depende de coord_origen."
          },
          "lon": {
            "type": [
              "number",
              "null"
            ],
            "description": "Longitud en WGS84, o null si el hecho no está geolocalizado. La precisión depende de coord_origen."
          },
          "coord_origen": {
            "type": "string",
            "enum": [
              "explicita",
              "geocodificada",
              ""
            ],
            "description": "Procedencia de lat/lon. 'explicita' = las coordenadas vinieron con el dato (catálogo de la fuente original como eltiempo.com o sgc.gov.co, o indicadas explícitamente en el post); su precisión no está verificada de forma independiente y pueden ser aproximadas o de relleno. 'geocodificada' = no venían con el dato y el pipeline las derivó de un nombre de lugar mediante OSM/Nominatim; son aproximadas y pueden caer en el centroide del área o estar desplazadas. Cadena vacía si no aplica o se desconoce. Ningún valor garantiza una dirección exacta."
          },
          "descripcion": {
            "type": "string",
            "description": "El hecho resumido en una frase, normalizado por un modelo de IA a partir del o los posts originales."
          },
          "descripcion_en": {
            "type": "string",
            "description": "Traducción inglesa validada. Cadena vacía si no hay traducción válida."
          },
          "traducido_por": {
            "type": ["string", "null"],
            "description": "Método de traducción, o null si descripcion_en está vacía."
          },
          "idioma_origen": {
            "type": "string",
            "enum": ["es"]
          },
          "tipo_necesidad": {
            "type": [
              "string",
              "null"
            ],
            "description": "Recurso solicitado, presente solo cuando la categoría es 'necesidad': agua, comida, medicinas, insumos, voluntarios, sangre, rescate, refugio, ropa u otro. Es null en las demás categorías."
          },
          "atrapados": {
            "type": "boolean",
            "description": "true si el texto menciona personas atrapadas o bajo escombros. Es un indicio de fuentes abiertas, NO una confirmación oficial: verifícalo antes de actuar."
          },
          "n_fuentes": {
            "type": "integer",
            "description": "Número de registros de fuente DISTINTOS que respaldan el hecho (deduplicados por URL; si no hay URL, por autor). Un valor mayor indica más registros de apoyo, no necesariamente orígenes independientes. Igual a la longitud de 'fuentes'."
          },
          "fuentes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Fuente"
            },
            "description": "Procedencia completa: todas las fuentes distintas del hecho, deduplicadas por url. Úsala para dar crédito a la fuente original y para deduplicar contra tus propios datos; combínala con el filtro excluir_fuente para no reingestar lo tuyo."
          },
          "media": {
            "type": "array",
            "items": {"type": "string"},
            "description": "URLs de medios cuya fuente y entidad están atribuidas en media_items."
          },
          "media_items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {"type": "string"},
                "post_id": {"type": "string"},
                "source": {"type": "string"},
                "source_url": {"type": "string"}
              },
              "required": ["url", "post_id", "source", "source_url"]
            },
            "description": "Atribución por archivo. Un medio multi-entidad ambiguo no se publica."
          },
          "confianza": {
            "type": "object",
            "description": "Estado de confianza fail-closed y sus ejes. distinct_named_sources cuenta nombres canónicos; independent_origins solo cuenta linajes de origen explícitamente establecidos mediante origin_id. Verificado exige al menos dos orígenes confirmados, independence_status='confirmed', checks.status='verified', credibilidad A/B y ninguna fuente no fiable. Si falta una compuerta, un registro atribuible permanece Reportado.",
            "properties": {
              "badge": {
                "type": "string",
                "enum": ["verified", "reported", "unverified", "inspeccionado", "pendiente", "en_revision"]
              },
              "model": {
                "type": "string",
                "const": "2.0"
              },
              "credibility": {
                "type": "object",
                "properties": {
                  "grade": {"type": "string", "enum": ["A", "B", "C", "D", "E"]},
                  "independent_origins": {"type": "integer", "minimum": 0},
                  "distinct_named_sources": {"type": "integer", "minimum": 0},
                  "independence_status": {"type": "string", "enum": ["confirmed", "not_established"]},
                  "method": {"type": "string"}
                }
              },
              "checks": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": ["not_run", "verified", "source-assessed", "pending", "conflict"]
                  },
                  "elements": {
                    "type": "object",
                    "description": "Solo aparece con status=verified; los cuatro elementos deben valer verified.",
                    "properties": {
                      "provenance": {"type": "string", "const": "verified"},
                      "source": {"type": "string", "const": "verified"},
                      "date": {"type": "string", "const": "verified"},
                      "location": {"type": "string", "const": "verified"}
                    },
                    "required": ["provenance", "source", "date", "location"]
                  }
                }
              }
            }
          },
          "fecha": {
            "type": "string",
            "format": "date-time",
            "description": "Última fecha entre las fuentes canónicas que respaldan la entidad mostrada. Una publicación cercana no relacionada no refresca este campo."
          },
          "primera_vez": {
            "type": "string",
            "format": "date-time",
            "description": "Primera vez que se detectó el hecho (ISO 8601)."
          }
        },
        "required": [
          "id",
          "categoria",
          "conflicto_nivel",
          "municipio",
          "estado",
          "descripcion",
          "n_fuentes",
          "fuentes",
          "fecha"
        ]
      },
      "Meta": {
        "type": "object",
        "properties": {
          "cantidad": {
            "type": "integer",
            "description": "Número de registros devueltos en esta página."
          },
          "total": {
            "type": "integer",
            "description": "Total de registros tras aplicar los filtros (antes de limite/offset)."
          },
          "generado": {
            "type": "string",
            "format": "date-time",
            "description": "Instante de generación de la respuesta."
          },
          "licencia": {
            "type": "string",
            "example": "CC-BY-4.0"
          },
          "atribucion": {
            "type": "string",
            "example": "crisiscolombia.org + fuentes originales"
          },
          "consulta": {
            "type": "object",
            "description": "Eco de los parámetros validados que se aplicaron.",
            "additionalProperties": true
          }
        }
      },
      "RespuestaJSON": {
        "type": "object",
        "properties": {
          "meta": {
            "$ref": "#/components/schemas/Meta"
          },
          "datos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Hecho"
            }
          }
        }
      },
      "RasgoGeoJSON": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "Feature"
            ]
          },
          "geometry": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "Point"
                ]
              },
              "coordinates": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "minItems": 2,
                "maxItems": 2,
                "description": "[lon, lat]"
              }
            }
          },
          "properties": {
            "$ref": "#/components/schemas/Hecho",
            "description": "El registro completo sin lat/lon (van en geometry)."
          }
        }
      },
      "ColeccionGeoJSON": {
        "type": "object",
        "description": "FeatureCollection GeoJSON (RFC 7946) con solo los registros geolocalizados. 'meta' es un miembro extranjero informativo.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "FeatureCollection"
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          },
          "features": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RasgoGeoJSON"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Mensaje de error legible."
          }
        },
        "required": [
          "error"
        ]
      }
    }
  }
}
