Appearance
Reading Management — API Analysis
The reading tab of a gauge works with reading entries: everything read on one gauge at one moment — each tariff, direction and channel — together with the note and supporting documents. A manual entry is a stored readingEntry plus its reading rows; a remote entry is the group of remote rows sharing a readAt, has no identifier and cannot be changed here. The row-level writes the importer and integrations use are not part of this API.
The project has no shared API-conventions document yet, so the envelopes are stated once here. Success:
json
{
"data": {},
"status": 200,
"message": "OK",
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b21"
}Error:
json
{
"status": 409,
"error": {
"code": "ERR_READING_VALUE_CONFLICT",
"message": "A reading with the same identity and a different value already exists.",
"details": {}
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b21"
}Authentication, tenant resolution and permission failures are answered by the tenant API's shared handling and are not restated per endpoint. Validation failures answer 400 VALIDATION_ERROR with error.details.fieldErrors.
Permission codes: tenant.readings.read (see readings), tenant.readings.write (enter a reading), tenant.readings.delete (edit and delete an entered reading — one gate for both), tenant.readings.override-monotonicity (save a value that breaks the continuity of its series).
📥 GET /v1/reading-entries
Returns one page of a gauge's reading entries, manual or remote, newest first, each with its values per series and the consumption since the previous reading of that series.
Authorization
Requires permission code tenant.readings.read, tenant-scoped.
Request Headers
None beyond the shared API conventions.
Request Body
None.
Request Parameters
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
gauge | Query | UUID | Yes | - | Gauge whose readings are listed. | Must be a gauge of the tenant. | reading.gaugeId |
source | Query | Enum | Yes | - | manual for the manual view, remote for the remote view. | manual or remote from Enum - ReadingSource. | reading.source |
from | Query | Timestamp | No | manual: none; remote: 30 days before to | Start of the window, inclusive. | ISO 8601. | reading.readAt |
to | Query | Timestamp | No | now | End of the window, inclusive. | ISO 8601; from ≤ to; for source = remote a window of at most 366 days. | reading.readAt |
sort | Query | String | No | readAt | Manual view only: column to sort by — readAt or a series key (energy.import.high, energy.import.high.consumption, …). | Ignored for source = remote, which is always by readAt. | Column catalogue → reading series |
order | Query | Enum | No | desc | Manual view only: asc or desc. | - | - |
page | Query | Integer | No | 1 | Manual view only: page number. | ≥ 1. | - |
pageSize | Query | Integer | No | manual 25; remote 100 | Entries per page or batch. | 1–100 (manual), 1–500 (remote). | - |
cursor | Query | String | No | - | Remote view only: opaque position returned as meta.nextCursor. | Keep gauge, source, from, to identical across a walk. | reading.readAt |
Pagination: two modes, chosen by source. The manual view is small (tens of rows per year) and is a plain table: page / pageSize with sorting, as on every other overview. The remote view runs to tens of thousands of rows per gauge and year and is appended continuously, so an offset page would drift while the user reads it: keyset on readAt descending, newest first, loaded batch by batch (Načíst další).
Request Logic
- Reads
readingfor the gauge, source and window, and forsource = manualthe matching activereadingEntryrows with the count of activereadingEntryDocumentrows. - Groups rows into entries by
readAt; the page boundary always falls between two entries, never inside one. For the manual view sorts by the requested column and pages by number; for the remote view orders byreadAtdescending and continues from the cursor. - For each series, reads the nearest earlier row of the same series (outside the page if needed) and derives
consumption = value − previous value;nullfor the first row of a series, for a row withlifecycleState = initial, and forformat = deltaortype = power, where the value itself is already the interval figure.
sql
SELECT r.read_at, r.type, r.direction, r.tariff, r.value, r.unit, r.lifecycle_state,
r.value - LAG(r.value) OVER (
PARTITION BY r.type, r.direction, r.tariff ORDER BY r.read_at
) AS consumption
FROM reading r
WHERE r.gauge_id = @gauge
AND r.source = @source
AND r.read_at BETWEEN @from AND @to
AND (@cursor IS NULL OR r.read_at < @cursorReadAt)
ORDER BY r.read_at DESC;Transactional Operations
N/A — read-only.
✅ Success Response (200)
json
{
"data": [
{
"id": "019247a1-5c3e-7d10-8a2b-1f4c6e8d9a01",
"gaugeId": "018f7a10-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"readAt": "2026-05-04T08:40:00.000Z",
"source": "manual",
"hasNote": true,
"documentCount": 1,
"values": [
{
"type": "energy",
"direction": "import",
"tariff": "high",
"value": 192054,
"unit": "kWh",
"consumption": 6097,
"lifecycleState": "reading"
},
{
"type": "energy",
"direction": "import",
"tariff": "low",
"value": 73787,
"unit": "kWh",
"consumption": 2481,
"lifecycleState": "reading"
}
]
}
],
"meta": {
"page": 1,
"pageSize": 25,
"total": 13,
"nextCursor": null
},
"status": 200,
"message": "OK",
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b31"
}Response Data Mapping
| Response Field | Source / Value |
|---|---|
id | readingEntry.id; null for a remote entry |
gaugeId | reading.gaugeId |
readAt | reading.readAt |
source | reading.source |
hasNote | Derived: readingEntry.note is set; false for a remote entry |
documentCount | Derived: active readingEntryDocument rows of the entry; 0 for a remote entry |
values[].type | reading.type |
values[].direction | reading.direction |
values[].tariff | reading.tariff |
values[].value | reading.value |
values[].unit | reading.unit |
values[].consumption | Derived: difference to the nearest earlier row of the same series, as described in Request Logic |
values[].lifecycleState | reading.lifecycleState |
meta.page, meta.total | Manual view: current page and number of matching entries; null for the remote view |
meta.pageSize | Effective page or batch size |
meta.nextCursor | Remote view: position after the last entry of the batch; null on the last batch and for the manual view |
❌ Error Responses
404 ERR_GAUGE_NOT_FOUND
Thrown when gauge is not a gauge of the tenant.
json
{
"status": 404,
"error": {
"code": "ERR_GAUGE_NOT_FOUND",
"message": "Gauge not found.",
"details": {}
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b31"
}📤 POST /v1/reading-entries/export
Downloads a gauge's manual or remote reading overview as XLSX, with the columns the user currently shows, in their current order, for every entry matching the filter — for the remote view over a chosen range, independent of the period shown on screen.
Authorization
Requires permission code tenant.readings.read, tenant-scoped.
Request Headers
Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, beyond the shared API conventions.
Request Body
json
{
"filters": {
"gauge": "018f7a10-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"source": "remote",
"range": "3m"
},
"columns": ["readAt", "energy.import.high", "energy.import.low", "energy.import.high.consumption", "power.import"],
"ids": null
}| Field | Type | Required | Validation | DB Mapping |
|---|---|---|---|---|
filters | Object | Yes | gauge and source as in the GET /v1/reading-entries query; for source = remote either range or from / to. | As in the list |
filters.range | Enum | No | Remote view only: 7d, 3m, 1y (the last 7 days, 3 months, year before now) or all (every stored reading); when given, from / to are ignored. The manual view always exports every reading. | reading.readAt |
columns | Array of String | Yes | 1–100 unique keys from the readings-manual or readings-remote column catalogue (Enum - TableViewKey), matching filters.source. | Column catalogue → reading series |
ids | Array of UUID | No | 1–5 000 entry ids; only for source = manual. null exports every matching entry. | readingEntry.id |
Request Parameters
None.
Request Logic
- Validates
columnsagainst the catalogue of the overview selected byfilters.source; the catalogue holds the union of the seriestariffSeries(gauge, at)gives over the exported range, so a tariff change in the range adds columns rather than losing rows; a series the gauge never had is rejected. - Row set: every entry matching
filters(or the listedidswithin them), never limited to a page; counted first and refused above the export limit (50 000 entries, a configuration value). - One row per entry, one column per requested key, in the requested order; derived columns (consumption, the conversion to kWh of a gas gauge) carry the same server-computed value as the list.
- Writes nothing.
Transactional Operations
N/A — read-only. Count and read run in one read-only transaction, so the limit check and the file describe the same data.
✅ Success Response (200)
Binary XLSX body; Content-Disposition: attachment; filename="odecty-<gauge>-YYYY-MM-DD.xlsx". Not wrapped in the envelope.
Response Data Mapping
| Response Field | Source / Value |
|---|---|
Column readAt | reading.readAt, in the tenant's time zone |
Column <type>.<direction>.<tariff> | reading.value of that series, with reading.unit in the header |
Column <series>.consumption | Derived, as values[].consumption in the list |
Column <series>.energy (gas volume series) | Derived: the consumption converted to kWh by Calorific Value Management |
Column note (manual only) | readingEntry.note |
❌ Error Responses
400 ERR_READING_EXPORT_UNKNOWN_COLUMN
Thrown when a requested column is not in the overview's catalogue or the gauge has no such series.
json
{
"status": 400,
"error": {
"code": "ERR_READING_EXPORT_UNKNOWN_COLUMN",
"message": "Unknown column.",
"details": { "column": "energy.export.high" }
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b41"
}422 ERR_EXPORT_TOO_LARGE
Thrown when the row set exceeds the export limit.
json
{
"status": 422,
"error": {
"code": "ERR_EXPORT_TOO_LARGE",
"message": "The export exceeds the row limit; narrow the filter.",
"details": { "rows": 61234, "limit": 50000 }
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b41"
}📥 GET /v1/reading-entries/{id}
Returns one manual reading with its values, note and supporting documents — the content of the reading detail. Its change history is read from the audit trail.
Authorization
Requires permission code tenant.readings.read, tenant-scoped.
Request Headers
None beyond the shared API conventions.
Request Body
None.
Request Parameters
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
id | Path | UUID | Yes | - | The reading entry. | An active entry of the tenant. | readingEntry.id |
Request Logic
- Reads the active
readingEntry, itsreadingrows (source = manual, samegaugeIdandreadAt) and its activereadingEntryDocumentrows with each document's name and type. - The history is not part of this response: the screen opens the shared per-record history of Audit Log for the entry and its rows; nothing is duplicated here.
Transactional Operations
N/A — read-only.
✅ Success Response (200)
json
{
"data": {
"id": "019247a1-5c3e-7d10-8a2b-1f4c6e8d9a01",
"gaugeId": "018f7a10-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"readAt": "2026-05-04T08:40:00.000Z",
"note": "Odečteno při kontrole rozvaděče, displej špatně čitelný.",
"values": [
{
"readingId": "019247a1-5c40-7a11-9c2d-3e5f7a9b1c02",
"type": "energy",
"direction": "import",
"tariff": "high",
"value": 192054,
"unit": "kWh"
}
],
"documents": [
{
"documentId": "019247a0-9e1d-7c3b-8f2a-4d6e8a0c2e34",
"name": "odecet-0405.jpg",
"mimeType": "image/jpeg"
}
],
"createdBy": "user:018ed0b3-…",
"createdAt": "2026-05-04T09:41:12.000Z",
"updatedBy": "user:018ed0b3-…",
"updatedAt": "2026-05-06T14:12:40.000Z"
},
"status": 200,
"message": "OK",
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b51"
}Response Data Mapping
| Response Field | Source / Value |
|---|---|
id | readingEntry.id |
gaugeId | readingEntry.gaugeId |
readAt | readingEntry.readAt |
note | readingEntry.note |
values[].readingId | reading.id |
values[].type, direction, tariff, value, unit | The same-named attributes of reading |
documents[].documentId | readingEntryDocument.documentId |
documents[].name, mimeType | The linked document's name and type, from Document Management |
createdBy, createdAt, updatedBy, updatedAt | The same-named attributes of readingEntry |
❌ Error Responses
404 ERR_READING_ENTRY_NOT_FOUND
Thrown when no active entry with this id exists in the tenant.
json
{
"status": 404,
"error": {
"code": "ERR_READING_ENTRY_NOT_FOUND",
"message": "Reading not found.",
"details": {}
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b51"
}📝 POST /v1/reading-entries
Records a manual reading — every series of the gauge read at one moment, with an optional note and supporting documents — in one step.
Authorization
Requires permission code tenant.readings.write, tenant-scoped.
Request Headers
None beyond the shared API conventions.
Request Body
json
{
"gaugeId": "018f7a10-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"readAt": "2026-06-04T08:30:00.000Z",
"values": [
{ "type": "energy", "direction": "import", "tariff": "high", "value": 198142 },
{ "type": "energy", "direction": "import", "tariff": "low", "value": 76142 },
{ "type": "energy", "direction": "export", "tariff": "total", "value": 3410 }
],
"note": "Odečteno při kontrole rozvaděče.",
"documentIds": ["019247a0-9e1d-7c3b-8f2a-4d6e8a0c2e34"]
}| Field | Type | Required | Validation | DB Mapping |
|---|---|---|---|---|
gaugeId | UUID | Yes | A gauge of the tenant with at least one parameter whose readingMethod is manual or both. | readingEntry.gaugeId, reading.gaugeId |
readAt | Timestamp | Yes | Between 1990-01-01 and now; no active entry of the gauge at this moment. | readingEntry.readAt, reading.readAt |
values | Array | Yes | 1–20 items; each series at most once; each series must be one tariffSeries(gauge, readAt) returns for a hand-read parameter of the gauge (GET /v1/gauges/{id}/tariff-series?at=, Gauge Management); every such series must be present, except the feed-in (direction = export) series of a gauge that also reads consumption. | One reading row per item |
values[].type | Enum | Yes | Enum - ReadingType. | reading.type |
values[].direction | Enum | No | import / export; defaults to the gauge channel's direction. | reading.direction |
values[].tariff | Enum | No | Enum - ReadingTariff. | reading.tariff |
values[].value | Number | Yes | Finite. | reading.value |
note | String | No | Max 2 000 characters. | readingEntry.note |
documentIds | Array of UUID | No | 0–10 active documents of the tenant, each at most once. A file uploaded "from computer" is registered as a document first. | readingEntryDocument.documentId |
Request Parameters
None.
Request Logic
- Resolves the gauge through the gauge lookup; rejects when no parameter of it is read by hand.
- Resolves the series with
tariffSeries(gauge, readAt)— the distribution rate or tariff switches valid atreadAt, not today's — and, per parameter, the manual gaugeSourceSetting valid atreadAt; a parameter without one is rejected withERR_SOURCE_SETTING_MISSING. - Writes one
readingEntry, onereadingrow per value (source = manual,unitfrom that source setting,formatper the matrix on the reading page,lifecycleState = reading) and onereadingEntryDocumentrow per document. The value is stored as read; the source coefficient is applied by the consumption computation. - For each cumulative series, compares the value with the nearest earlier and later row of the same series; a value below the earlier or above the later one is rejected with
ERR_CUMULATIVE_VALUE_TOO_LOW, carrying every offending series so the screen can ask for the reason (typo, meter replacement, or the override). The comparison skips a predecessor withlifecycleState = final. - Announces a recalculation of consumption for the affected period and writes the audit entries.
Transactional Operations
Entry, rows and document links commit together or not at all. Two users entering the same gauge and moment at once are separated by the unique key on readingEntry; the second receives ERR_READING_ENTRY_EXISTS.
✅ Success Response (201)
json
{
"data": { "id": "019247a1-7e5a-7b32-8d4e-5f6a8c0d2e03" },
"status": 201,
"message": "Created",
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b61"
}Response Data Mapping
| Response Field | Source / Value |
|---|---|
id | readingEntry.id of the new entry |
❌ Error Responses
422 ERR_MANUAL_READING_NOT_ALLOWED
Thrown when no parameter of the gauge is read by hand (readingMethod of every parameter is remote).
json
{
"status": 422,
"error": {
"code": "ERR_MANUAL_READING_NOT_ALLOWED",
"message": "This gauge does not accept manual readings.",
"details": {}
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b61"
}400 ERR_READING_DATE_OUT_OF_RANGE
Thrown when readAt is before 1990-01-01 or in the future.
json
{
"status": 400,
"error": {
"code": "ERR_READING_DATE_OUT_OF_RANGE",
"message": "The reading date is out of range.",
"details": {}
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b61"
}400 ERR_READING_SERIES_MISSING
Thrown when a required series of the gauge has no value.
json
{
"status": 400,
"error": {
"code": "ERR_READING_SERIES_MISSING",
"message": "A value is required for every series of the gauge.",
"details": { "type": "energy", "direction": "import", "tariff": "low" }
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b61"
}400 ERR_READING_SERIES_UNKNOWN
Thrown when a value names a series tariffSeries(gauge, readAt) does not return for a hand-read parameter at that moment, or names a series twice.
json
{
"status": 400,
"error": {
"code": "ERR_READING_SERIES_UNKNOWN",
"message": "The gauge has no such series.",
"details": { "type": "energy", "direction": "export", "tariff": "high" }
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b61"
}422 ERR_SOURCE_SETTING_MISSING
Thrown when a hand-read parameter of the gauge has no manual source setting (unit) valid at readAt; the setting is completed in Gauge Management.
json
{
"status": 422,
"error": {
"code": "ERR_SOURCE_SETTING_MISSING",
"message": "The parameter has no manual reading unit valid at this moment.",
"details": { "gaugeId": "018f7a10-2c3d-7e4f-8a5b-6c7d8e9f0a1b", "source": "manual", "readAt": "2026-05-04T08:40:00Z" }
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b61"
}409 ERR_READING_ENTRY_EXISTS
Thrown when the gauge already has an active manual reading at readAt.
json
{
"status": 409,
"error": {
"code": "ERR_READING_ENTRY_EXISTS",
"message": "A reading already exists at this moment.",
"details": { "id": "019247a1-5c3e-7d10-8a2b-1f4c6e8d9a01" }
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b61"
}422 ERR_CUMULATIVE_VALUE_TOO_LOW
Thrown when a cumulative value breaks the continuity of its series.
json
{
"status": 422,
"error": {
"code": "ERR_CUMULATIVE_VALUE_TOO_LOW",
"message": "A value is lower than the previous reading of its series.",
"details": {
"series": [
{
"type": "energy",
"direction": "import",
"tariff": "high",
"value": 190000,
"previousValue": 192054,
"previousReadAt": "2026-05-04T08:40:00.000Z"
}
]
}
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b61"
}422 ERR_DOCUMENT_NOT_FOUND
Thrown when a listed document does not exist in the tenant or is deleted.
json
{
"status": 422,
"error": {
"code": "ERR_DOCUMENT_NOT_FOUND",
"message": "Document not found.",
"details": { "documentId": "019247a0-9e1d-7c3b-8f2a-4d6e8a0c2e34" }
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b61"
}📝 POST /v1/reading-entries/override
Records a manual reading whose values break the continuity of their series, after the user confirmed the values are correct and wrote the reason into the note.
Authorization
Requires permission code tenant.readings.override-monotonicity, tenant-scoped.
Request Headers
None beyond the shared API conventions.
Request Body
The body of POST /v1/reading-entries; note is required here.
Request Parameters
None.
Request Logic
- As
POST /v1/reading-entries, without the continuity comparison. Every other check applies, andnotemust be non-empty: the note carries the reason for the override. - The audit entry for each row is marked as written with the continuity override.
Transactional Operations
As POST /v1/reading-entries.
✅ Success Response (201)
As POST /v1/reading-entries.
Response Data Mapping
As POST /v1/reading-entries.
❌ Error Responses
As POST /v1/reading-entries, except ERR_CUMULATIVE_VALUE_TOO_LOW, which this endpoint never returns, plus:
400 ERR_OVERRIDE_NOTE_REQUIRED
Thrown when the note is empty.
json
{
"status": 400,
"error": {
"code": "ERR_OVERRIDE_NOTE_REQUIRED",
"message": "Saving with the continuity override requires a note stating the reason.",
"details": {}
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b71"
}✏️ PATCH /v1/reading-entries/{id}
Changes an entered manual reading as a whole — its moment, any of its values, its note and its documents.
Authorization
Requires permission code tenant.readings.delete, tenant-scoped — the one gate for changing and deleting stored readings.
Request Headers
None beyond the shared API conventions.
Request Body
json
{
"readAt": "2026-05-04T08:40:00.000Z",
"values": [
{ "type": "energy", "direction": "import", "tariff": "high", "value": 192054 }
],
"note": "Odečteno při kontrole rozvaděče. VT opraven podle fotky.",
"documentIds": ["019247a0-9e1d-7c3b-8f2a-4d6e8a0c2e34"]
}| Field | Type | Required | Validation | DB Mapping |
|---|---|---|---|---|
readAt | Timestamp | No | As on create; no other active entry of the gauge at the new moment. | readingEntry.readAt, reading.readAt |
values | Array | No | Any manual-reading series of the gauge: a listed series gets the new value (a row is added when the entry had none), an unlisted one keeps its value; "value": null removes an optional series. The required series of the gauge stay required. | reading.value |
note | String | No | Max 2 000 characters; null clears it. | readingEntry.note |
documentIds | Array of UUID | No | The complete new list; links not in it are detached, the documents themselves are kept. The document list may equally be served by a call of its own (PUT /v1/reading-entries/{id}/documents with the same field) if that is simpler to build; the behaviour is the same. | readingEntryDocument |
Request Parameters
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
id | Path | UUID | Yes | - | The reading entry. | An active entry of the tenant. | readingEntry.id |
Request Logic
- Rejects an entry containing a row with
lifecycleStatefinalorinitial: a meter replacement is changed as a whole throughPUT /v1/readings/meter-replacement/{id}(Meter Replacement Flow), never one half at a time. - A change of
readAtmoves everyreadingrow of the entry to the new moment (removed and re-inserted, becausereadAtpartitions the table) and updates the entry. - Changed and added values are re-checked for continuity against their neighbours at the resulting moment, as on create; a moved or added row takes the series and the unit valid at the resulting moment; a removed series is deleted as a row.
- Writes the audit entries with the before and after values and announces a recalculation for both the old and the new period.
Transactional Operations
The entry, its rows and its links change together or not at all. Concurrent changes of the same entry are not locked: the later write wins, and both are in the audit trail with their before and after values.
✅ Success Response (200)
json
{
"data": { "id": "019247a1-5c3e-7d10-8a2b-1f4c6e8d9a01" },
"status": 200,
"message": "OK",
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b81"
}Response Data Mapping
| Response Field | Source / Value |
|---|---|
id | readingEntry.id |
❌ Error Responses
ERR_READING_ENTRY_NOT_FOUND, ERR_READING_DATE_OUT_OF_RANGE, ERR_READING_SERIES_MISSING, ERR_READING_SERIES_UNKNOWN, ERR_SOURCE_SETTING_MISSING, ERR_READING_ENTRY_EXISTS, ERR_CUMULATIVE_VALUE_TOO_LOW and ERR_DOCUMENT_NOT_FOUND as on POST /v1/reading-entries and GET /v1/reading-entries/{id}, plus:
409 ERR_READING_ENTRY_IS_REPLACEMENT
Thrown when the entry is part of a meter replacement.
json
{
"status": 409,
"error": {
"code": "ERR_READING_ENTRY_IS_REPLACEMENT",
"message": "This reading is part of a meter replacement and is changed through the replacement.",
"details": {}
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b81"
}✏️ PATCH /v1/reading-entries/{id}/override
Changes an entered manual reading whose new values break the continuity of their series, after the user confirmed they are correct and wrote the reason into the note.
Authorization
Requires permission codes tenant.readings.delete and tenant.readings.override-monotonicity, tenant-scoped.
Request Headers
None beyond the shared API conventions.
Request Body
The body of PATCH /v1/reading-entries/{id}; the resulting note must be non-empty.
Request Parameters
As PATCH /v1/reading-entries/{id}.
Request Logic
- As
PATCH /v1/reading-entries/{id}, without the continuity comparison; the note (sent or already stored) must be non-empty and carries the reason; the audit entries are marked as written with the continuity override.
Transactional Operations
As PATCH /v1/reading-entries/{id}.
✅ Success Response (200)
As PATCH /v1/reading-entries/{id}.
Response Data Mapping
As PATCH /v1/reading-entries/{id}.
❌ Error Responses
As PATCH /v1/reading-entries/{id}, except ERR_CUMULATIVE_VALUE_TOO_LOW, plus ERR_OVERRIDE_NOTE_REQUIRED as on POST /v1/reading-entries/override.
🗑️ DELETE /v1/reading-entries/{id}
Deletes an entered manual reading — all its values — keeping the note, the document links and the history traceable.
Authorization
Requires permission code tenant.readings.delete, tenant-scoped.
Request Headers
None beyond the shared API conventions.
Request Body
None.
Request Parameters
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
id | Path | UUID | Yes | - | The reading entry. | An active entry of the tenant. | readingEntry.id |
Request Logic
- Removes the entry's
readingrows, setsdeletedAton the entry and on its document links. - Rejects an entry containing a row with
lifecycleStatefinalorinitial: a meter replacement is corrected through its own flow, never by deleting half of it. - Writes the audit entries and announces a recalculation for the affected period.
Transactional Operations
Rows, entry and links change together or not at all.
✅ Success Response (204)
No body.
Response Data Mapping
N/A — no payload.
❌ Error Responses
ERR_READING_ENTRY_NOT_FOUND as on GET /v1/reading-entries/{id}, plus:
409 ERR_READING_ENTRY_IS_REPLACEMENT
Thrown when the entry is part of a meter replacement.
json
{
"status": 409,
"error": {
"code": "ERR_READING_ENTRY_IS_REPLACEMENT",
"message": "This reading is part of a meter replacement and cannot be deleted here.",
"details": {}
},
"requestId": "019247a1-0000-7a3e-9b77-4f0d3a5e6b91"
}