Skip to content

GET /changes

GET /changes # recent changes across all countries
GET /changes/YYYY-MM-DD # all changes detected on a specific snapshot date

/changes is the diff log behind /rates. The daily TEDB cron compares each fresh snapshot against the previous one and writes one row per numeric field that moved. Use this instead of polling /rates and diffing the JSON yourself — it’s cheaper, sorted, and already filtered to actual moves.

  • Dashboard “recent changes” view — render the last N rate changes across all countries.
  • Alerting — react to a country moving its standard rate.
  • Audit trail — query “what changed in May 2026?” with a single GET and an ISO-date floor.

If you need a stream rather than a poll, subscribe a webhook to the rates.changed event — it fires once per cron tick when the diff is non-empty, with the same changes array embedded in the event payload.

{
"changes": [
{
"id": "rc_a1b2c3d4e5f6",
"snapshotDate": "2026-05-30",
"country": "HU",
"field": "standard",
"old": 27,
"new": 25,
"detectedAt": "2026-05-30T07:00:23.000Z"
}
],
"count": 1,
"limit": 50
}
  • id — stable identifier for the change row. Same id across retried webhook deliveries; safe to use for receiver-side dedupe.
  • snapshotDate — the ISO date of the TEDB snapshot where the change was detected. Effectively “when the move took effect” for Stawka’s purposes.
  • country — ISO 3166-1 alpha-2.
  • field — which rate moved. One of:
    • standard
    • super_reduced
    • parking
    • reduced[N] for the Nth slot in the reduced-rates array (0-indexed).
  • old — previous numeric value, or null if the rate didn’t exist before. null means “no such rate yesterday” (e.g. a country newly publishing a super-reduced rate).
  • new — current numeric value, or null if the rate has been removed.
  • detectedAt — ISO 8601 timestamp of when the cron run wrote the row. Distinct from snapshotDate: the snapshot date is what TEDB asserted; detectedAt is when we noticed.

All numeric values are percentage points, never basis points or fractions. 25 means 25%.

ParamTypeDefaultNotes
sinceYYYY-MM-DDnoneSnapshot-date floor. Only changes with snapshotDate >= since are returned.
country2-letter ISOnoneCase-insensitive on input. Filters to a single country.
limitint 1–20050Caps the row count. Values above 200 are clamped silently; values ≤ 0 or non-numeric return 400.

Unknown query parameters are ignored — useful for cache-busting in tests, harmless in production.

  • /changes returns rows newest-first: detected_at DESC, then country ASC, field ASC to break ties when many changes land in the same cron run.
  • /changes/YYYY-MM-DD returns rows ordered by country, field — detection time is identical for every row on a given snapshot date, so the alphabetical order is more useful.
  • /changesCache-Control: public, max-age=300, stale-while-revalidate=86400. The daily cron is the only writer; 300s at the edge is plenty for a dashboard view.
  • /changes/YYYY-MM-DDCache-Control: public, max-age=31536000, immutable. Per-snapshot diff is fixed once the cron has run; cache it forever.
StatusBodyCause
200{"changes": [], "count": 0, ...}Not an error. A steady-state day with no rate moves. Don’t treat as 404.
400{"error": "invalid 'since' — expected YYYY-MM-DD"}Malformed since query param.
400{"error": "invalid 'country' — expected 2-letter ISO code"}country is not exactly two letters.
400{"error": "invalid 'limit' — expected positive integer"}limit is not a positive integer.
401{"error": "INVALID_KEY"}Missing or invalid bearer token.
429{"error": "RATE_LIMITED"} or {"error": "QUOTA_EXCEEDED"}See rate limits.
GET /changes?since=2026-05-01&limit=200

The 200-row cap is the hard maximum. EU VAT moves rarely enough that a month’s worth of changes virtually never exceeds it.

”When did Hungary last move standard VAT?”

Section titled “”When did Hungary last move standard VAT?””
GET /changes?country=HU&limit=10

Returns the most recent Hungarian rate moves; the first row’s snapshotDate is the answer.

GET /changes/2026-05-30

Returns every field that moved that day, ordered by country. Useful for changelogs and post-mortems.

The rate_change table is populated from the daily cron starting when the feature shipped (see changelog). Historical KV snapshots predating that exist — you can still fetch them via /rates/YYYY-MM-DD — but the diff feed only contains moves the cron has had a chance to observe.