EKIA Data Service

El modelo de evidencia

Esta es la página que explica qué hace distinto a este servicio: no sirve números ni estados, sirve observaciones — con su evidencia, su historia y una línea de tiempo consultable. El modelo es uno para todos los dominios; los ejemplos alternan entre el tipo de cambio y la consulta RUC, y cada campo con estados viene con un ejemplo por valor posible.

El problema

Un tipo de cambio alimenta asientos contables, facturas y declaraciones; el estado de un RUC decide si una factura se acepta. Años después, un auditor pregunta: ¿de dónde salió este dato? Si la API que lo sirvió solo entregaba el valor del día, no hay respuesta: el dato pudo cambiar, la fuente no expone histórico de sus correcciones y el rastro se perdió. Este servicio está construido para que esa pregunta siempre tenga respuesta.

La observación: inmutable por diseño

Cada vez que el servicio observa una fuente oficial, crea una observación: qué publicó la fuente, para qué fecha, de qué fuente y con qué consulta exacta (identidad_consulta), en qué instante (capturado_en), con qué versión del parser, y con qué bytes crudos de respaldo (la evidencia y, en el tipo de cambio, el fragmento exacto del payload del que se parseó el valor).

Ninguno de esos campos se edita jamás. Si la fuente corrige un valor, el servicio no reescribe la observación: crea una nueva que la supersede y lo registra como evento. La observación corregida queda consultable para siempre en /v1/observations/{id}, con su cadena completa de ancestros y sucesoras.

El campo fuente: de dónde salió

fuente qué significa
sunat_txt Capturada directamente del tipoCambio.txt de SUNAT (serie publicado). La evidencia son los bytes exactos que SUNAT respondió.
bcrp Capturada de las series estadísticas del BCRP (serie cierre) por un colector remoto autorizado: el paquete viaja sellado con su hash y el servicio guarda los bytes exactos de la fuente.
sunat_padron_reducido El ZIP diario del padrón reducido de SUNAT, archivado completo (evidencia de tipo: artefacto); toda observación del dominio RUC sale de una edición de ese archivo.
importado Vía excepcional para datos que una fuente solo distribuye como archivo (el archivo queda como evidencia). Siempre subordinada a la observación directa: jamás la reemplaza.

La revisión: la línea de tiempo del registro

Toda escritura al registro — una captura nueva, una recaptura idéntica, una corrección, la publicación de una edición del padrón — avanza una revisión global, común a todos los dominios. La revisión N es el estado del registro tras el commit número N. Toda respuesta declara en dataset_revision con qué revisión respondió, y as_of_revision=N congela cualquier consulta en ella: la respuesta es siempre la misma, byte a byte.

Nota fechada (2026-09-01). Entre el 2026-08-20 y el 2026-08-31, 435 revisiones — en 22 tandas; la última, 3244–3444 — no traen ningún cambio de dataset: son un artefacto del incidente 2026-08-31 (la suite de pruebas del servicio escribía en la base productiva). Nada se borra: el dataset a una de esas revisiones es idéntico al de la revisión real anterior. Las revisiones de la vigilancia nocturna tampoco cambian el dataset: eso es diseño, no incidente.

Ejemplo ilustrativo: el asiento anclado a una revisión

El caso completo de por qué guardar dataset_revision (escenario ilustrativo — el registro real aún no tiene correcciones):

  1. El 26/08 su sistema contable toma la venta del día para un asiento: la API responde venta: "3.350", observacion_id: aaaa…, dataset_revision: 731. Su sistema guarda los tres.
  2. Días después la fuente corrige ese valor. El servicio no edita nada: crea una observación nueva (venta: "3.355") que supersede a aaaa…, en una revisión posterior (digamos la 810), y abre una alerta.
  3. Un auditor cuestiona el asiento. Su respaldo es exacto y reproducible: GET /v1/rates?currency=USD&series=publicado&from=2026-08-26&to=2026-08-26&as_of_revision=731 devuelve exactamente lo que la API le respondió aquel día — venta: "3.350", la misma observación, byte a byte (por eso viaja con ETag y Cache-Control: immutable).
  4. Y la misma consulta sin as_of_revision — o /v1/observations/aaaa… — muestra la corrección posterior: superseded_por apunta a la observación nueva. Usted decide si el asiento se ajusta; el servicio le da ambas verdades con fecha y revisión.

