{
  "components": {
    "schemas": {
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "title": "HTTPValidationError",
        "type": "object"
      },
      "ValidationError": {
        "properties": {
          "ctx": {
            "title": "Context",
            "type": "object"
          },
          "input": {
            "title": "Input"
          },
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "title": "Location",
            "type": "array"
          },
          "msg": {
            "title": "Message",
            "type": "string"
          },
          "type": {
            "title": "Error Type",
            "type": "string"
          }
        },
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError",
        "type": "object"
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "description": "Token emitido por Eurekia (uno por instalación/base). Ver https://docs.eurekia.pe/empezar.html",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "contact": {
      "email": "soporte@eurekia.pe",
      "name": "Eurekia — soporte"
    },
    "description": "API de **solo lectura** sobre el registro de observaciones de datos oficiales\nperuanos de Eurekia. Primer dominio: tipo de cambio — hoy la serie\n`publicado` (USD, SUNAT); la serie `cierre` (BCRP) se publicará cuando esté\nen producción.\n\n**Qué la distingue.** El servicio no entrega \"el número\" a secas: entrega\n*observaciones con evidencia*. Cada valor está respaldado por los bytes\ncrudos que la fuente publicó (descargables en `/v1/evidence/{id}` y\nverificables con SHA-256), y todo el registro está versionado por\n*revisiones*: con `as_of_revision` se puede consultar qué contenía el\ndataset en cualquier revisión pasada y obtener siempre la misma respuesta.\nGuía completa: [El modelo de evidencia](https://docs.eurekia.pe/modelo-de-evidencia.html).\n\n**Autenticación.** Token Bearer en el encabezado `Authorization` en todos\nlos endpoints salvo `GET /v1/health/live`. Los tokens los emite Eurekia —\nuno por instalación/base de datos. Cómo obtenerlo:\n[Empezar](https://docs.eurekia.pe/empezar.html).\n\n**Envelope común.** Toda respuesta JSON incluye:\n\n| campo | significado |\n|---|---|\n| `dataset_revision` | Revisión del dataset con la que se respondió. La revisión avanza con **toda** escritura al registro; anclarla (`as_of_revision`) congela la consulta en el tiempo. |\n| `request_id` | Identificador único del request. El servicio registra el SHA-256 de los bytes exactos servidos en cada respuesta autenticada; con el `request_id` puede demostrarse después qué respuesta se sirvió. |\n| `schema_version` | Versión del esquema de respuesta (`1.0`). En `/v1` solo se **agregan** campos; nunca se elimina ni cambia el significado de uno existente. |\n\n**Contrato de errores.** Todo error responde\n`{\"code\", \"message\", \"request_id\", \"retryable\"}` con código estable:\n\n| `code` | HTTP | cuándo ocurre | `retryable` |\n|---|---|---|---|\n| `INVALID_PARAMS` | 400 | Parámetro faltante o mal formado, rango inválido, `as_of_revision` fuera de rango, `page_token` corrupto o de otra consulta. | no |\n| `UNAUTHORIZED` | 401 | Token ausente, inválido, revocado o expirado. | no |\n| `UNSUPPORTED_CURRENCY` | 404 | Moneda que la API sabe que no sirve (p. ej. `EUR`). | no |\n| `UNSUPPORTED_SERIES` | 404 | Serie distinta de `publicado` y `cierre`. | no |\n| `NO_OBSERVATIONS` | 404 | Consulta válida pero sin observaciones en el rango pedido (distinto de moneda no soportada). | no |\n| `NOT_FOUND` | 404 | Observación o evidencia inexistente para el id pedido. | no |\n| `RATE_LIMITED` | 429 | Límite por token excedido (60/min o 5 000/día). Respeta el encabezado `Retry-After`. | sí |\n| `INTERNAL` | 500 | Error interno; el detalle nunca viaja en el body. | sí |\n\n**Límites.** 60 requests/min y 5 000/día por token. `/v1/rates` pagina en\nmáximo 366 ítems y el `next_page_token` fija la revisión de la primera\npágina: recorrer todas las páginas devuelve un snapshot consistente del\ndataset. Rango máximo de consulta: 5 años.\n\nGuías: [Empezar](https://docs.eurekia.pe/empezar.html) ·\n[El modelo de evidencia](https://docs.eurekia.pe/modelo-de-evidencia.html) ·\n[Errores y límites](https://docs.eurekia.pe/errores-y-limites.html) ·\n[Cobertura y frescura](https://docs.eurekia.pe/cobertura-y-frescura.html)\n",
    "title": "EKIA Data Service — API de Datos Oficiales Peruanos",
    "version": "1.0"
  },
  "openapi": "3.1.0",
  "paths": {
    "/v1/alerts": {
      "get": {
        "description": "Alertas del servicio con su ciclo de vida (`abierta` → `reconocida` →\n`resuelta`). La misma condición no se duplica: actualiza el incidente\nabierto (contador y último timestamp). Tipos: `fuente_no_disponible`,\n`invariante_fallida`, `discrepancia`, `plausibilidad_baja`,\n`cambio_formato`, `tunel_caido`, `backfill_incompleto`, `disco`,\n`reloj_fuera_de_tolerancia`.\n\nUn consumidor que quiera reaccionar (p. ej. Odoo con actividades) consulta\neste endpoint; el servicio no empuja notificaciones en v1.",
        "operationId": "alerts_v1_alerts_get",
        "parameters": [
          {
            "description": "`true` (default): solo alertas abiertas o reconocidas. `false`: todas.",
            "in": "query",
            "name": "open",
            "required": false,
            "schema": {
              "default": true,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "dataset_revision": 711,
                  "items": [],
                  "request_id": "481da5a2-d66a-4b59-b943-0315ff2aa0b3",
                  "schema_version": "1.0",
                  "total": 0
                },
                "schema": {}
              }
            },
            "description": "Respuesta real (2026-08-26): sin alertas abiertas."
          }
        },
        "summary": "Incidentes operativos del servicio",
        "tags": [
          "Alertas"
        ]
      }
    },
    "/v1/annotations": {
      "get": {
        "description": "Registro de valores anotados por un operador humano, con actor,\njustificación obligatoria y revisión. Las anotaciones **jamás** forman\nparte de la vigencia de una serie oficial ni reemplazan una observación\ndirecta de fuente: son un registro paralelo, visible aquí y vía\n`include_annotations=true` en `/v1/rates`. El consumidor que quiera aplicar\ncriterio humano lo hace en su propio sistema.",
        "operationId": "annotations_v1_annotations_get",
        "parameters": [
          {
            "description": "Filtro opcional por moneda.",
            "in": "query",
            "name": "currency",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filtro opcional por serie.",
            "in": "query",
            "name": "series",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filtro opcional: fecha inicial inclusiva.",
            "in": "query",
            "name": "from",
            "required": false,
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "Filtro opcional: fecha final inclusiva.",
            "in": "query",
            "name": "to",
            "required": false,
            "schema": {
              "format": "date",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "dataset_revision": 711,
                  "items": [],
                  "request_id": "00000000-0000-0000-0000-000000000000",
                  "schema_version": "1.0",
                  "total": 0
                },
                "schema": {}
              }
            },
            "description": "Sin anotaciones registradas a la fecha."
          }
        },
        "summary": "Anotaciones humanas (separadas de las series oficiales)",
        "tags": [
          "Observaciones y evidencia"
        ]
      }
    },
    "/v1/coverage": {
      "get": {
        "description": "Para cada serie con datos: primera y última `fecha_serie` vigente, total de\nfechas y la lista de huecos con su razón. Criterio de hueco: días hábiles\n(lunes a viernes menos feriados del calendario formal) entre la primera y la\núltima fecha.\n\n| razón del hueco | significado |\n|---|---|\n| `sin_dato` | Ninguna corrida obtuvo dato para esa fecha y no hay causa registrada. |\n| `fallo` | La fuente falló ese día (hay evento `fuente_no_disponible` o `invariante_fallida` con esa fecha esperada). |\n| `nd_fuente` | La propia fuente declara no tener dato para esa fecha (manifiesto de backfill). |",
        "operationId": "coverage_v1_coverage_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "dataset_revision": 711,
                  "request_id": "1424907f-1705-45fd-b7f9-296ce6a131d1",
                  "schema_version": "1.0",
                  "series": [
                    {
                      "criterio_huecos": "dias habiles lun-vie menos feriados, entre primera y ultima",
                      "huecos": [],
                      "moneda": "USD",
                      "primera": "2026-08-21",
                      "serie": "publicado",
                      "total_fechas": 6,
                      "ultima": "2026-08-26"
                    }
                  ]
                },
                "schema": {}
              }
            },
            "description": "Respuesta real (2026-08-26): la serie publicado USD cubre desde el 2026-08-21 (inicio del servicio) sin huecos."
          }
        },
        "summary": "Rango cubierto y huecos por serie",
        "tags": [
          "Cobertura"
        ]
      }
    },
    "/v1/evidence/{ev_id}": {
      "get": {
        "description": "Devuelve los **bytes exactos** que la fuente respondió en la captura (body\nHTTP descomprimido, antes de decodificar charset), con su `Content-Type`\noriginal. Encabezados: `X-Sha256` (SHA-256 de esos bytes), `ETag` (el mismo\nhash), `X-Capturado-En` y `X-Identidad-Consulta` (qué se consultó).\n\nLa evidencia es inmutable y cacheable indefinidamente. Verificación\nejecutable con `sha256sum`:\n[El modelo de evidencia](https://docs.eurekia.pe/modelo-de-evidencia.html).",
        "operationId": "evidence_v1_evidence__ev_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "ev_id",
            "required": true,
            "schema": {
              "title": "Ev Id",
              "type": "string"
            }
          },
          {
            "description": "UUID de la evidencia (`evidencia_id` de una observación o evento).",
            "in": "path",
            "name": "ev_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              },
              "text/plain": {
                "example": "21/08/2026|3.351|3.361|\n"
              }
            },
            "description": "Respuesta real (2026-08-26): los 24 bytes del tipoCambio.txt de SUNAT capturado el 21/08/2026. `X-Sha256: 40dad22e26fe688bd700781111a10212e3725ae1bf9c14df92de36787f3d5a59`."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          }
        },
        "summary": "Bytes crudos de una captura (verificables por SHA-256)",
        "tags": [
          "Observaciones y evidencia"
        ]
      }
    },
    "/v1/health/freshness": {
      "get": {
        "description": "Por cada serie con datos: última `fecha_serie`, fecha esperada a hoy (zona\nLima) y edad en días hábiles. Además: conteo de alertas abiertas y estado\ndel reloj del servidor (`ntp`: `sincronizado`, `no_sincronizado` o\n`no_verificado`) — el timestamp de las revisiones depende de ese reloj.",
        "operationId": "freshness_v1_health_freshness_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "alertas_abiertas": 0,
                  "dataset_revision": 711,
                  "hoy_lima": "2026-08-26",
                  "ntp": "sincronizado",
                  "request_id": "481da5a2-d66a-4b59-b943-0315ff2aa0b3",
                  "schema_version": "1.0",
                  "series": [
                    {
                      "edad_dias_habiles": 0,
                      "fecha_esperada": "2026-08-26",
                      "moneda": "USD",
                      "serie": "publicado",
                      "ultima_fecha_serie": "2026-08-26"
                    }
                  ]
                },
                "schema": {}
              }
            },
            "description": "Respuesta real (2026-08-26): al día, sin alertas, reloj sincronizado."
          }
        },
        "summary": "Frescura por serie, alertas abiertas y estado NTP",
        "tags": [
          "Salud"
        ]
      }
    },
    "/v1/health/live": {
      "get": {
        "description": "Único endpoint sin autenticación. Responde `{\"status\": \"alive\"}` si el proceso está vivo; no toca la base de datos.",
        "operationId": "live_v1_health_live_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "status": "alive"
                },
                "schema": {}
              }
            },
            "description": "El proceso está vivo."
          }
        },
        "security": [],
        "summary": "Liveness (público, sin autenticación)",
        "tags": [
          "Salud"
        ]
      }
    },
    "/v1/health/ready": {
      "get": {
        "description": "Responde `{\"status\": \"ready\"}` solo si la base de datos contesta.",
        "operationId": "ready_v1_health_ready_get",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "status": "ready"
                },
                "schema": {}
              }
            },
            "description": "Listo para servir."
          }
        },
        "summary": "Readiness (verifica la base de datos)",
        "tags": [
          "Salud"
        ]
      }
    },
    "/v1/observations/{obs_id}": {
      "get": {
        "description": "Devuelve la observación completa (valores canónicos y crudos, fuente,\n`identidad_consulta`, `fragmento` del payload que la produjo,\n`version_parser`, `revision_creada`), su cadena de supersesión (`ancestros`\ny `sucesoras`), sus eventos y sus cotejos.\n\nUna observación es **inmutable**: ningún campo se corrige jamás. Una\ncorrección es una observación nueva que *supersede* a la anterior\n(`superseded_por`), registrada como evento con su revisión — la historia\nqueda completa y consultable. Por eso esta representación es `no-store`:\nla cadena puede crecer.\n\n`evidencia_id` apunta a los bytes crudos en `/v1/evidence/{id}`;\n`fragmento` es la porción exacta del payload de la que se parseó el valor.",
        "operationId": "observation_v1_observations__obs_id__get",
        "parameters": [
          {
            "in": "path",
            "name": "obs_id",
            "required": true,
            "schema": {
              "title": "Obs Id",
              "type": "string"
            }
          },
          {
            "description": "UUID de la observación (`observacion_id` de cualquier ítem).",
            "in": "path",
            "name": "obs_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "cadena": {
                    "ancestros": [],
                    "sucesoras": []
                  },
                  "cotejos": [],
                  "dataset_revision": 711,
                  "eventos": [
                    {
                      "detalle": {
                        "compra": "3.351",
                        "sha256": "40dad22e26fe688bd700781111a10212e3725ae1bf9c14df92de36787f3d5a59",
                        "venta": "3.361"
                      },
                      "ejecucion_id": "bccdd8ab-6bcb-4953-a593-3ae237315cb3",
                      "evidencia_id": "f4a7a1c6-e1a4-4999-b175-7191e90c77c6",
                      "id": "f37f595b-5145-4df1-99c0-338777bff447",
                      "revision": 217,
                      "tipo": "capturada",
                      "ts": "2026-08-21T17:06:47.867368Z"
                    }
                  ],
                  "observacion": {
                    "capturado_en": "2026-08-21T17:06:47.880642Z",
                    "compra": "3.351",
                    "compra_cruda": "3.351",
                    "ejecucion_id": "bccdd8ab-6bcb-4953-a593-3ae237315cb3",
                    "evidencia_id": "f4a7a1c6-e1a4-4999-b175-7191e90c77c6",
                    "fecha_serie": "2026-08-21",
                    "fragmento": "21/08/2026|3.351|3.361|",
                    "fuente": "sunat_txt",
                    "id": "f39403ae-c387-4045-8091-c901973f8feb",
                    "identidad_consulta": "GET https://www.sunat.gob.pe/a/txt/tipoCambio.txt",
                    "moneda": "USD",
                    "revision_creada": 217,
                    "serie": "publicado",
                    "venta": "3.361",
                    "venta_cruda": "3.361",
                    "version_parser": "sunat_txt-1.0"
                  },
                  "request_id": "e0d54f70-aaeb-403c-9133-e458465ed1b0",
                  "schema_version": "1.0",
                  "superseded": false
                },
                "schema": {}
              }
            },
            "description": "Respuesta real (2026-08-26): la observación del 21/08/2026 (extracto de eventos)."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          }
        },
        "summary": "Una observación con su historia completa",
        "tags": [
          "Observaciones y evidencia"
        ]
      }
    },
    "/v1/rates": {
      "get": {
        "description": "Devuelve las observaciones **vigentes** de la serie pedida entre `from` y\n`to` (ambos inclusivos), en orden cronológico ascendente.\n\nSin `as_of_revision`, responde a la revisión actual del dataset\n(`Cache-Control: no-store`). Con `as_of_revision`, responde exactamente lo\nque el dataset contenía a esa revisión — respuesta inmutable, cacheable\nindefinidamente, con `ETag` = SHA-256 del body.\n\nPaginación: máximo 366 ítems por página. `next_page_token` fija la revisión\nde la primera página, de modo que recorrer todas las páginas entrega un\nsnapshot consistente aunque el dataset avance entre páginas. `total` es el\ntotal del rango a esa revisión.\n\nCon `include_annotations=true` agrega el campo `annotations`: anotaciones\nhumanas del rango (registro separado — jamás forman parte de la vigencia de\nlas series oficiales; ver `/v1/annotations`).\n\n**Campos del ítem** (los mismos en `/v1/rates` y `/v1/rates/latest`):\n\n| campo | significado |\n|---|---|\n| `fecha_serie` | Fecha a la que corresponde el valor según la fuente (no la fecha de captura). |\n| `compra` / `venta` | Valor canónico como **texto** de exactamente 3 decimales (p. ej. `\"3.351\"`), redondeo half-up (regla SBS). Nunca pasan por punto flotante en el servicio. |\n| `fuente` | `sunat_txt` (tipoCambio.txt de SUNAT), `bcrp` (series de cierre) o `importado` (importación humana; solo llena fechas sin observación directa, jamás las reemplaza). |\n| `cotejos` | Verificaciones cruzadas contra observaciones de otra fuente: cada una con `contraparte`, `resultado` (`ok`/`discrepante`), `regla` y `revision`. Lista vacía = aún sin contraparte que cotejar. |\n| `tiene_discrepancia_abierta` | `true` si existe un cotejo discrepante posterior al último `ok` contra la misma contraparte. El servicio **no arbitra** cuál fuente manda: expone la discrepancia. |\n| `marcas` | Tipos de evento registrados sobre la observación (`capturada`, `recaptura_identica`, `plausibilidad_baja`, …). |\n| `observacion_id` | UUID de la observación inmutable que respalda el valor. Guardarlo junto al dato da trazabilidad de punta a punta: `/v1/observations/{id}` devuelve su evidencia y su historia completa. |\n| `capturado_en` | Instante UTC en que el servicio observó la fuente. |\n| `superseded` | Siempre `false` en un ítem servido: a la revisión consultada, el ítem es el vigente por construcción. |\n| `superseded_por` | UUID de la observación que la corrigió **a la revisión más reciente** del dataset (o `null`). Útil al consultar revisiones históricas: revela si hoy ya existe una corrección. |",
        "operationId": "rates_v1_rates_get",
        "parameters": [
          {
            "description": "Moneda. Hoy solo `USD`; otra moneda responde `UNSUPPORTED_CURRENCY`.",
            "in": "query",
            "name": "currency",
            "required": true,
            "schema": {
              "enum": [
                "USD"
              ],
              "type": "string"
            }
          },
          {
            "description": "Serie oficial: `publicado` (SUNAT) o `cierre` (BCRP). La serie `cierre` aún no tiene datos publicados — ver Cobertura.",
            "in": "query",
            "name": "series",
            "required": true,
            "schema": {
              "enum": [
                "publicado",
                "cierre"
              ],
              "type": "string"
            }
          },
          {
            "description": "Fecha inicial `YYYY-MM-DD`, inclusiva.",
            "in": "query",
            "name": "from",
            "required": true,
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "Fecha final `YYYY-MM-DD`, inclusiva. Rango máximo: 5 años.",
            "in": "query",
            "name": "to",
            "required": true,
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "Revisión del dataset a la que consultar (entero ≥ 1). Omitido = revisión actual.",
            "in": "query",
            "name": "as_of_revision",
            "required": false,
            "schema": {
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Token opaco de la página anterior (`next_page_token`). Lleva embebida la revisión y la consulta a la que pertenece.",
            "in": "query",
            "name": "page_token",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Si es `true`, incluye las anotaciones humanas del rango.",
            "in": "query",
            "name": "include_annotations",
            "required": false,
            "schema": {
              "default": false,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "dataset_revision": 711,
                  "items": [
                    {
                      "capturado_en": "2026-08-21T17:06:47.880642Z",
                      "compra": "3.351",
                      "cotejos": [],
                      "fecha_serie": "2026-08-21",
                      "fuente": "sunat_txt",
                      "marcas": [
                        "capturada",
                        "recaptura_identica"
                      ],
                      "observacion_id": "f39403ae-c387-4045-8091-c901973f8feb",
                      "superseded": false,
                      "tiene_discrepancia_abierta": false,
                      "venta": "3.361"
                    },
                    {
                      "capturado_en": "2026-08-22T07:30:21.949415Z",
                      "compra": "3.343",
                      "cotejos": [],
                      "fecha_serie": "2026-08-22",
                      "fuente": "sunat_txt",
                      "marcas": [
                        "capturada",
                        "recaptura_identica"
                      ],
                      "observacion_id": "17c48571-7e9c-49df-a93e-6b294bdb3fed",
                      "superseded": false,
                      "tiene_discrepancia_abierta": false,
                      "venta": "3.352"
                    }
                  ],
                  "request_id": "c93c61b1-39ff-4a5b-8ba9-16eab2be4ce1",
                  "schema_version": "1.0",
                  "total": 2
                },
                "schema": {}
              }
            },
            "description": "Respuesta real (2026-08-26): serie publicado USD, 21 al 22 de agosto de 2026."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "code": "UNSUPPORTED_CURRENCY",
                  "message": "currency 'EUR' is not served by this API",
                  "request_id": "eb0c1f26-0c82-409e-a282-3e48a80db89f",
                  "retryable": false
                }
              }
            },
            "description": "`UNSUPPORTED_CURRENCY`, `UNSUPPORTED_SERIES` o `NO_OBSERVATIONS` (consulta válida sin datos en el rango)."
          }
        },
        "summary": "Tasas vigentes en un rango de fechas",
        "tags": [
          "Tasas"
        ]
      }
    },
    "/v1/rates/latest": {
      "get": {
        "description": "Devuelve la observación vigente más reciente de la serie, más el bloque\n`frescura`: qué fecha se esperaba tener a hoy (zona `America/Lima`) y si el\nregistro está al día.\n\n| campo de `frescura` | significado |\n|---|---|\n| `estado` | `al_dia` o `atrasada`, comparando `fecha_serie` contra `fecha_esperada`. |\n| `n_dias_habiles` | Días hábiles de atraso (0 si está al día). |\n| `fecha_esperada` | Para `publicado`: hoy en Lima. Para `cierre`: el último día hábil ≤ hoy — un domingo con dato del viernes es `al_dia`. |\n| `hoy_lima` / `zona` | La fecha de referencia y su zona horaria. |\n\n**Campos del ítem** (los mismos en `/v1/rates` y `/v1/rates/latest`):\n\n| campo | significado |\n|---|---|\n| `fecha_serie` | Fecha a la que corresponde el valor según la fuente (no la fecha de captura). |\n| `compra` / `venta` | Valor canónico como **texto** de exactamente 3 decimales (p. ej. `\"3.351\"`), redondeo half-up (regla SBS). Nunca pasan por punto flotante en el servicio. |\n| `fuente` | `sunat_txt` (tipoCambio.txt de SUNAT), `bcrp` (series de cierre) o `importado` (importación humana; solo llena fechas sin observación directa, jamás las reemplaza). |\n| `cotejos` | Verificaciones cruzadas contra observaciones de otra fuente: cada una con `contraparte`, `resultado` (`ok`/`discrepante`), `regla` y `revision`. Lista vacía = aún sin contraparte que cotejar. |\n| `tiene_discrepancia_abierta` | `true` si existe un cotejo discrepante posterior al último `ok` contra la misma contraparte. El servicio **no arbitra** cuál fuente manda: expone la discrepancia. |\n| `marcas` | Tipos de evento registrados sobre la observación (`capturada`, `recaptura_identica`, `plausibilidad_baja`, …). |\n| `observacion_id` | UUID de la observación inmutable que respalda el valor. Guardarlo junto al dato da trazabilidad de punta a punta: `/v1/observations/{id}` devuelve su evidencia y su historia completa. |\n| `capturado_en` | Instante UTC en que el servicio observó la fuente. |\n| `superseded` | Siempre `false` en un ítem servido: a la revisión consultada, el ítem es el vigente por construcción. |\n| `superseded_por` | UUID de la observación que la corrigió **a la revisión más reciente** del dataset (o `null`). Útil al consultar revisiones históricas: revela si hoy ya existe una corrección. |",
        "operationId": "latest_v1_rates_latest_get",
        "parameters": [
          {
            "description": "Moneda. Hoy solo `USD`; otra moneda responde `UNSUPPORTED_CURRENCY`.",
            "in": "query",
            "name": "currency",
            "required": true,
            "schema": {
              "enum": [
                "USD"
              ],
              "type": "string"
            }
          },
          {
            "description": "Serie oficial: `publicado` (SUNAT) o `cierre` (BCRP). La serie `cierre` aún no tiene datos publicados — ver Cobertura.",
            "in": "query",
            "name": "series",
            "required": true,
            "schema": {
              "enum": [
                "publicado",
                "cierre"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "capturado_en": "2026-08-26T07:30:01.251782Z",
                  "compra": "3.339",
                  "cotejos": [],
                  "dataset_revision": 711,
                  "fecha_serie": "2026-08-26",
                  "frescura": {
                    "estado": "al_dia",
                    "fecha_esperada": "2026-08-26",
                    "hoy_lima": "2026-08-26",
                    "n_dias_habiles": 0,
                    "zona": "America/Lima"
                  },
                  "fuente": "sunat_txt",
                  "marcas": [
                    "capturada",
                    "recaptura_identica"
                  ],
                  "observacion_id": "27fdd039-36e7-4336-935f-f54e3a8f504e",
                  "request_id": "2d5a8567-d2d8-4a02-93cc-e5b769a25e39",
                  "schema_version": "1.0",
                  "superseded": false,
                  "tiene_discrepancia_abierta": false,
                  "venta": "3.350"
                },
                "schema": {}
              }
            },
            "description": "Respuesta real (2026-08-26)."
          }
        },
        "summary": "Tasa vigente más reciente, con frescura",
        "tags": [
          "Tasas"
        ]
      }
    },
    "/v1/revisions": {
      "get": {
        "description": "Resuelve un timestamp contra el historial de revisiones: devuelve la última\nrevisión cuyo commit es ≤ `at`. Es el puente instante → revisión: permite\npreguntar \"¿qué se sabía el día X?\" y luego consultar `/v1/rates` con ese\n`as_of_revision`.\n\n`at` debe ser ISO-8601 **con zona horaria** (p. ej.\n`2026-08-21T23:59:59-05:00`). Si `at` es anterior a la primera revisión,\nresponde `NO_OBSERVATIONS`.",
        "operationId": "revisions_v1_revisions_get",
        "parameters": [
          {
            "description": "Instante ISO-8601 con offset de zona.",
            "in": "query",
            "name": "at",
            "required": true,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "dataset_revision": 711,
                  "request_id": "57d47928-1818-4157-b075-5ea23454a4d8",
                  "revision": 658,
                  "schema_version": "1.0",
                  "ts_commit": "2026-08-22T03:17:11.877559Z"
                },
                "schema": {}
              }
            },
            "description": "Respuesta real (2026-08-26): al terminar el 21/08/2026 en Lima regía la revisión 658."
          }
        },
        "summary": "Qué revisión regía en un instante dado",
        "tags": [
          "Cobertura"
        ]
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "servers": [
    {
      "url": "https://api.eurekia.pe"
    }
  ],
  "tags": [
    {
      "description": "Valores vigentes del tipo de cambio por fecha de serie, a la revisión actual o a una revisión histórica.",
      "name": "Tasas"
    },
    {
      "description": "El respaldo de cada valor: la observación inmutable, su cadena de correcciones, los bytes crudos de la fuente y las anotaciones humanas.",
      "name": "Observaciones y evidencia"
    },
    {
      "description": "Qué fechas existen en el registro, qué huecos hay y cómo mapear un instante del pasado a una revisión del dataset.",
      "name": "Cobertura"
    },
    {
      "description": "Incidentes operativos del servicio (fuente no disponible, discrepancia, etc.) con su ciclo de vida.",
      "name": "Alertas"
    },
    {
      "description": "Liveness, readiness y frescura por serie. Solo /v1/health/live es público.",
      "name": "Salud"
    }
  ]
}
