About this site
The JSON API
The record this site renders, as files a program can fetch. There is no query endpoint, no account and no key: every answer is a file, and the record's files are the committed bytes of the changelogs repository, at the same relative paths as in a clone of it.
Three ways in
- The JSON API Every change as JSON files: an index per act, one file per version, fetched with curl.
- The MCP server Ask a model client about the record, read-only, with no key.
- Watch from CI Open an issue when a provision you depend on changes.
/api/v1/substances.json lists every CAS and EC number an entry in the record prints, and /api/v1/substances/<scheme>-<value>.json gives each one's entries and changes as data.
Layout
| Path | What it is | From |
|---|---|---|
/api/ | this page, with the CI example at its end | the site build |
/api/v1/index.json | the root index: one row per act | the record, served as committed |
/api/v1/<act_dir>/index.json | one act's versions, and every change per provision | the record |
/api/v1/<act_dir>/changes/<version>.json | one version's payload, with the texts | the record |
/api/v1/catalogue.json | labels, aliases, sectors, page and feed addresses | the site build |
/api/v1/schema/<name>.schema.json | the JSON Schema of each document | the site build |
/api/v1/substances.json | every CAS and EC number an entry prints | the site build |
/api/v1/substances/<scheme>-<value>.json | one number's entries and changes | the site build |
<act_dir> is the act's corpus and key, and <version> the version it is the payload of. Every path an index names is relative to /api/v1/. The schemas: act-index, change, entry, index, register, twins, catalogue, substance. Each carries its own file name as a relative $id.
The three dates
detected_onis the date the watcher saw the consolidated version: when this record learnt of the change, not when the law moved. A repair never changes it.in_forceis the start of validity the publisher states for the consolidated version: on a version's row the dates its changes report, on a change one date or null.applies_fromis a date only when one is deterministically readable from a date change in the act's own application provision; otherwiseunknown, the common and honest answer, orunchanged. It is never a deadline and never says whether a provision applies to you.
updated_on on a version's row is the latest of detected_on and every repair's date, the field to sort by to see what moved since you last looked.
Where the sources differ: signals and reasons
Three signals look at every transition: structural_diff, corpus_metadata and instruction_parse. Each reports one status per change:
observed: the signal names this unit.absent: the signal could speak about this window and does not name this unit.unavailable: the signal could not speak about this window at all. It is not dissent and never makes a dispute.
A change is disputed when one signal observed it and another is absent, or when the signals that observed it name kinds of change that share nothing. Each such change carries a dispute_reason, every other change carries null, and every row of an act index carries the field whether or not an older payload stored it. Filter on the reason or on the signal triple, never on disputed alone. The seven reasons, in the order they are tried:
kind_mismatch: No one kind of change is named by every source that found it.textless_both_others: Two sources name it, and neither carries any text.textless_metadata_only: Only the EU's own amendment metadata names it, and it carries no text.textless_instruction_only: Only the amending act's instructions name it, and they carry no text.both_others_silent: Only the text comparison found it.metadata_silent: The source that does not name it is the EU's own amendment metadata.instruction_silent: The source that does not name it is the amending act's instructions.
A reason is a reading of stored verdicts and says nothing about the law. The methodology page explains each signal and how often the sources differ.
Units no change carries
metadata_only_units, on each row ofevents: units the corpus metadata names that no change of that version carries.instruction_only_units, on each row ofevents: units only the instruction parse names.
Corrections
The record is corrected forward and its history is never rewritten. A repaired version gets a new sha256, a later updated_on and one more repairs row; a change a repair withdraws disappears from provisions. Key cached rows on (act, version, location, occurrence) and re-read a version whenever its sha256 moves.
Polling, CORS and keys
Poll the root index, never the whole tree. An act's index_sha256 moves exactly when its act index's bytes move, and a version's sha256 exactly when its payload's do, so a consumer holding a file whose hash still matches has nothing to fetch.
Any origin may GET any file. There are no credentials, no keys and no cookies, and no rate promise: a client that polls more often than the record is written gets the same bytes and gains nothing.
Licence
The record is licensed as its changelogs repository states, which also says on what terms the legislative text it quotes is reused.
Examples
Every act, with its newest version, its counts and its title:
curl -fsSL https://emendrix.eu/api/v1/index.json \ | jq -r '.acts[] | [.key, .newest_version, .changes, .disputed, .title] | @tsv'
The newest change to AR 1 of 32006R1907:
curl -fsSL https://emendrix.eu/api/v1/eu/32006R1907/index.json \ | jq '.provisions["AR 1"][0]'
The MCP server
The same record, for a model: a read-only MCP server at /mcp, answering streamable HTTP with one JSON response per request, with no session, no key and no account. Its eight tools are list_acts, find_provisions, changes_since, provision_history, get_change, get_event, list_disputed and find_substance, and its four resources emendrix://acts, emendrix://acts/{act}, emendrix://changes/{act}/{version}/{location} and emendrix://methodology. Every result carries the disclaimer and the file each fact was read from, with that file's sha256.
It decides nothing: it does not read or interpret law, cannot search law text and cannot resolve a free reference such as Art. 50(2). Name an act by its key and a provision by its canonical location, as list_acts and find_provisions give them.
The server describes itself at /mcp/server-card, and the site's /.well-known/api-catalog names this endpoint and the JSON API.
A client that takes a URL needs only https://emendrix.eu/mcp. In Claude Code:
claude mcp add --transport http emendrix https://emendrix.eu/mcp
In a client configured by a JSON file:
{
"mcpServers": {
"emendrix": {
"type": "http",
"url": "https://emendrix.eu/mcp"
}
}
}Watch a provision from CI
A GitHub Actions workflow that opens an issue when a provision you pin has a newer change. It is an example to adapt rather than a maintained action: set PIN to the act and the canonical location, and SEEN to the version you have already reviewed. It needs no secret beyond the job's own token and dedupes by issue title. It uses jq and gh, which GitHub-hosted runners have preinstalled.
# .github/workflows/regulations.yml: opens an issue when a provision you pin changes.
on:
schedule: [{ cron: "17 6 * * 1" }]
workflow_dispatch: {}
permissions: { issues: write }
jobs:
check:
runs-on: ubuntu-latest
env:
PIN: "32017R0745 AN I"
SEEN: "02017R0745-20260101"
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- run: |
act=${PIN%% *}; loc=${PIN#* }
slug=$(printf '%s' "$loc" | tr 'A-Z ' 'a-z-' | tr -d '()')
curl -fsSL -A "emendrix-snippet/1" "https://emendrix.eu/api/v1/eu/$act/index.json" -o idx.json
new=$(jq -r --arg l "$loc" '.provisions[$l][0].version // empty' idx.json)
if [ -z "$new" ] || [ "$new" = "$SEEN" ]; then exit 0; fi
title="$act $loc changed: $new"
gh issue list --state all --search "\"$title\" in:title" --json title -q '.[].title' \
| grep -qxF "$title" && exit 0
row=$(jq -c --arg l "$loc" '.provisions[$l][0]' idx.json)
gh issue create --title "$title" --body "$(printf '%s\n\n%s\n\n%s' \
"Newest index row: $row" \
"History, verbatim text and citations: https://emendrix.eu/acts/$act/$slug/" \
"Not legal advice: computed by emendrix from published texts, with no lawyer's review.")"
The emendrix-snippet/1 user agent lets the example's use be counted from the server's own request lines, with no cookies and no tracking; change it if you would rather not be counted.