Ejemplo real: un auditor en 2028 reconstruye el 1 de septiembre de 2026

Paso 1: mapear el instante a una revisión.

curl -s -H "Authorization: Bearer $EKIA_TOKEN" \
  'https://api.eurekia.pe/v1/revisions?at=2026-09-01T23:59:59-05:00'

Respuesta real (2026-09-28, revisión 4414): al cierre del 01/09/2026 en Lima regía la revisión 3479.

{
  "revision": 3479,
  "ts_commit": "2026-09-02T02:00:19.176263Z",
  "dataset_revision": 4414,
  "request_id": "8e783a05-c7a4-43df-81a1-a7c923e2f27f",
  "schema_version": "1.0"
}

Paso 2: consultar anclado a esa revisión. En el tipo de cambio, /v1/rates?…&as_of_revision=3479 devuelve solo las fechas que el registro conocía entonces — ni más ni menos. En el padrón, /v1/ruc/{ruc}?as_of_revision=3479 responde NO_DATA_AT_REVISION: el dominio RUC publicó su primera edición después de esa revisión, y el servicio lo dice en lugar de inventar un estado (la forma en Consulta RUC).

Las marcas: qué le pasó a una observación (tipo de cambio)

marcas lista los eventos que la observación acumuló hasta la revisión consultada. Un escenario por valor:

marca escenario que la produce
capturada La corrida de las 02:30 de Lima encuentra el dato del día por primera vez y crea la observación con su evidencia. Toda observación directa de fuente la lleva.
recaptura_identica Las corridas siguientes del día vuelven a consultar la fuente (siempre consultan) y encuentran el mismo valor canónico: no se crea observación nueva, pero cada re-consulta queda registrada con su propia evidencia. La observación real del 21/08 (f39403ae-…) lleva esta marca por sus recapturas — prueba de vigilancia continua.
sin_cotejo El publicado del día D se capturó antes de que existiera el cierre del día hábil anterior (la fuente lo publica por la tarde): todavía no había contraparte que cotejar. El cotejo corre solo cuando llega el cierre; la marca queda como constancia de que se intentó. Real en la respuesta de Empezar: capturada de madrugada, sin_cotejo hasta la tarde.
supersedida Una captura posterior trajo un valor distinto para la misma fecha/serie: corrección de la fuente. La observación queda en la historia con esta marca y superseded_por apunta a su sucesora (ilustrativo — aún no ha ocurrido en el registro real).
plausibilidad_baja El valor capturado varía frente al día anterior más de lo plausible (umbral configurable; 5 % por defecto). Ilustrativo: la venta pasa de 3.350 a 3.550 de un día a otro. La observación se crea igual — el servicio no descarta datos — pero queda marcada y con alerta abierta: usted decide si la usa.
reproceso La evidencia se guardó pero el parser de entonces no pudo procesarla (formato inesperado); una versión corregida del parser la reprocesó después. La observación nace con el capturado_en original — sin huecos irrecuperables.

Las transiciones: qué le pasó a un RUC (consulta RUC)

El padrón no tiene marcas ni supersesión: SUNAT publica cada día la edición entera, y el servicio la compara con la anterior. Cada diferencia por RUC es una transición (estado_anterior → estado_nuevo, los dos snapshots, campos_cambiados), con la revisión desde la que existe. Una edición corregida por SUNAT no reescribe la anterior: aparece como una transición más, en la edición siguiente. La historia completa de un RUC (/v1/ruc/{ruc}/history) es su estado inicial más sus transiciones — la forma en Consulta RUC.

Los controles de integridad

"Controles de integridad" es el paraguas de todo mecanismo con que el servicio contrasta lo que sirve. Cada dominio tiene el suyo, con su alcance declarado en su página:

