Appearance
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
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
medium | Query | Enum | No | all media | Limits 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 Field | Source / Value |
|---|---|
id | vatRate.id |
medium | vatRate.medium |
ratePercent | vatRate.ratePercent |
validFrom | vatRate.validFrom |
legalReference | vatRate.legalReference |
inForce | Derived: the row is the latest non-withdrawn one for its medium whose validFrom is not in the future |
withdrawn | Derived: 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
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
medium | Query | Enum | No | all media | Limits the export to one medium. | An entry from Enum - GaugeMedium. | vatRate.medium |
includeWithdrawn | Query | Boolean | No | true | Whether 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 Field | Source / Value |
|---|---|
| Medium | vatRate.medium, as its UI label |
| Status | Derived as inForce and withdrawn on the list endpoint, plus future when validFrom is after today |
| Rate | vatRate.ratePercent |
| Valid from | vatRate.validFrom |
| Legal reference | vatRate.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
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
medium | Body | Enum | Yes | - | Medium the rate applies to. | An entry from Enum - GaugeMedium; kvp and otherSensor are rejected as never billed. | vatRate.medium |
ratePercent | Body | Decimal | Yes | - | The rate as a percentage. | ≥ 0 and < 100; at most two decimal places. | vatRate.ratePercent |
validFrom | Body | Date | Yes | - | First day the rate applies. | YYYY-MM-DD; may be in the future. | vatRate.validFrom |
legalReference | Body | String | No | null | Citation of the amendment the rate comes from. | Max 200 characters. | vatRate.legalReference |
Request Logic
- Inserts one row into
shared.vat_ratewithtenant_idnull, 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 Field | Source / Value |
|---|---|
id | vatRate.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
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
id | Path | UUID | Yes | - | The rate being corrected. | Must exist and not be withdrawn. | vatRate.id |
ratePercent | Body | Decimal | No | unchanged | Corrected rate. | ≥ 0 and < 100; at most two decimal places. | vatRate.ratePercent |
validFrom | Body | Date | No | unchanged | Corrected start date. | YYYY-MM-DD. | vatRate.validFrom |
legalReference | Body | String | No | unchanged | Corrected 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, leavingmediumuntouched. - 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 Field | Source / Value |
|---|---|
id | vatRate.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
| Name | In | Type | Required | Default | Description | Validation / Notes | DB Mapping |
|---|---|---|---|---|---|---|---|
id | Path | UUID | Yes | - | 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_aton 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
validFromis on or beforeonDate, andnonewhere there is no such row — never a default. - The caller records the returned
idon the invoice, which is what keeps a completed amount reproducible after the catalogue changes.