construction.vat-reverse-charge
Whether the UK VAT domestic reverse charge applies to a construction supply, and the invoice wording.
1.0.2 · published 2026-10-03 by charlie · Anterra
Pinned by 24 tests, run in TypeScript, Python and Rust.
Not professional advice. This capability calculates tax figures from published rules. It is a software component for developers, not tax advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a VAT specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
Decides whether the UK VAT domestic reverse charge for building and construction services (VAT Act 1994 s.55A, in force from 1 March 2021) applies to one supply. It also gives the note the invoice must carry when it does. Under the reverse charge the supplier invoices without VAT and the customer accounts for the VAT to HMRC.
## The conditions, in the order they are checked
For example
domestic_reverse_charge(supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa…)→ applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. a VAT and CIS registered subcontractor billing a VAT and CIS registered contractordomestic_reverse_charge(supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category reduced, employment business false, end user notified fal…)→ applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. reduced-rated work is reverse charged toodomestic_reverse_charge(supply date 2021-03-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa…)→ applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. the first day of the reverse charge
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def domestic_reverse_charge(supply: ReverseChargeSupply) -> ReverseChargeDecision
| supply | ReverseChargeSupply | |
| returns | ReverseChargeDecision |
The types it declares, generated into your project
VatCategory = Literal["standard", "reduced", "zero", "exempt"]
ReverseChargeReason = Literal["reverse-charge", "before-start", "supplier-not-vat-registered", "customer-not-vat-registered", "not-reported-under-cis", "not-standard-or-reduced", "employment-business", "end-user-notified", "five-percent-disregard"]
@dataclass(frozen=True)
class ReverseChargeSupply:
"""The facts about one supply of construction services that decide its VAT treatment."""
#: tax point of the supply
supply_date: str
#: the supplier is VAT registered in the UK
supplier_vat_registered: bool
#: the customer is VAT registered in the UK
customer_vat_registered: bool
#: payment for the supply is reported within the Construction Industry Scheme
reported_under_cis: bool
#: the rate the supply would bear under normal rules
vat_category: VatCategory
#: the supplier is an employment business supplying staff or workers
employment_business: bool
#: the customer has told the supplier in writing it is an end user or intermediary supplier
end_user_notified: bool
#: contracted value of the reverse charge services as a share of the whole supply; 10000 when all of it
reverse_charge_share_basis_points: int
@dataclass(frozen=True)
class ReverseChargeDecision:
"""The treatment, why, and what the invoice must say."""
applies: bool
reason: ReverseChargeReason
#: the note the invoice must carry when the reverse charge applies; null otherwise
invoice_wording: Optional[str]
Your code names it in one line, in the file that uses it
from fune.construction.vat_reverse_charge import domestic_reverse_charge # construction.vat-reverse-charge@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import re
from .construction_vat_reverse_charge_data import REVERSE_CHARGE_RULES, REVERSE_CHARGE_RULES_HISTORY, REVERSE_CHARGE_RULES_HORIZON ← this capability’s own data, compiled from data/reverse-charge.json into the same file by fune build
from .construction_vat_reverse_charge_types import ReverseChargeDecision, ReverseChargeReason, ReverseChargeSupply
ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
def _not_applied(reason: ReverseChargeReason) -> ReverseChargeDecision:
return ReverseChargeDecision(applies=False, reason=reason, invoice_wording=None)
def domestic_reverse_charge(supply: ReverseChargeSupply) -> ReverseChargeDecision:
"""Whether the UK VAT domestic reverse charge for building and construction
services (VATA 1994 s.55A) applies to one supply.
The conditions are checked in a fixed order and the first that fails is the
reason, so the answer is the same in every language and explains itself.
"""
if not isinstance(supply.supply_date, str) or not ISO_DATE.fullmatch(supply.supply_date):
raise ValueError('supplyDate must be an ISO date (YYYY-MM-DD), received "%s"' % (supply.supply_date,))
share = supply.reverse_charge_share_basis_points
if isinstance(share, bool) or not isinstance(share, int):
raise TypeError("reverseChargeShareBasisPoints must be an integer, received %r" % (share,))
if share < 0 or share > 10000:
raise ValueError("reverseChargeShareBasisPoints must be between 0 and 10000, received %d" % (share,))
if supply.vat_category not in ("standard", "reduced", "zero", "exempt"):
raise ValueError(
'unknown vatCategory "%s": expected standard, reduced, zero or exempt' % (supply.vat_category,)
)
date = supply.supply_date
rule = None
for row in REVERSE_CHARGE_RULES:
if row.jurisdiction != "GB" or date < row.valid_from:
continue
if row.valid_to is not None and date > row.valid_to:
continue
if rule is None or row.valid_from > rule.valid_from:
rule = row
if rule is None:
# A pruned build must not answer "before the reverse charge started" for
# a date it simply no longer carries rules for.
if REVERSE_CHARGE_RULES_HORIZON is not None and date < REVERSE_CHARGE_RULES_HORIZON:
raise ValueError(
"no reverse charge rule for %s: this build was installed with history=%s, "
"so it only carries rules from %s. Reinstall with history=full."
% (date, REVERSE_CHARGE_RULES_HISTORY, REVERSE_CHARGE_RULES_HORIZON)
)
return _not_applied("before-start")
if not supply.supplier_vat_registered:
return _not_applied("supplier-not-vat-registered")
if not supply.customer_vat_registered:
return _not_applied("customer-not-vat-registered")
if not supply.reported_under_cis:
return _not_applied("not-reported-under-cis")
if supply.vat_category not in ("standard", "reduced"):
return _not_applied("not-standard-or-reduced")
if supply.employment_business:
return _not_applied("employment-business")
if supply.end_user_notified:
return _not_applied("end-user-notified")
if share <= rule.disregard_basis_points:
return _not_applied("five-percent-disregard")
return ReverseChargeDecision(applies=True, reason="reverse-charge", invoice_wording=rule.invoice_wording)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add construction.vat-reverse-charge
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./construction.vat-reverse-charge-1.0.2-python.fune, or fetch it from a terminal with fune pull construction.vat-reverse-charge@1.0.2:python.
The whole function, every language, is one file too: construction.vat-reverse-charge-1.0.2.fune, 35,697 bytes, sha256 da27d1183c7730bec44c1eddb84b28ee0e321a5d184e5d382e7eca4805b2fab4. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before construction.vat-reverse-charge
after — your function gets the result and the arguments, and returns the final result.
# fune: after construction.vat-reverse-charge
replace — it requires no other capability, so there is no dependency to replace.
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show construction.vat-reverse-charge --steps.
# fune: step construction.vat-reverse-charge after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a VAT and CIS registered subcontractor billing a VAT and CIS registered contractor | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. |
| reduced-rated work is reverse charged too | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category reduced, employment business false, end user notified fal… | → | applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. |
| the first day of the reverse charge | supply date 2021-03-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. |
| the day before it started | supply date 2021-02-28, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies false, reason before-start, invoice wording — |
| before March 2021 nothing else matters | supply date 2020-06-01, supplier vat registered true, customer vat registered false, reported under cis true, vat category standard, employment business false, end user notified f… | → | applies false, reason before-start, invoice wording — |
| a supplier that is not VAT registered charges no VAT at all | supply date 2026-09-01, supplier vat registered false, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified f… | → | applies false, reason supplier-not-vat-registered, invoice wording — |
| a householder is not VAT registered, so normal VAT rules apply | supply date 2026-09-01, supplier vat registered true, customer vat registered false, reported under cis false, vat category standard, employment business false, end user notified … | → | applies false, reason customer-not-vat-registered, invoice wording — |
| a payment not reported under CIS | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis false, vat category standard, employment business false, end user notified f… | → | applies false, reason not-reported-under-cis, invoice wording — |
| zero-rated new build housing is not reverse charged | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category zero, employment business false, end user notified false,… | → | applies false, reason not-standard-or-reduced, invoice wording — |
| an exempt supply is not reverse charged | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category exempt, employment business false, end user notified fals… | → | applies false, reason not-standard-or-reduced, invoice wording — |
Show the other 14 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| an employment business supplies staff, not construction services | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business true, end user notified fal… | → | applies false, reason employment-business, invoice wording — |
| a customer that has notified end user status in writing | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified tr… | → | applies false, reason end-user-notified, invoice wording — |
| an intermediary supplier (a landlord re-supplying to its tenant) notifies the same way | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified tr… | → | applies false, reason end-user-notified, invoice wording — |
| zero-rated is checked before end user status | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category zero, employment business false, end user notified true, … | → | applies false, reason not-standard-or-reduced, invoice wording — |
| exactly 5 percent is disregarded: the rule is 5% or less | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies false, reason five-percent-disregard, invoice wording — |
| a supply and fix contract whose fitting is 2 percent | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies false, reason five-percent-disregard, invoice wording — |
| 5.01 percent is not disregarded | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. |
| a share above 100 percent | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | error: reverseChargeShareBasisPoints must be between 0 and 10000 |
| a negative share | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | error: reverseChargeShareBasisPoints must be between 0 and 10000 |
| a fractional share | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | error: reverseChargeShareBasisPoints must be an integer |
| a malformed date | supply date 01/09/2026, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | error: supplyDate must be an ISO date |
| an unknown VAT category | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category STANDARD, employment business false, end user notified fa… | → | error: unknown vatCategory |
| a trailing newline after supplyDate is not an ISO date | supply date 2026-09-16 , supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified f… | → | error: supplyDate must be an ISO date |
| Arabic-Indic digits in supplyDate are not an ISO date | supply date ٢٠٢٦-09-16, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | error: supplyDate must be an ISO date |
More from the author
The first condition that fails becomes the `reason`, so every answer explains itself and the three languages agree on which reason wins:
| reason | the reverse charge does not apply because | |---|---| | `before-start` | the tax point is before 1 March 2021 | | `supplier-not-vat-registered` | the supplier is not UK VAT registered (and charges no VAT at all) | | `customer-not-vat-registered` | the customer is not UK VAT registered, e.g. a householder | | `not-reported-under-cis` | the payment is not reported within the Construction Industry Scheme | | `not-standard-or-reduced` | the supply is zero-rated or exempt | | `employment-business` | the supplier is an employment business supplying staff or workers | | `end-user-notified` | the customer has said in writing that it is an end user or intermediary supplier | | `five-percent-disregard` | the reverse charge services are 5% or less of the whole supply | | `reverse-charge` | none of the above: it applies (`applies` is true) |
**End users and intermediary suppliers.** Being one only changes anything once the customer has told the supplier **in writing**. Until then HMRC says the supplier must still apply the reverse charge. An intermediary supplier is connected or linked to the end user, either with a relevant interest in the same land (landlord and tenant) or as part of the same corporate group (Companies Act 2006 s.1161). It gives the same kind of written notice, so both cases are the one `endUserNotified` flag rather than a separate connected-party input.
**The 5% disregard** is judged on the contracted values agreed at the start of the contract, not invoice by invoice. Pass that share as `reverseChargeShareBasisPoints` (10000 when the whole supply is construction services). The guide says "5% or less", so exactly 500 is disregarded, and normal VAT rules then apply to the whole supply. When the predominant element is zero-rated, the supply is not standard or reduced-rated in the first place, so `not-standard-or-reduced` is reported.
## Invoice wording
When the reverse charge applies, `invoiceWording` is "Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies." The VAT Regulations 1995 require the reference "reverse charge", and the other two phrases are HMRC's own examples ("Customer to pay the VAT to HMRC", "S55A VATA 94 applies"). The invoice must also show the VAT due under the reverse charge (or the rate, if the amount cannot be shown) without adding it to the amount charged. That is for the invoice itself; `finance.tax.add-vat` gives the amount.
The start date, the 5% threshold and the wording are dated data in `data/reverse-charge.json`. A `history=current` build refuses a date before its horizon rather than calling it `before-start`.
## Not covered
Credit notes spanning the March 2021 change, the transitional scaffolding rules, and whether a service is within CIS at all (that is the caller's `reportedUnderCis`). The CIS deduction itself is `construction.cis-deduction`.
## Sources
- HMRC, "VAT reverse charge technical guide": https://www.gov.uk/guidance/vat-reverse-charge-technical-guide (start date 1 March 2021, end users and intermediary suppliers, employment businesses, the 5% disregard, invoice wording). - HMRC, "How to use the VAT reverse charge if you supply building and construction services": https://www.gov.uk/guidance/how-to-use-the-vat-reverse-charge-if-you-supply-building-and-construction-services (the list of conditions). - HMRC, "VAT domestic reverse charge for building and construction services": https://www.gov.uk/guidance/vat-domestic-reverse-charge-for-building-and-construction-services
1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in supplyDate; adds tests.
## Notices
Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).
1.0.2 adds its attribution notices (NOTICE). The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| NOTICE | 194 |
| README.md | 4,435 |
| data/reverse-charge.json | 312 |
| impl/python.py | 3,359 |
| impl/rust.rs | 5,431 |
| impl/typescript.ts | 3,178 |
| vectors.json | 12,388 |