Functional Weave
Code in TypeScript

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

  • domesticReverseCharge(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 contractor
  • domesticReverseCharge(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 too
  • domesticReverseCharge(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.

export function domesticReverseCharge(supply: ReverseChargeSupply): ReverseChargeDecision
supplyReverseChargeSupply
returnsReverseChargeDecision

The types it declares, generated into your project

export type VatCategory = "standard" | "reduced" | "zero" | "exempt";

export type ReverseChargeReason = "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";

/** The facts about one supply of construction services that decide its VAT treatment. */
export interface ReverseChargeSupply {
  /** tax point of the supply */
  readonly supplyDate: string;
  /** the supplier is VAT registered in the UK */
  readonly supplierVatRegistered: boolean;
  /** the customer is VAT registered in the UK */
  readonly customerVatRegistered: boolean;
  /** payment for the supply is reported within the Construction Industry Scheme */
  readonly reportedUnderCis: boolean;
  /** the rate the supply would bear under normal rules */
  readonly vatCategory: VatCategory;
  /** the supplier is an employment business supplying staff or workers */
  readonly employmentBusiness: boolean;
  /** the customer has told the supplier in writing it is an end user or intermediary supplier */
  readonly endUserNotified: boolean;
  /** contracted value of the reverse charge services as a share of the whole supply; 10000 when all of it */
  readonly reverseChargeShareBasisPoints: number;
}

/** The treatment, why, and what the invoice must say. */
export interface ReverseChargeDecision {
  readonly applies: boolean;
  readonly reason: ReverseChargeReason;
  /** the note the invoice must carry when the reverse charge applies; null otherwise */
  readonly invoiceWording: string | null;
}

Your code names it in one line, in the file that uses it

import { domesticReverseCharge } from "#fune/construction.vat-reverse-charge@^1";
impl/typescript.ts · 59 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { REVERSE_CHARGE_RULES, REVERSE_CHARGE_RULES_HISTORY, REVERSE_CHARGE_RULES_HORIZON } from "./construction_vat_reverse_charge_data.ts";  ← this capability’s own data, compiled from data/reverse-charge.json into the same file by fune build
import { type ReverseChargeDecision, type ReverseChargeReason, type ReverseChargeSupply } from "./construction_vat_reverse_charge_types.ts";

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;

function notApplied(reason: ReverseChargeReason): ReverseChargeDecision {
  return { applies: false, reason, invoiceWording: null };
}

/**
 * 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.
 */
export function domesticReverseCharge(supply: ReverseChargeSupply): ReverseChargeDecision {
  if (typeof supply.supplyDate !== "string" || !ISO_DATE.test(supply.supplyDate)) {
    throw new RangeError(`supplyDate must be an ISO date (YYYY-MM-DD), received "${supply.supplyDate}"`);
  }
  const share = supply.reverseChargeShareBasisPoints;
  if (!Number.isInteger(share)) {
    throw new TypeError(`reverseChargeShareBasisPoints must be an integer, received ${share}`);
  }
  if (share < 0 || share > 10000) {
    throw new RangeError(`reverseChargeShareBasisPoints must be between 0 and 10000, received ${share}`);
  }
  if (!["standard", "reduced", "zero", "exempt"].includes(supply.vatCategory)) {
    throw new RangeError(`unknown vatCategory "${supply.vatCategory}": expected standard, reduced, zero or exempt`);
  }

  const date = supply.supplyDate;
  let rule: (typeof REVERSE_CHARGE_RULES)[number] | null = null;
  for (const row of REVERSE_CHARGE_RULES) {
    if (row.jurisdiction !== "GB" || date < row.validFrom) continue;
    if (row.validTo !== null && date > row.validTo) continue;
    if (rule === null || row.validFrom > rule.validFrom) rule = row;
  }
  if (rule === null) {
    // 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 !== null && date < REVERSE_CHARGE_RULES_HORIZON) {
      throw new RangeError(
        `no reverse charge rule for ${date}: this build was installed with history=${REVERSE_CHARGE_RULES_HISTORY}, ` +
          `so it only carries rules from ${REVERSE_CHARGE_RULES_HORIZON}. Reinstall with history=full.`
      );
    }
    return notApplied("before-start");
  }

  if (!supply.supplierVatRegistered) return notApplied("supplier-not-vat-registered");
  if (!supply.customerVatRegistered) return notApplied("customer-not-vat-registered");
  if (!supply.reportedUnderCis) return notApplied("not-reported-under-cis");
  if (supply.vatCategory !== "standard" && supply.vatCategory !== "reduced") return notApplied("not-standard-or-reduced");
  if (supply.employmentBusiness) return notApplied("employment-business");
  if (supply.endUserNotified) return notApplied("end-user-notified");
  if (share <= rule.disregardBasisPoints) return notApplied("five-percent-disregard");
  return { applies: true, reason: "reverse-charge", invoiceWording: rule.invoiceWording };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript 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
Download for TypeScript construction.vat-reverse-charge-1.0.2-typescript.fune · 26,533 bytes sha256 25a024b0279dab0f102b286c345f66067e613f9bb1ee743a599f1d039453e6d5

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./construction.vat-reverse-charge-1.0.2-typescript.fune, or fetch it from a terminal with fune pull construction.vat-reverse-charge@1.0.2:typescript.

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
NOTICE194
README.md4,435
data/reverse-charge.json312
impl/python.py3,359
impl/rust.rs5,431
impl/typescript.ts3,178
vectors.json12,388