GET /changes
GET /changes # recent changes across all countriesGET /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.
When to use it
Section titled “When to use it”- 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.
Response
Section titled “Response”{ "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}Field meanings
Section titled “Field meanings”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:standardsuper_reducedparkingreduced[N]for the Nth slot in the reduced-rates array (0-indexed).
old— previous numeric value, ornullif the rate didn’t exist before.nullmeans “no such rate yesterday” (e.g. a country newly publishing a super-reduced rate).new— current numeric value, ornullif the rate has been removed.detectedAt— ISO 8601 timestamp of when the cron run wrote the row. Distinct fromsnapshotDate: the snapshot date is what TEDB asserted;detectedAtis when we noticed.
All numeric values are percentage points, never basis points or
fractions. 25 means 25%.
Query parameters (/changes only)
Section titled “Query parameters (/changes only)”| Param | Type | Default | Notes |
|---|---|---|---|
since | YYYY-MM-DD | none | Snapshot-date floor. Only changes with snapshotDate >= since are returned. |
country | 2-letter ISO | none | Case-insensitive on input. Filters to a single country. |
limit | int 1–200 | 50 | Caps 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.
Ordering
Section titled “Ordering”/changesreturns rows newest-first:detected_at DESC, thencountry ASC, field ASCto break ties when many changes land in the same cron run./changes/YYYY-MM-DDreturns rows ordered bycountry, field— detection time is identical for every row on a given snapshot date, so the alphabetical order is more useful.
Cache behaviour
Section titled “Cache behaviour”/changes—Cache-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-DD—Cache-Control: public, max-age=31536000, immutable. Per-snapshot diff is fixed once the cron has run; cache it forever.
Errors
Section titled “Errors”| Status | Body | Cause |
|---|---|---|
| 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. |
Recipes
Section titled “Recipes””What changed this month?”
Section titled “”What changed this month?””GET /changes?since=2026-05-01&limit=200The 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=10Returns the most recent Hungarian rate moves; the first row’s
snapshotDate is the answer.
”All changes on a specific day”
Section titled “”All changes on a specific day””GET /changes/2026-05-30Returns every field that moved that day, ordered by country. Useful for changelogs and post-mortems.
Backfill caveat
Section titled “Backfill caveat”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.