Method
What is collected, what is held back until a second run agrees, how entries are ordered, what “basis” means against a record, what date is held against a change, and how long a change takes to appear, and what that figure does not measure.
Collection
Each watched source is an official publisher’s own page or file: a government list of providers, or a page stating the country’s mandate. Nothing is read from a login, a paywall or behind any access control, and a source’s robots.txt is checked before every request. Where a robots.txt forbids a page, the page is not requested, and that is the authority’s instruction. Where no robots.txt can be read at all, the page is not requested either, and the record says that this was the register’s own caution and not an instruction from the authority.
A run downloads the current file, stores the exact bytes it received under the SHA-256 of their content, and parses it into rows. The parsed rows are compared against the previous run’s stored state to produce a diff: providers added, providers no longer present, and fields that changed. Every one of those is written as an event in an append-only, hash-chained log before anything reaches this site. Proof shows how to check the log.
What holds a change back
A single observation is never enough to publish a statement about a named company. Four guards sit between a parsed page and a public record:
- Two-run confirmation. An addition or a removal is written
provisional. Only when a later run agrees does it becomeconfirmed, and only confirmed events reach the exports and this site. - A minimum hold on a removal. A company disappearing from a list is published only once two consecutive runs agree it is gone, at least 72 hours have passed since it was first absent, and no anomaly was raised for that source in the meantime.
- The anomaly guard. A parse that returns zero rows, or that would drop more of a list in one run than a set limit allows, or whose column headers no longer match what was expected, raises a
source_anomalyevent and publishes no removals at all. A parser problem is assumed before a mass withdrawal is. - Quarantine for a new source. A source just added observes silently for the number of days printed as
quarantine_daysbelow before anything it reports is published, so its own baseline is established before it can claim a change. The period is counted in whole UTC days. Add that number of days to the date the source was first read: the window closes at the end of that day, and the source publishes from 00:00 UTC on the day after. A source first read at any time on 6 October 2026, with 14 days, publishes from 21 October 2026. The count rounds up, never down, so the window is never shorter than the figure, and the day a source starts publishing follows from the day it was first read, not from the time of day a run began. Rounded down, a source first read at 23:59 would be watched for 13 days and one minute, and one first read at 00:01 for almost 14.
The figures below are the ones the run applied, read from run_summary.json under guards.parameters, not from the project’s configuration as it stands now. Each is followed by the engine’s own sentence for what it does, word for word. The rules are written out in docs/DEFAULTS.md, section 3.
anomaly_max_vanished_fraction: 20 per cent of the previous list (published as0.2, a fraction of one)- The largest share of a source's previous list that may vanish in one run. Above it the run raises a source_anomaly, writes no state for that source and publishes no removal from it: a parser problem is assumed before a mass withdrawal is.
anomaly_min_vanished: 2 records- How many records must vanish before the share above is tested at all. Both tests must trip, so one provider leaving a short list is not an anomaly.
quarantine_days: 14 days- How long a newly activated source observes before anything it reports is published, so its own baseline is established before it can claim a change.
withdrawal_hold_hours: 72 hours- How long a company's disappearance waits after the run that first missed it before the withdrawal is published, on top of two agreeing runs. Published again as the floor beneath the figure it dominates, in latency.withdrawal_hold_hours.
How entries are ordered
The rule is published here because it has to be checkable, not taken on trust: a list of companies is ordered alphabetically by name; a list of events, changes or withdrawals is ordered by date, most recent first. That is the whole rule. No entry can pay for inclusion, for a higher position, or for removal from a list. Nothing on this site is sponsored while the site remains free to read; if that ever changes, sponsored material will carry a visible “Sponsored” label and will still never change ordering or inclusion.
What “basis” means
A provider’s name appearing on an official list is not always the same fact. Some authorities approve a provider before it can appear; others publish a list and say plainly that inclusion is not an approval. Saudi Arabia’s ZATCA is explicit that its published list of solution providers is not an approval of any of them. Recording that as an accreditation would misrepresent what the authority itself said, so every listing record on this site carries a basis, which records what the authority actually did, in its own words. Where the authority states it, the quoted sentence itself is shown on the country’s page as “What the authority says this list means”.
| Basis | What it means |
|---|---|
accredited | The authority approves the provider, and its approval is required. |
qualified | The provider met criteria the authority published. In the pages this register has read, it has not found evidence of the two things a stronger reading would need: the authority’s approval for this provider, and a rule making that approval a condition on the taxpayer. That is what was not found, not what the authority denied. |
recognised | The provider passed a recognition process for a named regime. |
listed | Published by the authority without any assessment of the provider. |
self_declared | The provider asserted this and the authority only relays it. |
One of these is never recorded as another. Where a company’s record shows a basis other than accredited, the company’s page says so beside the record rather than beside a generic “accredited” label.
A recognised listing records that the provider passed the authority’s recognition process for a named regime. It is not an approval, an accreditation or a recommendation, and this site does not call it one.
What “regime” means
A regime is the label the register’s source record gives a source’s list, such as e-invoicing or mtd-vat (Making Tax Digital for VAT). It is recorded for each source and copied unchanged onto every record from that source. The set of labels is open: this site quotes the label and does not define it. A country can hold sources in more than one regime, and then its page names all of them and counts each on its own.
HMRC’s list of software recognised for Making Tax Digital for VAT is recorded as mtd-vat. It is not recorded as an e-invoicing list, and a page here that covered the United Kingdom without saying so would be wrong.
Where no source in a country records a regime, the page says that nothing is recorded. The register does not infer a regime from the country, from the name of a list, from its web address or from its basis.
How the publisher name is known
Every source on a country page carries a publisher name. Not all of those names rest on the same evidence. A government sometimes runs its list on a domain that never says, in the page itself, who publishes it. Some pages this record watches have never been fetched at all, because robots.txt blocks them, they sit behind a challenge, or they are otherwise unreachable (rule 11). Where a name cannot be read off the source, it is still recorded, from this project’s own identification of who runs the domain, rather than left blank, because a checkable name beside its address is worth more than no name at all. What it rests on is recorded too, so a reader can tell the two apart.
| Value | What it means |
|---|---|
stated | The fetched page or file names itself — a footer, a copyright line or a sentence in its own text — so the name is checkable against something the publisher wrote, not only against the source address. |
read | The source was fetched — for its rows, its structure, its robots.txt or its reuse terms — but nothing recorded from it names the publisher; the name is this project’s own identification of who runs the domain. |
unread | The page has never been fetched — blocked by robots.txt, behind a challenge, or otherwise unreachable — so the name rests only on the source address and this project’s own knowledge of who runs it. |
This says how the name is known, not whether to trust it. An identification this project made from the domain is checkable the same way a reading of the page is: the source address is published beside it on every country page, and a reader can visit it and judge for themselves.
How “as at a date” is built
/as-at answers who was on a country’s list on a date in the past, worked out from the start and end date held against every listing record, not from a stored snapshot of the list. The roster computed this way only changes on a date a record started or ended. That date is a boundary, and this site builds one page per (country, boundary) pair: 2 pages in this run. That is every date on which the computed answer can differ, not a sample of them.
A date between two boundaries is answered from the boundary before it, because the register recorded no change in between. That is a statement about what was recorded. The register holds a finding only for the days it read a list, and this set does not say which days those were, so it cannot say whether the list was the same on a day between two readings. The page for such a date says so, giving the date asked for, the date the answer is as at, and the next change recorded, rather than serving a computed page silently: which readings the answer rests on is itself part of the answer.
A date before the first boundary a country has is answered as not recorded, never as an empty list: this record did not observe that list yet, which is a different fact from the list having nobody on it. Building a page for every possible calendar date, rather than only the dates that can answer differently, was considered and rejected: it would need many more pages than the 2 that already cover every distinct answer, for no date that could change what this record computes.
What date is held against a change
Every event on this site carries one date: ts, the run that first detected it. That is the date a scheduled check of the official source found it different from the run before. It is not the date the source itself made the change, and not the date the source published it.
No source this record parses states, in a form this record can copy without interpreting prose, the date it itself published a change. Reading a date out of free text is interpretation, and one bad parse would publish a false statement, so this record does not attempt it. A measured claim nothing measures would be worse than no claim at all: what appears against every event is ts alone, and no figure is given for the gap between an authority’s decision and its page. What this record can measure from its own run times is set out in the next section, with what it leaves out.
How long a change takes to appear
Two sets of figures follow. Both are worked out from this record’s own run times, which each run writes into run_summary.json; this page reads them when it is built, and none is typed into it. They describe the run dated . How any of it can be checked is on the proof page.
What these figures measure
Time from a change becoming visible to anyone polling the source to this ledger publishing it. Not time from the authority's own decision: no source read here publishes the date it decided in any form a parser can take, so the gap between the decision and the page is unmeasurable from these files and is in none of these figures. A figure that claimed it would be a guess with a decimal point on it.
run_summary.json, latency.measuresThe gap between an authority deciding and its page showing the decision is not measured here, and nothing on this page estimates it. No source this record reads publishes the date it decided in any form a parser can take, so there is nothing to subtract from. This page therefore gives no figure, range or average for that gap, and a figure that did would not be a measurement.
Why a removal waits
A removal naming a company is published only after two consecutive runs agree it is gone and at least 72 hours (3 days) have passed since it was first absent. That minimum is a guard against publishing a false statement about a named company, not slowness in the pipeline, and it is published beside the figure it affects so the two are read together.
detection_to_publication is dominated by withdrawal_hold_hours, which is deliberate: a withdrawal naming a company waits two agreeing runs and then that hold before it is published (docs/DEFAULTS.md section 3). The floor is published beside the figure so it reads as the guard it is rather than as slowness.
run_summary.json, latency.noteThe rule is set out above under What holds a change back, and in the project’s standing answers as docs/DEFAULTS.md section 3.
How often each source is read
A fetch interval is the gap between two consecutive runs that read a source. It says how often the source was read; it does not say how quickly a change reached a reader after the authority made it. A run whose fetch or parse failed leaves no run time for the source, so it is not counted as a read. Run times are kept for the latest 20 fetches of each source, and the figures cover those.
- Pooled over
- 18 intervals on record
- Median
- 1 days (86604 s)
- Shortest
- 29.62 minutes (1777 s)
- Longest
- 6.12 days (528798 s)
By source, 2 have an interval, 0 have one fetch on record, 61 have not yet been fetched and 0 have a run time on record that cannot be placed. These are counts of sources, not figures: the pooled set above is every interval published in the table, not an average of the per-source medians.
Where a source has no interval, its row says why in place of a figure: a short label, then the reason as the file states it. One fetch on record leaves no gap to measure, a source never fetched has none, and a run time that cannot be placed leaves none either, because a gap measured across it would be arithmetic on a date nothing places. None of these is shown as 0 or as a dash: 0 would read as a measurement of no delay, and a dash as nothing worth reporting. Sources are listed alphabetically by identifier, each with a link to its country page.
If this table is wider than the space it has here, scroll it sideways to see every column.
| Source | Fetches on record | Median gap | Shortest | Longest |
|---|---|---|---|---|
| ae_asp_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| ae_asp_preapproved_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| ae_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| au_ap_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| be_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| bg_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| bh_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| br_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| cl_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| co_pac_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| cz_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| de_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| dk_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| ee_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| eg_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| es_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| fi_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| fr_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| fr_pa_list | 11 | 1 days86517 s | 29.62 minutes1777 s | 6.12 days528798 s |
| fr_pa_reserve_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| gb_budget_2026_roadmap | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| gb_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| gb_openpeppol_uk_wg | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| gb_recognised_software | 9 | 1 days86701.5 s | 18.52 hours66662 s | 6.12 days528798 s |
| gb_tax_update_2026 | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| gr_asp_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| hr_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| hu_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| id_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| ie_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| il_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| in_gsp_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| is_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| it_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| jo_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| jp_peppol_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| ke_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| lt_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| lv_ap_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| mx_pac_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| my_intermediaries | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| ng_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| no_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| nz_ap_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| om_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| pe_pse_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| peppol_certified_providers | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| ph_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| pl_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| pt_certified_software | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| ro_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| rs_intermediaries | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| sa_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| sa_solution_providers | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| se_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| sg_ap_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| sg_ap_list_conditional | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| si_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| sk_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| th_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| tr_integrator_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| tw_mandate_page | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
| vn_provider_list | 0 | Not yet fetched. no fetch on record. The source is not active, or every attempt on it failed and a failed attempt writes no state (engine/run.py). | ||
How long a change waited before it was published
The wait for a published change is the time from the run that first saw it to the run that published it: ts minus first_present for an addition, changed_at for a modification and removed_at for a removal. It includes the second agreeing run that every statement about a named company needs and, for a removal, the hold above. Only changes in the published feed are measured, so every figure rests on a record a reader can read.
- Pooled over
- 6 published changes
- Median
- 18.72 hours (67407 s)
- Shortest
- 18.72 hours (67407 s)
- Longest
- 18.72 hours (67407 s)
If this table is wider than the space it has here, scroll it sideways to see every column.
| Kind of change | Changes measured | Median wait | Shortest | Longest |
|---|---|---|---|---|
| Additions | 6 | 18.72 hours67407 s | 18.72 hours67407 s | 18.72 hours67407 s |
| Modifications | 0 | No wait measured yet. no published change carries both ends of its own wait yet. | ||
| Removals | 0 | No wait measured yet. no published change carries both ends of its own wait yet. | ||
| Floor under every removal: 3 days (259200 s), the least time a removal is held after it is first absent, before it can be published. | ||||
The same wait, by source
If this table is wider than the space it has here, scroll it sideways to see every column.
| Source | Changes measured | Median wait | Shortest | Longest |
|---|---|---|---|---|
| fr_pa_list | 6 | 18.72 hours67407 s | 18.72 hours67407 s | 18.72 hours67407 s |
What is left out of the wait
0 changes to mandate pages are left out. A mandate page is not a statement about a named company, so a change to one is published on the run that sees it rather than held for a second; its detection and its publication are the same run, and counting that would report a wait nobody served.
0 additions of a provider that had a published withdrawal before it are left out. The exclusion was made when an addition was dated from first_seen, which a returning provider keeps so that its history does not restart, and which gave the record’s whole life as the wait. An addition is now dated from first_present, the run that first saw that addition, so a returning provider’s own wait can be stated. The exclusion has not been lifted, and a count is published here so that the number left out is visible.
0 changes are left out because the run that first saw them will not read as a timestamp, or is dated after the run that published them.
The second exclusion is decided from the published feed, so that this ledger and a reader holding the same file leave out exactly the same changes. Where a withdrawal was withheld from the feed, by an owner hold or while a source was in quarantine, and the provider then returns, nobody reading the published files can see the withdrawal. That addition is not recognised as a return, so it is measured, and its wait runs from its own first_present like any other addition’s. Two returning providers can therefore be treated differently here, one left out and one measured, depending only on whether the withdrawal that preceded them is published.
Recompute it yourself
Every figure above can be worked out again from published files, and this site serves them (see the list of files on the proof page). The run names the file each comes from: run_summary.json for the fetch intervals, which hold the run times, and the file named in the second string for the waits.
run_summary.json: latency.fetch_interval.sources[].fetched_at
run_summary.json, latency.recomputable_from.fetch_intervalchanges/<year>.json: every confirmed added, modified and removed event, as ts - changed_at/first_present/removed_at by type
run_summary.json, latency.recomputable_from.detection_to_publicationThe rule, in whole seconds throughout. A fetch interval is the gap between two consecutive run times of one source, oldest first. A wait is the ts of a confirmed change minus its detection field, named above for each kind. A median is the ordinary median: the middle value, or the mean of the two middle ones. Shortest and longest are the extremes.
The wait string above names the file to recompute from and says, in its own words, whether that file holds every published change or only a window of them; a figure recomputed from a window is taken over that window. Where a set publishes year files (changes/<year>.json), they hold every published change: in this set they are 2026.json, listed in changes/index.json.
Both are served from this site, at the paths listed on the proof page, which do not move. The portable archive that the proof page describes holds the same files.
This page is information taken from official published lists. It is not tax, legal or professional advice, and it does not replace checking the official source.