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.
- En el tipo de cambio, la observación es un valor (
compra/venta) de una fecha y una serie; nace en cada captura. - En la consulta RUC, la observación es el estado de un RUC en una edición del padrón; se materializa cuando se responde y es única por RUC y edición: la prueba de lo que se respondió.
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):
- 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. - Días después la fuente corrige ese valor. El servicio no edita nada:
crea una observación nueva (
venta: "3.355") que supersede aaaaa…, en una revisión posterior (digamos la 810), y abre una alerta. - 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=731devuelve exactamente lo que la API le respondió aquel día —venta: "3.350", la misma observación, byte a byte (por eso viaja conETagyCache-Control: immutable). - Y la misma consulta sin
as_of_revision— o/v1/observations/aaaa…— muestra la corrección posterior:superseded_porapunta 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:
supersededes relativo a la revisión consultada:falsesignifica "a esa revisión, este era el valor vigente". En un ítem de/v1/ratessiempre esfalsepor construcción.superseded_pormira el presente: si el dataset actual ya corrigió esa observación, aquí aparece el UUID de la sucesora.
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:
- La aplicación no puede editar. Los roles de base de datos del
servicio no tienen privilegios
UPDATEniDELETEsobre 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. - 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.
- 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
- El alcance de cada control de integridad está declarado arriba: el cotejo no ve un error común a ambas fuentes; la confiabilidad no ve un error del padrón mismo.
- El alcance exacto de "inmutable" está declarado capa por capa.
- Una corrección de la fuente posterior a la última consulta del día solo se detecta de forma indirecta (por cotejo posterior, o por la transición de la edición siguiente): las fuentes no exponen su propio histórico.
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. |