dominio control qué detecta qué no detecta
tipo de cambio cotejo entre fuentes (cotejos[], tiene_discrepancia_abierta): el publicado de cada día contra el cierre del último día hábil anterior — el mismo número por construcción. Errores de transmisión, parseo o publicación en cualquiera de las dos cadenas de captura. Un error aguas arriba, común a ambas fuentes (comparten origen: la tabla de la SBS).
consulta RUC confiabilidad por verificación exhaustiva (confiabilidad): la edición reconstruida desde el baseline y las transiciones se contrasta RUC por RUC contra el artefacto original, tras cada publicación. Más dv_valido, el dígito verificador de cada RUC. Cualquier diferencia entre lo que el servicio sirve y lo que SUNAT publicó. Un error en el padrón mismo: RUC no tiene cotejo porque la fuente es única; una corrección de SUNAT llega como transición, no como supersesión.

El cotejo, el día normal — ambas fuentes coinciden (cotejo real del publicado del 25/08/2026 contra el cierre del 24/08/2026):

"cotejos": [
  {
    "cotejo_id": "5c3d6be3-f5cd-4633-b5ee-193523d6791a",
    "contraparte": "3b6f2c09-6897-4730-a84e-1fe72775cbff",
    "resultado": "ok",
    "regla": "publicado_vs_cierre_prev_habil-1.0",
    "revision": 2843,
    "ts": "2026-08-28T22:00:26.894339Z"
  }
],
"tiene_discrepancia_abierta": false

El cotejo, el día discrepante — las fuentes no coinciden (ilustrativo: aún no ha ocurrido en el registro real):

"cotejos": [
  {"contraparte": "1f2e3d4c-…", "resultado": "discrepante",
   "regla": "publicado_vs_cierre_prev_habil-1.0", "revision": 995}
],
"tiene_discrepancia_abierta": true

Ante una discrepancia el servicio no arbitra cuál fuente manda: la expone y deja la decisión al consumidor. La decisión típica: un sistema contable conservador retiene el dato marcado hasta que un cotejo posterior vuelva a ok o un humano decida con constancia. tiene_discrepancia_abierta es el derivado ya calculado: existe un cotejo discrepante posterior al último ok contra la misma contraparte.

La confiabilidad tiene tres valores. verificada significa exactamente esto: la edición reconstruida desde el baseline y las transiciones se contrastó RUC por RUC (18,4 millones de comparaciones) contra el artefacto original. Con el levantamiento exhaustivo tras cada publicación, verificada es el estado normal de toda edición publicada; sin_verificar es la ventana entre publicar y terminar ese levantamiento; fallida es una inconsistencia conocida, con alerta_ref. La respuesta real de Empezar lo muestra.

La supersesión: correcciones sin reescribir el pasado

Dos campos del ítem de tipo de cambio cuentan la historia de vigencia, y no significan lo mismo:

La cadena completa vive en /v1/observations/{id}: cadena.ancestros (las observaciones que esta corrigió) y cadena.sucesoras (las que la corrigieron a ella). Las correcciones son encadenables sin límite: A supersedida por B, B por C — y las tres siguen consultables para siempre.

Sin relleno: nunca otra fecha como si fuera la pedida

El principio es uno: si una fecha no tiene dato, la API lo dice, con la razón del hueco, y jamás entrega otra fecha en lugar de la pedida. Si su sistema recibe un valor, es de la fecha que pidió. La forma cambia por dominio:

dominio fecha sin dato el hueco y su razón
tipo de cambio 404 NO_OBSERVATIONS — ningún ítem, ningún sustituto (forma). /v1/coverage: huecos[].razon ∈ fallo · nd_fuente · sin_dato.
consulta RUC 200 con solo metadatos — cobertura {efectiva: false, razon} y las ediciones vecinas, sin ningún campo de estado; usted elige la vecina y la pide explícitamente (forma). /v1/ruc-coverage: una fila por fecha con razon (no_capturada, no_durable, rechazada, retenida_*, excede_capacidad_v1, no_esperada).

La evidencia: verifique el hash usted mismo

