Functional Weave
Code in Rust

property.stamp-duty

Stamp duty on a property purchase: SDLT (England, NI), LBTT (Scotland), LTT (Wales), with reliefs and surcharges.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

Pinned by 49 tests, run in TypeScript, Python and Rust.

Not professional advice. This capability calculates property figures from published rules. It is a software component for developers, not legal or financial 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 conveyancer or tax adviser (SDLT, LBTT, LTT) review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

Status: needs review by a conveyancing/tax professional before it is published.

The tax on buying land or property in the UK, which is three different taxes:

For example

  • stamp_duty(price £295,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01) → tax SDLT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,750.00, supplement basis points 0%, supplement £0.00, total £4,750.00 England, £295,000 from April 2025: £4,750
  • stamp_duty(price £2,000,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01) → tax SDLT, schedule standard, slices ×5, surcharge basis points 0%, band tax £153,750.00, supplement basis points 0%, supplement £0.00, total £153,750.00 England, £2,000,000: every band up to 12%
  • stamp_duty(price £0.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01) → tax SDLT, schedule standard, slices , surcharge basis points 0%, band tax £0.00, supplement basis points 0%, supplement £0.00, total £0.00 England, a zero price: no bands, no tax

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.

pub fn stamp_duty(purchase: &StampDutyPurchase) -> StampDutyResult
purchaseStampDutyPurchase
returnsStampDutyResult

The types it declares, generated into your project

// StampDutyRegion is a string in Rust, one of: "england-ni", "scotland", "wales".
// Parameters take it as &str and results hold it as String.

// PropertyUse is a string in Rust, one of: "residential", "non-residential".
// Parameters take it as &str and results hold it as String.

/// One purchase, as the buyer would describe it on the return.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StampDutyPurchase {
    /// the chargeable consideration, in GBP
    pub price: Money,
    /// where the land is: england-ni for SDLT, scotland for LBTT, wales for LTT
    pub region: String,
    /// non-residential includes mixed use
    pub property_use: String,
    /// every buyer is a first-time buyer buying their main home (residential only)
    pub first_time_buyer: bool,
    /// the purchase attracts the higher rates or ADS: a buyer will own more than one dwelling and is not replacing a main home
    pub additional_property: bool,
    /// a buyer is not UK resident for SDLT; only England and NI have a surcharge
    pub non_resident: bool,
    /// usually completion; the date the rates are taken from
    pub effective_date: String,
}

/// One band of the price and the tax on it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StampDutySlice {
    /// the band's rate including any surcharge added to every band
    pub basis_points: i64,
    /// the part of the price in this band
    pub taxable: Money,
    /// taxable × rate, rounded down to the penny; for display
    pub tax: Money,
}

/// The tax, how it was reached, and in which schedule.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StampDutyResult {
    /// SDLT, LBTT or LTT
    pub tax: String,
    /// standard, first-time-buyer or higher
    pub schedule: String,
    /// the bands the price reaches, lowest first
    pub slices: Vec<StampDutySlice>,
    /// points added to every band (England: higher rates and non-resident)
    pub surcharge_basis_points: i64,
    /// the banded tax, rounded down to the pound
    pub band_tax: Money,
    /// a whole-price supplement's rate (Scotland's ADS), or 0
    pub supplement_basis_points: i64,
    /// the supplement, rounded down to the pound
    pub supplement: Money,
    /// bandTax + supplement
    pub total: Money,
}

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

fune!(property.stamp-duty@^1);  // then call stamp_duty(…)
impl/rust.rs · 228 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::money_amount::{money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune
use super::property_stamp_duty_data::{
    StampDutyBand, StampDutySurcharge, BANDS, BANDS_HISTORY, BANDS_HORIZON, SURCHARGES, SURCHARGES_HISTORY,
    SURCHARGES_HORIZON,
};

// £10bn: price × the highest rate stays an exact JavaScript number; the same limit here.
const MAX_PRICE: i64 = 1_000_000_000_000;

fn is_iso_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() == 10
        && bytes.iter().enumerate().all(|(i, b)| if i == 4 || i == 7 { *b == b'-' } else { b.is_ascii_digit() })
}

