Skip to content
Updated Sep 16, 2026 by Pablo Coufal · Owner: analysisactiveapi-a Edit on GitHub

VAT Rate Administration — API Analysis ​

Five endpoints on the platform administration application, following the pattern the other cross-tenant catalogues use: a flat collection under v1/admin, a list without paging, writes that return the identifier only, and an XLSX export shaped like the tenant application's building export.

The project has no shared API-conventions document yet, so the two envelope shapes these endpoints use are stated once here and not repeated per endpoint. Success:

json
{
  "data": {},
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21"
}

Error:

json
{
  "errorCode": "ERR_VAT_RATE_DUPLICATE",
  "errorMessage": "A rate for this medium already starts on this date.",
  "status": 409,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21"
}

Authentication and permission failures are answered by the platform application's shared handling and are not restated per endpoint — except the history lock on PATCH and DELETE, which depends on the row rather than on the caller alone.

Permission codes: platform.vat-rates.read (see the catalogue), platform.vat-rates.write (add, correct and withdraw the rate in force and future rates), platform.vat-rates.manage-history (additionally correct and withdraw historical rates; super admin only).


📥 GET /v1/admin/vat-rates ​

Returns the whole catalogue — every rate for every medium, including expired ones — so the administration screen can show each medium's history.

Authorization ​

Requires permission code platform.vat-rates.read, platform-scoped.

Request Headers ​

None beyond the platform application's standard bearer authentication.

Request Body ​

None.

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
mediumQueryEnumNoall mediaLimits the result to one medium.An entry from Enum - GaugeMedium.vatRate.medium

Request Logic ​

  • Reads shared.vat_rate; writes nothing.
  • Returns withdrawn rows as well, flagged, because the screen shows the history including corrections.
  • Ordered so the screen needs no client-side sort:
sql
SELECT id, medium, rate_percent, valid_from, legal_reference, deleted_at
FROM shared.vat_rate
WHERE (@medium IS NULL OR medium = @medium)
ORDER BY medium ASC, valid_from DESC;

Transactional Operations ​

N/A — read-only.

✅ Success Response (200) ​