Cada captura guarda los bytes exactos que la fuente respondió y su SHA-256. Un solo contrato para toda evidencia: /v1/evidence/{id} da los metadatos (tipo, sha256, bytes, fuente, fecha, descarga_url) y /v1/evidence/{id}/descarga los bytes — directos para una captura pequeña (tipo: observacion, con ETag = sha256), por redirect firmado para un artefacto grande (tipo: artefacto, el ZIP del padrón). La receta completa, ejecutada real en los dos dominios: Verifique la evidencia en 60 segundos.

¿Qué garantiza «inmutable»?

Tres capas, cada una con su alcance declarado — la honestidad del límite es el argumento de confianza, no su debilidad:

  1. La aplicación no puede editar. Los roles de base de datos del servicio no tienen privilegios UPDATE ni DELETE sobre las tablas del registro: observaciones, eventos, evidencia, transiciones y revisiones solo se insertan. No es una política de código — es un permiso revocado en la base, probado por los tests del servicio.
  2. El administrador de la base de datos podría — y se declara. Un administrador con acceso directo podría alterar filas; ese límite está escrito en la especificación del servicio en vez de escondido. El control compensatorio son las copias de seguridad externas, versionadas y sin borrado desde el servidor, y el archivo de artefactos en un bucket con bloqueo de objetos: contra ellos se detecta una alteración local.
  3. Usted puede verificar sin confiar en nadie. Cada respuesta que su sistema guardó se verifica por hash: la evidencia por su SHA-256, las respuestas ancladas a revisión por su ETag, y el servicio registra el hash de cada body servido — ante un body que usted aporte, puede probarse su identidad e integridad.

Retención: indefinida. Ni las observaciones ni la evidencia se borran. Los artefactos del padrón viven en un archivo inmutable con un bloqueo mínimo renovable de 1095 días (el bloqueo impide borrar incluso a quien tenga la clave; se renueva antes de vencer): el bloqueo es el mínimo, no el plazo de retención.

Tabla de estados completa

campo valores dónde aparece
fuente sunat_txt · bcrp · sunat_padron_reducido · importado Ítems de tasas, /v1/observations/{id}, metadatos de /v1/evidence/{id}
marcas capturada · recaptura_identica · sin_cotejo · supersedida · plausibilidad_baja · reproceso Ítems de tasas; eventos completos en /v1/observations/{id}
cotejos[].resultado ok · discrepante Ítems de tasas y /v1/observations/{id}
tiene_discrepancia_abierta true · false Ítems de tasas — derivado ya calculado de los cotejos
superseded true · false (relativo a la revisión consultada) Ítems de tasas y /v1/observations/{id}
superseded_por UUID de la sucesora · null (mira el dataset actual) Ítems de tasas y /v1/observations/{id}
frescura.estado al_dia · atrasada · adelantada (solo RUC), con dias_atraso y fecha_esperada /v1/rates/latest, /v1/ruc/{ruc}, /v1/ruc-coverage — reglas en cada servicio
huecos[].razon fallo · nd_fuente · sin_dato /v1/coverage — Tipo de cambio
cobertura[].razon (alias causa) no_capturada · no_durable · rechazada · retenida_pendiente · retenida_excepcional_pendiente · retenida_caducada · excede_capacidad_v1 · no_esperada /v1/ruc-coverage y el bloque cobertura de as_of_fecha — Consulta RUC
estado_fila presente · no_interpretable · ausente · ausente_incierto /v1/ruc/{ruc} y los snapshots de la historia — Consulta RUC
confiabilidad verificada · sin_verificar · fallida (con alerta_ref) /v1/ruc/{ruc}; por tramo y global en /v1/ruc/{ruc}/history
dv_valido true · false /v1/ruc/{ruc} y su historia
tipo (evidencia) observacion · artefacto /v1/evidence/{id}
code (errores) tabla completa, por dominio Todo error — Errores y límites

Los límites, declarados

Qué guardar en su sistema

Con cada dato que tome de la API, guarde lo que permite reconstruirlo años después — qué observación lo respaldaba, qué evidencia tenía y qué contenía el registro cuando lo tomó:

dominio guarde
todos observacion_id y dataset_revision.
consulta RUC además edicion.ordinal y edicion.fecha_padron (qué edición del padrón se consultó) y, si va a re-verificar el artefacto, edicion.sha256.