// A build installed with history=current keeps only rules still in force, so
// an older date can find nothing even after the horizon check; say why.
fn pruned_note(history: &str) -> String {
    if history == "full" {
        String::new()
    } else {
        format!(" (this build was installed with history={}; reinstall with history=full for older dates)", history)
    }
}

fn in_force(valid_from: &str, valid_to: Option<&str>, on_date: &str) -> bool {
    on_date >= valid_from && valid_to.map_or(true, |to| on_date <= to)
}

fn schedule(region: &str, use_: &str, name: &str, on_date: &str) -> Vec<&'static StampDutyBand> {
    let mut rows: Vec<&'static StampDutyBand> = BANDS
        .iter()
        .filter(|b| b.region == region && b.property_use == use_ && b.schedule == name && in_force(b.valid_from, b.valid_to, on_date))
        .collect();
    rows.sort_by_key(|b| b.from_minor);
    // The bands must run from zero without gaps, or the data is wrong.
    let mut expected: Option<i64> = Some(0);
    for row in &rows {
        if expected != Some(row.from_minor) {
            panic!("the {} bands for {} {} on {} do not form a ladder", name, region, use_, on_date);
        }
        expected = row.to_minor;
    }
    if !rows.is_empty() && expected.is_some() {
        panic!("the {} bands for {} {} on {} do not form a ladder", name, region, use_, on_date);
    }
    rows
}

fn find_surcharge(region: &str, kind: &str, on_date: &str) -> Option<&'static StampDutySurcharge> {
    SURCHARGES
        .iter()
        .find(|s| s.region == region && s.kind == kind && in_force(s.valid_from, s.valid_to, on_date))
}

