Skip to content
Updated Oct 5, 2026 by Pavel Coufal · Owner: analysisactiveapi-a Edit on GitHub

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 ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
gaugeQueryUUIDYes-Gauge whose readings are listed.Must be a gauge of the tenant.reading.gaugeId
sourceQueryEnumYes-manual for the manual view, remote for the remote view.manual or remote from Enum - ReadingSource.reading.source
fromQueryTimestampNomanual: none; remote: 30 days before toStart of the window, inclusive.ISO 8601.reading.readAt
toQueryTimestampNonowEnd of the window, inclusive.ISO 8601; from ≤ to; for source = remote a window of at most 366 days.reading.readAt
sortQueryStringNoreadAtManual 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
orderQueryEnumNodescManual view only: asc or desc.--
pageQueryIntegerNo1Manual view only: page number.≥ 1.-
pageSizeQueryIntegerNomanual 25; remote 100Entries per page or batch.1–100 (manual), 1–500 (remote).-
cursorQueryStringNo-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 reading for the gauge, source and window, and for source = manual the matching active readingEntry rows with the count of active readingEntryDocument rows.
  • 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 by readAt descending 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; null for the first row of a series, for a row with lifecycleState = initial, and for format = delta or type = 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 FieldSource / Value
idreadingEntry.id; null for a remote entry
gaugeIdreading.gaugeId
readAtreading.readAt
sourcereading.source
hasNoteDerived: readingEntry.note is set; false for a remote entry
documentCountDerived: active readingEntryDocument rows of the entry; 0 for a remote entry
values[].typereading.type
values[].directionreading.direction
values[].tariffreading.tariff
values[].valuereading.value
values[].unitreading.unit
values[].consumptionDerived: difference to the nearest earlier row of the same series, as described in Request Logic
values[].lifecycleStatereading.lifecycleState
meta.page, meta.totalManual view: current page and number of matching entries; null for the remote view
meta.pageSizeEffective page or batch size
meta.nextCursorRemote 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
}
FieldTypeRequiredValidationDB Mapping
filtersObjectYesgauge and source as in the GET /v1/reading-entries query; for source = remote either range or from / to.As in the list
filters.rangeEnumNoRemote 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
columnsArray of StringYes1–100 unique keys from the readings-manual or readings-remote column catalogue (Enum - TableViewKey), matching filters.source.Column catalogue → reading series
idsArray of UUIDNo1–5 000 entry ids; only for source = manual. null exports every matching entry.readingEntry.id

Request Parameters ​

None.

Request Logic ​

  • Validates columns against the catalogue of the overview selected by filters.source; the catalogue holds the union of the series tariffSeries(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 listed ids within 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 FieldSource / Value
Column readAtreading.readAt, in the tenant's time zone
Column <type>.<direction>.<tariff>reading.value of that series, with reading.unit in the header
Column <series>.consumptionDerived, 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 ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The reading entry.An active entry of the tenant.readingEntry.id

Request Logic ​

  • Reads the active readingEntry, its reading rows (source = manual, same gaugeId and readAt) and its active readingEntryDocument rows 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 FieldSource / Value
idreadingEntry.id
gaugeIdreadingEntry.gaugeId
readAtreadingEntry.readAt
notereadingEntry.note
values[].readingIdreading.id
values[].type, direction, tariff, value, unitThe same-named attributes of reading
documents[].documentIdreadingEntryDocument.documentId
documents[].name, mimeTypeThe linked document's name and type, from Document Management
createdBy, createdAt, updatedBy, updatedAtThe 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"]
}
FieldTypeRequiredValidationDB Mapping
gaugeIdUUIDYesA gauge of the tenant with at least one parameter whose readingMethod is manual or both.readingEntry.gaugeId, reading.gaugeId
readAtTimestampYesBetween 1990-01-01 and now; no active entry of the gauge at this moment.readingEntry.readAt, reading.readAt
valuesArrayYes1–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[].typeEnumYesEnum - ReadingType.reading.type
values[].directionEnumNoimport / export; defaults to the gauge channel's direction.reading.direction
values[].tariffEnumNoEnum - ReadingTariff.reading.tariff
values[].valueNumberYesFinite.reading.value
noteStringNoMax 2 000 characters.readingEntry.note
documentIdsArray of UUIDNo0–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 at readAt, not today's — and, per parameter, the manual gaugeSourceSetting valid at readAt; a parameter without one is rejected with ERR_SOURCE_SETTING_MISSING.
  • Writes one readingEntry, one reading row per value (source = manual, unit from that source setting, format per the matrix on the reading page, lifecycleState = reading) and one readingEntryDocument row 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 with lifecycleState = 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 FieldSource / Value
idreadingEntry.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, and note must 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"]
}
FieldTypeRequiredValidationDB Mapping
readAtTimestampNoAs on create; no other active entry of the gauge at the new moment.readingEntry.readAt, reading.readAt
valuesArrayNoAny 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
noteStringNoMax 2 000 characters; null clears it.readingEntry.note
documentIdsArray of UUIDNoThe 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 ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The reading entry.An active entry of the tenant.readingEntry.id

Request Logic ​

  • Rejects an entry containing a row with lifecycleState final or initial: a meter replacement is changed as a whole through PUT /v1/readings/meter-replacement/{id} (Meter Replacement Flow), never one half at a time.
  • A change of readAt moves every reading row of the entry to the new moment (removed and re-inserted, because readAt partitions 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 FieldSource / Value
idreadingEntry.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 ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The reading entry.An active entry of the tenant.readingEntry.id

Request Logic ​

  • Removes the entry's reading rows, sets deletedAt on the entry and on its document links.
  • Rejects an entry containing a row with lifecycleState final or initial: 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"
}