---
module: recon.atm-cycle-history
title: ATM Cycle History
tab: Claude
order: 40
audience: dev
---

Terse act-without-guessing reference. Owner: **hiveops-recon** (port 8091, NGINX `/api/recon/`, prod external 8013). DB `hiveiq_recon`.

## Endpoints (all base `/api/recon`)

| Method + path | Role | Backend | Source |
|---|---|---|---|
| GET `/atm/{atmId}/cycles?page=&size=20` | any auth (inst-scoped) | `CycleController.list` | `reconciliation_cycles` |
| GET `/atm/{atmId}/cycles/current` | any auth (inst-scoped) | `CycleController.current` | `reconciliation_cycles` |
| GET `/cycles/{id}` | any auth (inst-scoped) | `CycleController.get` | `reconciliation_cycles` |
| GET `/cycles/{id}/startup-snapshots` | any auth (inst-scoped) | `CycleController.startupSnapshots` | live journal STARTUP (not stored) |
| POST `/atm/{atmId}/sync` | MSP_ADMIN/BCOS_ADMIN | `CycleController.sync` | journal `/api/journal/transactions` |
| GET `/atm/{atmId}/cassettes` | any auth | `AtmCassetteController.getConfig` | `atm_cassette_configs` |
| PUT `/atm/{atmId}/cassettes` | MSP_ADMIN/BCOS_ADMIN | `AtmCassetteController.upsertConfig` | `atm_cassette_configs` |
| POST `/atm/{atmId}/cassettes/sync` | MSP_ADMIN/BCOS_ADMIN | `AtmCassetteController.syncFromIncident` | incident `/api/atms/{id}/properties` |
| GET `/replenishments/{id}` | any auth | `ReplenishmentController.get` | `replenishment_sessions` + `replenishment_cassette_entries` |
| GET `/atm/{atmId}/summary` | any auth | `SummaryController` | `atm_cash_positions` (`journalFormat`) |

`atmId` is the **string** device id. Cassette sync needs incident's numeric id (recon maps it internally via `GET /api/atms`).

## Tables (`hiveiq_recon`)

`reconciliation_cycles` · `replenishment_sessions` · `replenishment_cassette_entries` · `atm_cassette_configs` · `atm_cash_positions` · `device_summary`

## Kafka (recon = consumer-only, no producers)

| Topic | Consumer / group |
|---|---|
| `hiveops.device.events` | `DeviceEventConsumer` / `hiveops-recon-device-sync` → `device_summary` |
| `journal.replenishments` | `ReplenishmentEventConsumer` / `recon-replenishment-group` |
| `hiveops.journal.transactions.recorded` | `JournalTransactionConsumer` / `recon-transaction-group` |

## Upstream deps (WebClient)

- journal: `services.journal.url` (default `http://hiveops-journal:8087`) — `GET /api/journal/transactions`
- incident: `services.incident.url` (default `http://hiveiq-incident:8081`) — `GET /api/atms`, `GET /api/atms/{id}/properties`

## Gotchas

- Reads are **institution-scoped** via `InstitutionContext.resolveScope()`; null scope = all. Missing cycles for a user is usually scope, not data loss.
- ↻ Sync Journal is disabled in UI unless a **current open cycle** exists.
- Startup snapshots are fetched **live from journal every call** — no table; empty means no journal STARTUP txns in `[cycleStart, cycleEnd]`.
- "Sync from Device" (cassettes) reads **incident**, NOT devices, NOT journal. Empty result ⇒ incident down or ATM not in incident.
- `journalFormat` (on `atm_cash_positions`) drives cassette parsing; wrong format ⇒ wrong cassette math.
- `device_summary` has no reconciler — stale if `hiveops.device.events` stops.
- Variance: `expected = opening + added − removed − withdrawals + deposits − reversals`; `variance = actual − expected`; status BALANCED iff variance 0.

## Do NOT

- Do NOT expect a `startup_snapshots` / snapshot table — it does not exist.
- Do NOT route cassette sync to hiveops-devices — it goes to hiveops-incident.
- Do NOT assume read GETs are role-annotated — they are not; only writes require MSP_ADMIN/BCOS_ADMIN.
- Do NOT treat a 403 on Sync/Save as a bug for non-admin users — it is by design.
- Do NOT look for a `DB_URL` var — recon prod uses `DB_HOST`/`DB_PORT`/`DB_NAME`.
- Do NOT expect recon to publish Kafka events — it is consumer-only.

Cross-links: [platform.service-ownership], [platform.kafka], [platform.data-architecture], [technical.recon].