/// Stamp duty on one purchase: SDLT in England and Northern Ireland, LBTT in
/// Scotland, LTT in Wales. The price is sliced into the bands in force on the
/// effective date; England's higher rates and non-resident surcharge add
/// points to every band, Wales's higher rates are a schedule of their own, and
/// Scotland's ADS is a separate charge on the whole price. Each tax is rounded
/// down to the whole pound once, as the returns require.
///
/// # Panics
/// Panics on a malformed date, an unknown region or use, a price not in GBP
/// or out of range, contradictory flags, or no rates for the date.
pub fn stamp_duty(purchase: &StampDutyPurchase) -> StampDutyResult {
    let price = &purchase.price;
    let region = purchase.region.as_str();
    let use_ = purchase.property_use.as_str();
    let on_date = purchase.effective_date.as_str();
    if !is_iso_date(on_date) {
        panic!("effectiveDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    let tax_name = match region {
        "england-ni" => "SDLT",
        "scotland" => "LBTT",
        "wales" => "LTT",
        other => panic!("unknown region \"{}\": use england-ni, scotland or wales", other),
    };
    if use_ != "residential" && use_ != "non-residential" {
        panic!("unknown use \"{}\": use residential or non-residential", use_);
    }
    if price.currency != "GBP" {
        panic!("price must be in GBP, received {}", price.currency);
    }
    if price.minor < 0 || price.minor > MAX_PRICE {
        panic!("price must be between 0 and {} minor units, received {}", MAX_PRICE, price.minor);
    }
    if use_ == "non-residential" && (purchase.first_time_buyer || purchase.additional_property) {
        panic!("firstTimeBuyer and additionalProperty apply only to residential purchases");
    }
    if purchase.first_time_buyer && purchase.additional_property {
        panic!("a first-time buyer cannot be buying an additional property");
    }
    // A pruned build must refuse a date it no longer has the rules for.
    for (history, horizon) in [(BANDS_HISTORY, BANDS_HORIZON), (SURCHARGES_HISTORY, SURCHARGES_HORIZON)] {
        if let Some(h) = horizon {
            if history != "full" && on_date < h {
                panic!(
                    "no {} rates for {}: this build was installed with history={}, so it only carries rules from {}",
                    tax_name, on_date, history, h
                );
            }
        }
    }

    let mut surcharges: Vec<&'static StampDutySurcharge> = Vec::new();
    if use_ == "residential" {
        if purchase.additional_property {
            match find_surcharge(region, "additional-property", on_date) {
                None => panic!("no higher rates rule for {} on {}{}", region, on_date, pruned_note(SURCHARGES_HISTORY)),
                Some(row) => {
                    if price.minor >= row.minimum_price_minor {
                        surcharges.push(row);
                    }
                }
            }
        }
        if purchase.non_resident {
            if let Some(row) = find_surcharge(region, "non-resident", on_date) {
                if price.minor >= row.minimum_price_minor {
                    surcharges.push(row);
                }
            }
        }
    }

    let mut schedule_name = "standard";
    if surcharges.iter().any(|s| s.basis == "higher-schedule") {
        schedule_name = "higher";
    } else if purchase.first_time_buyer {
        let relief = schedule(region, use_, "first-time-buyer", on_date);
        if let Some(first) = relief.first() {
            if first.price_cap_minor.map_or(true, |cap| price.minor <= cap) {
                schedule_name = "first-time-buyer";
            }
        }
    }
    let bands = schedule(region, use_, schedule_name, on_date);
    if bands.is_empty() {
        panic!(
            "no {} {} rates for {} {} on {}{}",
            tax_name,
            schedule_name,
            region,
            use_,
            on_date,
            pruned_note(BANDS_HISTORY)
        );
    }

    let surcharge_points: i64 = surcharges.iter().filter(|s| s.basis == "each-band").map(|s| s.basis_points).sum();
    let supplement_points: i64 = surcharges.iter().filter(|s| s.basis == "whole-price").map(|s| s.basis_points).sum();

    // Everything below is in pence × basis points, exact until the final floor.
    let mut slices: Vec<StampDutySlice> = Vec::new();
    let mut exact: i64 = 0;
    for band in bands {
        if price.minor <= band.from_minor {
            break;
        }
        let top = band.to_minor.map_or(price.minor, |to| price.minor.min(to));
        let taxable = top - band.from_minor;
        let rate = band.basis_points + surcharge_points;
        exact += taxable * rate;
        slices.push(StampDutySlice {
            basis_points: rate,
            taxable: money(taxable, "GBP"),
            tax: money(taxable * rate / 10000, "GBP"),
        });
    }
    let band_tax = exact / 1_000_000 * 100;
    let supplement = price.minor * supplement_points / 1_000_000 * 100;
    StampDutyResult {
        tax: tax_name.to_string(),
        schedule: schedule_name.to_string(),
        slices,
        surcharge_basis_points: surcharge_points,
        band_tax: money(band_tax, "GBP"),
        supplement_basis_points: supplement_points,
        supplement: money(supplement, "GBP"),
        total: money(band_tax + supplement, "GBP"),
    }
}

pub fn stamp_duty_purchase_from_value(v: &Value) -> StampDutyPurchase {
    StampDutyPurchase {
        price: money_from_value(v.get("price")),
        region: v.get("region").as_str().to_string(),
        property_use: v.get("propertyUse").as_str().to_string(),
        first_time_buyer: v.get("firstTimeBuyer").as_bool(),
        additional_property: v.get("additionalProperty").as_bool(),
        non_resident: v.get("nonResident").as_bool(),
        effective_date: v.get("effectiveDate").as_str().to_string(),
    }
}

pub fn stamp_duty_result_to_value(r: &StampDutyResult) -> Value {
    Value::obj(vec![
        ("tax", Value::str(&r.tax)),
        ("schedule", Value::str(&r.schedule)),
        (
            "slices",
            Value::Arr(
                r.slices
                    .iter()
                    .map(|s| {
                        Value::obj(vec![
                            ("basisPoints", Value::Int(s.basis_points)),
                            ("taxable", money_to_value(&s.taxable)),
                            ("tax", money_to_value(&s.tax)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("surchargeBasisPoints", Value::Int(r.surcharge_basis_points)),
        ("bandTax", money_to_value(&r.band_tax)),
        ("supplementBasisPoints", Value::Int(r.supplement_basis_points)),
        ("supplement", money_to_value(&r.supplement)),
        ("total", money_to_value(&r.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    stamp_duty_result_to_value(&stamp_duty(&stamp_duty_purchase_from_value(&args[0])))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add property.stamp-duty
Download for Rust property.stamp-duty-1.0.0-rust.fune · 115,222 bytes sha256 3f9465e24755c0c8aebd5b03e03f098175cd796ca2a01524d391a4fb99c26fc1

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./property.stamp-duty-1.0.0-rust.fune, or fetch it from a terminal with fune pull property.stamp-duty@1.0.0:rust.

The whole function, every language, is one file too: property.stamp-duty-1.0.0.fune, 129,189 bytes, sha256 fcd3d2d8bd1ad9d340cf7d48b2397b61661731d25e301809dc084f1f4f9c2100. 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 property.stamp-duty

after — your function gets the result and the arguments, and returns the final result.

// fune: after property.stamp-duty

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace money.amount in property.stamp-duty

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 property.stamp-duty --steps.

// fune: step property.stamp-duty 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
England, £295,000 from April 2025: £4,750 price £295,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,750.00, supplement basis points 0%, supplement £0.00, total £4,750.00
England, £2,000,000: every band up to 12% price £2,000,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×5, surcharge basis points 0%, band tax £153,750.00, supplement basis points 0%, supplement £0.00, total £153,750.00
England, a zero price: no bands, no tax price £0.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax SDLT, schedule standard, slices , surcharge basis points 0%, band tax £0.00, supplement basis points 0%, supplement £0.00, total £0.00
England first-time buyer, £500,000: relief up to the limit, £10,000 price £500,000.00, region england-ni, property use residential, first time buyer true, additional property false, non resident false, effective date 2025-06-01 → tax SDLT, schedule first-time-buyer, slices ×2, surcharge basis points 0%, band tax £10,000.00, supplement basis points 0%, supplement £0.00, total £10,000.00
England first-time buyer a penny over £500,000 loses the relief entirely: £15,000 price £500,000.01, region england-ni, property use residential, first time buyer true, additional property false, non resident false, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×3, surcharge basis points 0%, band tax £15,000.00, supplement basis points 0%, supplement £0.00, total £15,000.00
England first-time buyer 1 July 2021: 5% above £300,000 price £450,000.00, region england-ni, property use residential, first time buyer true, additional property false, non resident false, effective date 2021-07-01 → tax SDLT, schedule first-time-buyer, slices ×2, surcharge basis points 0%, band tax £7,500.00, supplement basis points 0%, supplement £0.00, total £7,500.00
England first-time buyer 23 September 2022: nil band £425,000, limit £625,000 price £625,000.00, region england-ni, property use residential, first time buyer true, additional property false, non resident false, effective date 2022-09-23 → tax SDLT, schedule first-time-buyer, slices ×2, surcharge basis points 0%, band tax £10,000.00, supplement basis points 0%, supplement £0.00, total £10,000.00
England first-time buyer 31 March 2025, last day of the £425,000 band price £450,000.00, region england-ni, property use residential, first time buyer true, additional property false, non resident false, effective date 2025-03-31 → tax SDLT, schedule first-time-buyer, slices ×2, surcharge basis points 0%, band tax £1,250.00, supplement basis points 0%, supplement £0.00, total £1,250.00
England first-time buyer 1 April 2025, same price, back to £300,000 price £450,000.00, region england-ni, property use residential, first time buyer true, additional property false, non resident false, effective date 2025-04-01 → tax SDLT, schedule first-time-buyer, slices ×2, surcharge basis points 0%, band tax £7,500.00, supplement basis points 0%, supplement £0.00, total £7,500.00
England additional property from April 2025, £300,000: 5 points on every band, £20,000 price £300,000.00, region england-ni, property use residential, first time buyer false, additional property true, non resident false, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×3, surcharge basis points 5%, band tax £20,000.00, supplement basis points 0%, supplement £0.00, total £20,000.00
Show the other 39 tests
CaseArgumentsExpected
England additional property on 30 October 2024: 3 points, £11,500 price £300,000.00, region england-ni, property use residential, first time buyer false, additional property true, non resident false, effective date 2024-10-30 → tax SDLT, schedule standard, slices ×2, surcharge basis points 3%, band tax £11,500.00, supplement basis points 0%, supplement £0.00, total £11,500.00
England additional property on 31 October 2024: 5 points, £17,500 price £300,000.00, region england-ni, property use residential, first time buyer false, additional property true, non resident false, effective date 2024-10-31 → tax SDLT, schedule standard, slices ×2, surcharge basis points 5%, band tax £17,500.00, supplement basis points 0%, supplement £0.00, total £17,500.00
England additional property under £40,000: no higher rates price £39,999.99, region england-ni, property use residential, first time buyer false, additional property true, non resident false, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×1, surcharge basis points 0%, band tax £0.00, supplement basis points 0%, supplement £0.00, total £0.00
England additional property at exactly £40,000: higher rates apply, £2,000 price £40,000.00, region england-ni, property use residential, first time buyer false, additional property true, non resident false, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×1, surcharge basis points 5%, band tax £2,000.00, supplement basis points 0%, supplement £0.00, total £2,000.00
England non-resident first-time buyer at £700,000 (the gov.uk worked example): £39,000 price £700,000.00, region england-ni, property use residential, first time buyer true, additional property false, non resident true, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×3, surcharge basis points 2%, band tax £39,000.00, supplement basis points 0%, supplement £0.00, total £39,000.00
England non-resident buying an additional property: both surcharges, 7 points price £300,000.00, region england-ni, property use residential, first time buyer false, additional property true, non resident true, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×3, surcharge basis points 7%, band tax £26,000.00, supplement basis points 0%, supplement £0.00, total £26,000.00
England during the 2021 holiday: a non-resident pays 2% even in the nil band price £500,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident true, effective date 2021-04-15 → tax SDLT, schedule standard, slices ×1, surcharge basis points 2%, band tax £10,000.00, supplement basis points 0%, supplement £0.00, total £10,000.00
England during the 2021 holiday: first-time buyer relief was replaced by the reduced rates price £400,000.00, region england-ni, property use residential, first time buyer true, additional property false, non resident false, effective date 2021-05-01 → tax SDLT, schedule standard, slices ×1, surcharge basis points 0%, band tax £0.00, supplement basis points 0%, supplement £0.00, total £0.00
England 30 September 2021: nil band £250,000 price £300,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2021-09-30 → tax SDLT, schedule standard, slices ×2, surcharge basis points 0%, band tax £2,500.00, supplement basis points 0%, supplement £0.00, total £2,500.00
England 1 October 2021: nil band £125,000 and the 2% band return price £300,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2021-10-01 → tax SDLT, schedule standard, slices ×3, surcharge basis points 0%, band tax £5,000.00, supplement basis points 0%, supplement £0.00, total £5,000.00
tax is rounded down to the pound once: £130,049.99 is £100.9998, so £100 price £130,049.99, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×2, surcharge basis points 0%, band tax £100.00, supplement basis points 0%, supplement £0.00, total £100.00
England non-residential, £300,000: £4,500 price £300,000.00, region england-ni, property use non-residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,500.00, supplement basis points 0%, supplement £0.00, total £4,500.00
England non-residential bought by a non-resident: no surcharge on non-residential price £300,000.00, region england-ni, property use non-residential, first time buyer false, additional property false, non resident true, effective date 2025-06-01 → tax SDLT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,500.00, supplement basis points 0%, supplement £0.00, total £4,500.00
Scotland, £300,000: £4,600 price £300,000.00, region scotland, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax LBTT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,600.00, supplement basis points 0%, supplement £0.00, total £4,600.00
Scotland first-time buyer, £300,000: nil band £175,000 saves £600 price £300,000.00, region scotland, property use residential, first time buyer true, additional property false, non resident false, effective date 2025-06-01 → tax LBTT, schedule first-time-buyer, slices ×3, surcharge basis points 0%, band tax £4,000.00, supplement basis points 0%, supplement £0.00, total £4,000.00
Scotland, £800,000: into the 12% band price £800,000.00, region scotland, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax LBTT, schedule standard, slices ×5, surcharge basis points 0%, band tax £54,350.00, supplement basis points 0%, supplement £0.00, total £54,350.00
Scotland ADS at 8% of the whole price from 5 December 2024 price £300,000.00, region scotland, property use residential, first time buyer false, additional property true, non resident false, effective date 2025-06-01 → tax LBTT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,600.00, supplement basis points 8%, supplement £24,000.00, total £28,600.00
Scotland ADS at 6% on 4 December 2024 price £300,000.00, region scotland, property use residential, first time buyer false, additional property true, non resident false, effective date 2024-12-04 → tax LBTT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,600.00, supplement basis points 6%, supplement £18,000.00, total £22,600.00
Scotland ADS at 4% on 15 December 2022 price £300,000.00, region scotland, property use residential, first time buyer false, additional property true, non resident false, effective date 2022-12-15 → tax LBTT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,600.00, supplement basis points 4%, supplement £12,000.00, total £16,600.00
Scotland ADS not due under £40,000 price £39,999.00, region scotland, property use residential, first time buyer false, additional property true, non resident false, effective date 2025-06-01 → tax LBTT, schedule standard, slices ×1, surcharge basis points 0%, band tax £0.00, supplement basis points 0%, supplement £0.00, total £0.00
Scotland LBTT and ADS are each rounded down: £4,600.5625 and £24,000.90 price £300,011.25, region scotland, property use residential, first time buyer false, additional property true, non resident false, effective date 2025-06-01 → tax LBTT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,600.00, supplement basis points 8%, supplement £24,000.00, total £28,600.00
Scotland has no non-resident surcharge price £300,000.00, region scotland, property use residential, first time buyer false, additional property false, non resident true, effective date 2025-06-01 → tax LBTT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,600.00, supplement basis points 0%, supplement £0.00, total £4,600.00
Scotland non-residential, £400,000: £8,500 price £400,000.00, region scotland, property use non-residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax LBTT, schedule standard, slices ×3, surcharge basis points 0%, band tax £8,500.00, supplement basis points 0%, supplement £0.00, total £8,500.00
Wales, £300,000 from 10 October 2022: £4,500 price £300,000.00, region wales, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax LTT, schedule standard, slices ×2, surcharge basis points 0%, band tax £4,500.00, supplement basis points 0%, supplement £0.00, total £4,500.00
Wales, £300,000 on 9 October 2022: £4,950 price £300,000.00, region wales, property use residential, first time buyer false, additional property false, non resident false, effective date 2022-10-09 → tax LTT, schedule standard, slices ×3, surcharge basis points 0%, band tax £4,950.00, supplement basis points 0%, supplement £0.00, total £4,950.00
Wales during the 2021 reduction: nil band £250,000 price £300,000.00, region wales, property use residential, first time buyer false, additional property false, non resident false, effective date 2021-04-01 → tax LTT, schedule standard, slices ×2, surcharge basis points 0%, band tax £2,500.00, supplement basis points 0%, supplement £0.00, total £2,500.00
Wales higher rates from 11 December 2024 are their own table: £19,950 price £300,000.00, region wales, property use residential, first time buyer false, additional property true, non resident false, effective date 2025-06-01 → tax LTT, schedule higher, slices ×3, surcharge basis points 0%, band tax £19,950.00, supplement basis points 0%, supplement £0.00, total £19,950.00
Wales higher rates on 10 December 2024: £16,950 price £300,000.00, region wales, property use residential, first time buyer false, additional property true, non resident false, effective date 2024-12-10 → tax LTT, schedule higher, slices ×3, surcharge basis points 0%, band tax £16,950.00, supplement basis points 0%, supplement £0.00, total £16,950.00
Wales higher rates do not apply under £40,000 price £39,999.00, region wales, property use residential, first time buyer false, additional property true, non resident false, effective date 2025-06-01 → tax LTT, schedule standard, slices ×1, surcharge basis points 0%, band tax £0.00, supplement basis points 0%, supplement £0.00, total £0.00
Wales has no first-time buyer relief: the main rates apply price £200,000.00, region wales, property use residential, first time buyer true, additional property false, non resident false, effective date 2025-06-01 → tax LTT, schedule standard, slices ×1, surcharge basis points 0%, band tax £0.00, supplement basis points 0%, supplement £0.00, total £0.00
Wales non-residential, £1,200,000: £49,750 price £1,200,000.00, region wales, property use non-residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → tax LTT, schedule standard, slices ×4, surcharge basis points 0%, band tax £49,750.00, supplement basis points 0%, supplement £0.00, total £49,750.00
a first-time buyer cannot also be buying an additional property price £300,000.00, region england-ni, property use residential, first time buyer true, additional property true, non resident false, effective date 2025-06-01 → error: a first-time buyer cannot be buying an additional property
first-time buyer relief is residential only price £300,000.00, region england-ni, property use non-residential, first time buyer true, additional property false, non resident false, effective date 2025-06-01 → error: apply only to residential purchases
England residential before 8 July 2020 is outside the data price £300,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2020-07-07 → error: no SDLT standard rates for england-ni residential on 2020-07-07
Scotland residential before 1 April 2021 is outside the data price £300,000.00, region scotland, property use residential, first time buyer false, additional property false, non resident false, effective date 2021-03-31 → error: no LBTT standard rates for scotland residential on 2021-03-31
Northern Ireland is part of england-ni price £300,000.00, region northern-ireland, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → error: unknown region
a price in euros is refused price €300,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → error: price must be in GBP
a negative price is refused price -£0.01, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 2025-06-01 → error: price must be between 0 and
a malformed date is refused price £300,000.00, region england-ni, property use residential, first time buyer false, additional property false, non resident false, effective date 1 June 2025 → error: effectiveDate must be an ISO date

More from the author

| region | tax | administered by | |---|---|---| | `england-ni` | Stamp Duty Land Tax (SDLT) | HMRC | | `scotland` | Land and Buildings Transaction Tax (LBTT) | Revenue Scotland | | `wales` | Land Transaction Tax (LTT) | Welsh Revenue Authority |

All three are "slice" taxes: the price is cut into bands and each band is taxed at its own rate. `stampDuty` takes one purchase and returns the bands it reaches, the tax on each, the total, and which schedule of rates applied.

## What it covers

- **Residential and non-residential** (non-residential includes mixed use) purchase prices, for every region. - **First-time buyers.** England: relief rates up to a price limit; above the limit the relief is lost entirely and the standard rates apply to the whole price (£500,000 → £10,000; £500,000.01 → £15,000). Scotland: the nil band rises to £175,000, with no price limit. Wales has no first-time buyer relief, so the main rates apply. From 8 July 2020 to 30 June 2021 England's first-time buyer rates were replaced by the reduced standard rates, so the standard schedule applies then. - **Additional dwellings** (`additionalProperty`): England's higher rates add 3 points to every band (5 points from 31 October 2024); Scotland's Additional Dwelling Supplement is a separate charge on the whole price (4%, then 6% from 16 December 2022, 8% from 5 December 2024); Wales's higher residential rates are a separate table with their own bands. None applies below £40,000. - **Non-resident surcharge** (`nonResident`): England and Northern Ireland only, 2 points on every residential band from 1 April 2021, on top of any other rates including the nil band and first-time buyer rates. It does not apply to non-residential purchases, and Scotland and Wales have none, so the flag changes nothing there.

The caller says whether the higher rates apply. Whether a purchase attracts them (replacing a main residence, spouses, inherited shares, the 36-month refund window) is a legal judgement, not arithmetic.

## Rounding

Each tax is rounded down to the whole pound, once, from the exact sum of the bands (HMRC SDLT Manual SDLTM00050: "The amount calculated is round down to the nearest pound"; Revenue Scotland and the Welsh Revenue Authority return guidance: tax entered rounded down to the nearest pound). In Scotland LBTT and ADS are separate lines on the return, each rounded down on its own, so they are here too: `bandTax` and `supplement`, with `total` their sum. The per-band `tax` in `slices` is rounded down to the penny and is for display; it is not what the total is built from.

## Rules as dated data

Every band, relief schedule and surcharge is a dated row in `data/bands.json` and `data/surcharges.json`, taken from the date the rates changed, and the lookup uses `effectiveDate` (normally completion). The data covers every date from 1 April 2021 onwards in every region (and back to 8 July 2020 in England, 27 July 2020 in Wales, 2016 and 2018 for non-residential):

- England residential: 8 July 2020 (nil band £500,000), 1 July 2021 (£250,000), 1 October 2021 (£125,000 and a 2% band), 23 September 2022 (£250,000; first-time buyers £425,000 up to £625,000), 1 April 2025 (back to £125,000; first-time buyers £300,000 up to £500,000). - Scotland residential from 1 April 2021 (unchanged through 2026-27). - Wales main rates: 27 July 2020, 1 July 2021, 10 October 2022; higher rates 1 April 2018, 22 December 2020, 11 December 2024.

A date outside the data is an error, not a guess. A project installed with `history=current` keeps only the rules still in force and refuses an older date with an explanation rather than answering it with today's rates.

## What it does not do

- **Transitional contract rules.** Where a contract exchanged before a change completes after it (England's 3% higher rates for contracts before 31 October 2024; Scotland's 6% ADS for contracts on or before 4 December 2024; Wales's October 2022 and December 2024 rules), the old rates may apply. Pass the date whose rates apply as `effectiveDate`. - Leases (SDLT/LBTT/LTT on the net present value of rent), linked transactions, companies (England's 17% rate for non-natural persons over £500,000), multiple dwellings relief (abolished in England from 1 June 2024), mixed-use apportionment, and other reliefs and exemptions. - ADS on joint purchases or partial interests, and ADS or higher rates repayment claims.

## Sources

England and Northern Ireland (SDLT), fetched September 2026:

- GOV.UK, "Stamp Duty Land Tax: Residential property rates" (current rates, first-time buyers, 5% additional property surcharge, 2% non-resident surcharge): https://www.gov.uk/stamp-duty-land-tax/residential-property-rates - GOV.UK, "Stamp Duty Land Tax rates from 1 December 2003 to 31 March 2025", and the period pages linked from it (8 July 2020 to 30 June 2021; 1 July 2021 to 30 September 2021; 1 October 2021 to 22 September 2022; 23 September 2022 to 30 October 2024; 31 October 2024 to 31 March 2025; 4 December 2014 to 7 July 2020 for the non-residential rates from 17 March 2016 and the 3% higher rates from 1 April 2016): https://www.gov.uk/government/publications/rates-and-allowances-stamp-duty-land-tax - GOV.UK, "Higher rates of Stamp Duty Land Tax" (not below £40,000): https://www.gov.uk/guidance/stamp-duty-land-tax-buying-an-additional-residential-property - GOV.UK, "Rates of Stamp Duty Land Tax for non-UK residents" (£40,000; applies on top of the zero and first-time buyer rates; the £700,000 worked example, £39,000): https://www.gov.uk/guidance/rates-of-stamp-duty-land-tax-for-non-uk-residents - HMRC Stamp Duty Land Tax Manual SDLTM00050 (rounding): https://www.gov.uk/hmrc-internal-manuals/stamp-duty-land-tax-manual/sdltm00050

Scotland (LBTT):

- Revenue Scotland, "Residential property" (rates from 1 April 2021, first-time buyer nil band £175,000): https://revenue.scot/taxes/land-buildings-transaction-tax/residential-property - Revenue Scotland, "Non-residential property" (rates from 25 January 2019): https://revenue.scot/taxes/land-buildings-transaction-tax/non-residential-property - Revenue Scotland, "The Additional Dwelling Supplement (ADS)" (4% from 25 January 2019, 6% from 16 December 2022, 8% from 5 December 2024; on the total price; not below £40,000): https://revenue.scot/taxes/land-buildings-transaction-tax/additional-dwelling-supplement-ads - Revenue Scotland, "About the calculation" (LBTT and ADS each rounded down to the nearest pound): https://revenue.scot/land-buildings-transaction-tax/guidance/how-to/paper-lbtt-return/about-calculation - Scottish Government, "Scottish Budget 2026 to 2027: Scottish tax ready reckoners" (LBTT rates and bands, including ADS, unchanged for 2026-27): https://www.gov.scot/publications/scottish-budget-2026-2027-scottish-tax-ready-reckoners/pages/4/

Wales (LTT):

- Welsh Government, "Land Transaction Tax rates and bands" (main residential rates from 27 July 2020, 1 July 2021 and 10 October 2022; higher residential rates from 1 April 2018, 22 December 2020 and 11 December 2024; non-residential rates from 1 April 2018 and 22 December 2020): https://www.gov.wales/land-transaction-tax-rates-and-bands - Welsh Government, "Higher rates of Land Transaction Tax: overview" (a residential property worth £40,000 or more): https://www.gov.wales/higher-rates-land-transaction-tax-overview - Welsh Government, "Land Transaction Tax return guidance: about the calculation" (amounts rounded down to the nearest pound): https://www.gov.wales/land-transaction-tax-return-guidance-about-calculation-all-transactions-html

Files

PathBytes
README.md7,912
data/bands.json28,076
data/surcharges.json2,029
impl/python.py6,763
impl/rust.rs9,325
impl/typescript.ts6,732
vectors.json49,502