json
{
  "data": [
    {
      "id": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21",
      "medium": "heat",
      "ratePercent": 12.00,
      "validFrom": "2024-01-01",
      "legalReference": "Zákon č. 349/2023 Sb.",
      "inForce": true,
      "withdrawn": false
    },
    {
      "id": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b22",
      "medium": "heat",
      "ratePercent": 10.00,
      "validFrom": "2020-05-01",
      "legalReference": "Zákon č. 299/2020 Sb.",
      "inForce": false,
      "withdrawn": false
    }
  ],
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

Response Data Mapping ​

Response FieldSource / Value
idvatRate.id
mediumvatRate.medium
ratePercentvatRate.ratePercent
validFromvatRate.validFrom
legalReferencevatRate.legalReference
inForceDerived: the row is the latest non-withdrawn one for its medium whose validFrom is not in the future
withdrawnDerived: vatRate.deletedAt is set

❌ Error Responses ​

400 ERR_VAT_RATE_INVALID_MEDIUM ​

Thrown when the medium parameter is not a value of the medium enum.

json
{
  "errorCode": "ERR_VAT_RATE_INVALID_MEDIUM",
  "errorMessage": "Unknown medium.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

📥 GET /v1/admin/vat-rates/export ​

Downloads the catalogue as an XLSX file, with the same filter the screen applies. Replaces the previous system's generic list export.

Authorization ​

Requires permission code platform.vat-rates.read, platform-scoped.

Request Headers ​

None beyond the platform application's standard bearer authentication.

Request Body ​

None.

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
mediumQueryEnumNoall mediaLimits the export to one medium.An entry from Enum - GaugeMedium.vatRate.medium
includeWithdrawnQueryBooleanNotrueWhether withdrawn rows are exported.Mirrors the screen's toggle for hiding withdrawn rows.vatRate.deletedAt

Request Logic ​

  • Reads the same rows, in the same order, as the list endpoint; writes nothing.
  • One sheet, one row per rate, columns in the screen's order: medium (UI label), status (in force / future / historical / withdrawn), rate in percent, valid from, legal reference.
  • Bypasses the success envelope: the body is the file itself.
sql
SELECT medium, rate_percent, valid_from, legal_reference, deleted_at
FROM shared.vat_rate
WHERE (@medium IS NULL OR medium = @medium)
  AND (@includeWithdrawn OR deleted_at IS NULL)
ORDER BY medium ASC, valid_from DESC;

Transactional Operations ​

N/A — read-only.

✅ Success Response (200) ​

Binary body with Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet and Content-Disposition: attachment; filename="vat-rates.xlsx".

Response Data Mapping ​

Response FieldSource / Value
MediumvatRate.medium, as its UI label
StatusDerived as inForce and withdrawn on the list endpoint, plus future when validFrom is after today
RatevatRate.ratePercent
Valid fromvatRate.validFrom
Legal referencevatRate.legalReference

❌ Error Responses ​

400 ERR_VAT_RATE_INVALID_MEDIUM ​

Thrown when the medium parameter is not a value of the medium enum.

json
{
  "errorCode": "ERR_VAT_RATE_INVALID_MEDIUM",
  "errorMessage": "Unknown medium.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b36"
}

📥 POST /v1/admin/vat-rates ​

Adds a rate for one medium from one date, including a date in the future.

Authorization ​

Requires permission code platform.vat-rates.write, platform-scoped.

Request Headers ​

None beyond the platform application's standard bearer authentication.

Request Body ​

json
{
  "medium": "heat",
  "ratePercent": 12.00,
  "validFrom": "2026-01-01",
  "legalReference": "Zákon č. 349/2023 Sb."
}

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
mediumBodyEnumYes-Medium the rate applies to.An entry from Enum - GaugeMedium; kvp and otherSensor are rejected as never billed.vatRate.medium
ratePercentBodyDecimalYes-The rate as a percentage.≥ 0 and < 100; at most two decimal places.vatRate.ratePercent
validFromBodyDateYes-First day the rate applies.YYYY-MM-DD; may be in the future.vatRate.validFrom
legalReferenceBodyStringNonullCitation of the amendment the rate comes from.Max 200 characters.vatRate.legalReference

Request Logic ​

  • Inserts one row into shared.vat_rate with tenant_id null, so the rate is readable by every tenant.
  • Collision is left to the partial unique index rather than a preceding read, so two administrators entering the same change cannot both succeed.
  • Writes one platform audit entry filed against the new row.
sql
INSERT INTO shared.vat_rate
    (id, tenant_id, medium, rate_percent, valid_from, legal_reference, created_by, updated_by)
VALUES
    (@id, NULL, @medium, @ratePercent, @validFrom, @legalReference, @actor, @actor);

Transactional Operations ​

The row and its audit entry commit together; a duplicate rejected by the unique index rolls back both, so the trail never records a change that did not happen.

✅ Success Response (201) ​

json
{
  "data": {
    "id": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b23"
  },
  "status": 201,
  "message": "Created",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b32"
}

Response Data Mapping ​

Response FieldSource / Value
idvatRate.id of the created row

❌ Error Responses ​

400 ERR_VAT_RATE_INVALID ​

Thrown when the medium is unknown or not billable, the rate is outside its range or carries more than two decimals, or the date is not a valid calendar date.

json
{
  "errorCode": "ERR_VAT_RATE_INVALID",
  "errorMessage": "Rate must be between 0 and 100.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b32"
}

409 ERR_VAT_RATE_DUPLICATE ​

Thrown when a rate for the same medium already starts on the same date.

json
{
  "errorCode": "ERR_VAT_RATE_DUPLICATE",
  "errorMessage": "A rate for this medium already starts on this date.",
  "status": 409,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b32"
}

📥 PATCH /v1/admin/vat-rates/{id} ​

Corrects a rate that was entered wrongly. The medium is not correctable — a rate filed against the wrong medium is withdrawn and re-entered, so the history shows what happened.

Authorization ​

Requires permission code platform.vat-rates.write, platform-scoped. When the target row is historical — not withdrawn, starting on or before today, and superseded by a later non-withdrawn row for the same medium that also starts on or before today — the request additionally requires platform.vat-rates.manage-history, held only by the super admin.

Request Headers ​

None beyond the platform application's standard bearer authentication.

Request Body ​

json
{
  "ratePercent": 12.50,
  "validFrom": "2026-01-01",
  "legalReference": "Zákon č. 349/2023 Sb., ve znění pozdějších předpisů"
}

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The rate being corrected.Must exist and not be withdrawn.vatRate.id
ratePercentBodyDecimalNounchangedCorrected rate.≥ 0 and < 100; at most two decimal places.vatRate.ratePercent
validFromBodyDateNounchangedCorrected start date.YYYY-MM-DD.vatRate.validFrom
legalReferenceBodyStringNounchangedCorrected citation.Max 200 characters.vatRate.legalReference

Request Logic ​

  • Checks, in the same transaction, whether the row is historical; if it is and the caller lacks platform.vat-rates.manage-history, rejects with 403.
  • Recalculates nothing: invoices already completed keep the rate recorded on them.
  • Updates the named row in shared.vat_rate, leaving medium untouched.
  • Writes one platform audit entry carrying the changed fields only.
  • Invoices completed before the correction are unaffected: each records the rate it used.
sql
UPDATE shared.vat_rate
SET rate_percent    = COALESCE(@ratePercent, rate_percent),
    valid_from      = COALESCE(@validFrom, valid_from),
    legal_reference = COALESCE(@legalReference, legal_reference),
    updated_at      = now(),
    updated_by      = @actor
WHERE id = @id
  AND deleted_at IS NULL;

Transactional Operations ​

The update and its audit entry commit together. A corrected validFrom that collides with another row for the same medium is rejected by the unique index and rolls the pair back.

✅ Success Response (200) ​

json
{
  "data": {
    "id": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b23"
  },
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b33"
}

Response Data Mapping ​

Response FieldSource / Value
idvatRate.id of the corrected row

❌ Error Responses ​

400 ERR_VAT_RATE_INVALID ​

Thrown for the same validation failures as on creation.

json
{
  "errorCode": "ERR_VAT_RATE_INVALID",
  "errorMessage": "Rate must be between 0 and 100.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b33"
}

403 ERR_VAT_RATE_HISTORY_LOCKED ​

Thrown when the target row is historical and the caller lacks platform.vat-rates.manage-history.

json
{
  "errorCode": "ERR_VAT_RATE_HISTORY_LOCKED",
  "errorMessage": "Only the super admin can change a historical rate.",
  "status": 403,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b35"
}

404 ERR_VAT_RATE_NOT_FOUND ​

Thrown when no such rate exists, or it has already been withdrawn.

json
{
  "errorCode": "ERR_VAT_RATE_NOT_FOUND",
  "errorMessage": "Rate not found.",
  "status": 404,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b33"
}

409 ERR_VAT_RATE_DUPLICATE ​

Thrown when the corrected start date collides with another rate for the same medium.

json
{
  "errorCode": "ERR_VAT_RATE_DUPLICATE",
  "errorMessage": "A rate for this medium already starts on this date.",
  "status": 409,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b33"
}

📥 DELETE /v1/admin/vat-rates/{id} ​

Withdraws a rate — any rate can be withdrawn when needed. The row stops applying and stays in the history. Withdrawing a historical rate is reserved for the super admin, because it changes what the history says past invoices were completed with; those invoices themselves are not recalculated.

Authorization ​

Requires permission code platform.vat-rates.write, platform-scoped. When the target row is historical — not withdrawn, starting on or before today, and superseded by a later non-withdrawn row for the same medium that also starts on or before today — the request additionally requires platform.vat-rates.manage-history, held only by the super admin.

Request Headers ​

None beyond the platform application's standard bearer authentication.

Request Body ​

None.

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The rate being withdrawn.Must exist and not already be withdrawn.vatRate.id

Request Logic ​

  • Checks, in the same transaction, whether the row is historical; if it is and the caller lacks platform.vat-rates.manage-history, rejects with 403.
  • Recalculates nothing: invoices already completed keep the rate recorded on them.
  • Sets deleted_at on the row; nothing is physically removed.
  • Frees the medium and date for re-entry, because the unique index ignores withdrawn rows.
  • Writes one platform audit entry carrying the row in full.
sql
UPDATE shared.vat_rate
SET deleted_at = now(),
    updated_at = now(),
    updated_by = @actor
WHERE id = @id
  AND deleted_at IS NULL;

Transactional Operations ​

The withdrawal and its audit entry commit together.

✅ Success Response (204) ​

No payload.

Response Data Mapping ​

N/A — no payload.

❌ Error Responses ​

403 ERR_VAT_RATE_HISTORY_LOCKED ​

Thrown when the target row is historical and the caller lacks platform.vat-rates.manage-history.

json
{
  "errorCode": "ERR_VAT_RATE_HISTORY_LOCKED",
  "errorMessage": "Only the super admin can change a historical rate.",
  "status": 403,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b35"
}

404 ERR_VAT_RATE_NOT_FOUND ​

Thrown when no such rate exists, or it is already withdrawn.

json
{
  "errorCode": "ERR_VAT_RATE_NOT_FOUND",
  "errorMessage": "Rate not found.",
  "status": 404,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b34"
}

Internal access — the rate in force ​

The calculation that completes an invoice does not go through HTTP: the tenant application reads the catalogue directly from the cross-tenant schema, through a read port with one operation.

rateInForce(medium, onDate) -> { id, ratePercent } | none
  • Read-only, no transaction of its own; it participates in the caller's.
  • Returns the latest non-withdrawn row for that medium whose validFrom is on or before onDate, and none where there is no such row — never a default.
  • The caller records the returned id on the invoice, which is what keeps a completed amount reproducible after the catalogue changes.