insurance.ipt
UK Insurance Premium Tax at the standard or higher rate in force on a date, added to or taken out of a premium.
1.0.1 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 23 tests, run in TypeScript, Python and Rust.
Not professional advice. This capability calculates insurance figures from published rules. It is a software component for developers, not 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 an actuary review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
UK Insurance Premium Tax (IPT) on a premium, at the rate in force on a date. Give it a net premium and it adds the tax; give it a gross (IPT-inclusive) premium with `includesTax` and it takes the tax out. Either way you get net, tax and gross together, plus the rate and the day that rate came in.
## Rates are data
For example
insurancePremiumTax(£100.00, false, standard, 2026-09-23, half-up)→ net £100.00, tax £12.00, gross £112.00, rate 12%, band standard, valid from 2017-06-01 standard rate today: 12% on a 100.00 net premiuminsurancePremiumTax(£112.00, true, standard, 2026-09-23, half-up)→ net £100.00, tax £12.00, gross £112.00, rate 12%, band standard, valid from 2017-06-01 taking 12% out of 112.00 gives 12.00 of tax, not 13.44insurancePremiumTax(£50.00, false, higher, 2026-09-23, half-up)→ net £50.00, tax £10.00, gross £60.00, rate 20%, band higher, valid from 2011-01-04 higher rate today: 20% on a 50.00 travel premium
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 insurancePremiumTax(amount: Money, includesTax: boolean, band: IptBand, onDate: string, mode: RoundingMode): IptBreakdown
| amount | Money | the premium: net of IPT, or gross when includesTax is true; negative for a return premium |
| includesTax | bool | true to take the tax out of a gross premium, false to add it to a net one |
| band | IptBand | standard, or higher for travel, mechanical and electrical appliance and some vehicle insurance |
| onDate | date | the date the insurer receives the premium, which fixes the rate |
| mode | RoundingMode | how the one rounded figure rounds: the tax when adding, the net when taking out |
| returns | IptBreakdown |
The types it declares, generated into your project
export type IptBand = "standard" | "higher";
/** Net premium, tax and gross together, with the rate used. */
export interface IptBreakdown {
readonly net: Money;
readonly tax: Money;
readonly gross: Money;
/** 1200 = 12% */
readonly basisPoints: number;
readonly band: IptBand;
/** the day that rate came in */
readonly validFrom: string;
}
Your code names it in one line, in the file that uses it
import { insurancePremiumTax } from "#fune/insurance.ipt@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type RoundingMode, roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { IPT_RATES, IPT_RATES_HISTORY } from "./insurance_ipt_data.ts"; ← this capability’s own data, compiled from data/ipt-rates.json into the same file by fune build
import { type IptBand, type IptBreakdown } from "./insurance_ipt_types.ts";
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
/**
* Add UK Insurance Premium Tax to a net premium, or take it out of a gross one,
* at the rate in force on the date the insurer receives the premium.
*
* Only one figure is ever rounded - the tax when adding, the net when taking
* out - and the other is the difference, so net + tax is always the gross.
* Taking 12% off a gross premium is the classic error: the tax inside 112.00
* is 12.00, not 13.44.
*/
export function insurancePremiumTax(
amount: Money,
includesTax: boolean,
band: IptBand,
onDate: string,
mode: RoundingMode
): IptBreakdown {
if (band !== "standard" && band !== "higher") {
throw new RangeError(`unknown IPT band "${band}": use standard or higher`);
}
if (typeof onDate !== "string" || !ISO_DATE.test(onDate)) {
throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
}
let rule: (typeof IPT_RATES)[number] | null = null;
for (const r of IPT_RATES) {
if (r.band !== band || onDate < r.validFrom) continue;
if (r.validTo !== null && onDate > r.validTo) continue;
rule = r;
}
if (rule === null) {
// A history=current build keeps only the rates in force; answering an older
// date with today's rate would be exactly the wrong answer, so say so.
if (IPT_RATES_HISTORY !== "full") {
let earliest: string | null = null;
for (const r of IPT_RATES) {
if (r.band === band && (earliest === null || r.validFrom < earliest)) earliest = r.validFrom;
}
if (earliest !== null && onDate < earliest) {
throw new RangeError(
`no IPT rule for ${band} on ${onDate}: this build was installed with history=${IPT_RATES_HISTORY}, ` +
`so it only carries rates from ${earliest}. Reinstall with history=full for older premiums.`
);
}
}
throw new RangeError(`no IPT rule for ${band} on ${onDate}`);
}
const bp = rule.basisPoints;
let net: Money;
let tax: Money;
let gross: Money;
if (includesTax) {
gross = amount;
net = money(roundDiv(amount.minor * 10000, 10000 + bp, mode), amount.currency);
tax = money(amount.minor - net.minor, amount.currency);
} else {
net = amount;
tax = money(roundDiv(amount.minor * bp, 10000, mode), amount.currency);
gross = money(amount.minor + tax.minor, amount.currency);
}
return { net, tax, gross, basisPoints: bp, band, validFrom: rule.validFrom };
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, 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 insurance.ipt
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./insurance.ipt-1.0.1-typescript.fune, or fetch it from a terminal with fune pull insurance.ipt@1.0.1:typescript.
The whole function, every language, is one file too: insurance.ipt-1.0.1.fune, 28,285 bytes, sha256 73e6506693fac418c1882c188e97a4232d869190ddeef1d550c2e58623bd1c22. 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 insurance.ipt
after — your function gets the result and the arguments, and returns the final result.
// fune: after insurance.ipt
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 math.round-div in insurance.ipt
// fune: replace money.amount in insurance.ipt
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 insurance.ipt --steps.
// fune: step insurance.ipt 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 | |
|---|---|---|---|
| standard rate today: 12% on a 100.00 net premium | £100.00, false, standard, 2026-09-23, half-up | → | net £100.00, tax £12.00, gross £112.00, rate 12%, band standard, valid from 2017-06-01 |
| taking 12% out of 112.00 gives 12.00 of tax, not 13.44 | £112.00, true, standard, 2026-09-23, half-up | → | net £100.00, tax £12.00, gross £112.00, rate 12%, band standard, valid from 2017-06-01 |
| higher rate today: 20% on a 50.00 travel premium | £50.00, false, higher, 2026-09-23, half-up | → | net £50.00, tax £10.00, gross £60.00, rate 20%, band higher, valid from 2011-01-04 |
| first day of 12%: net of 250.00 gross is 223.21 (22321.43 rounds down) | £250.00, true, standard, 2017-06-01, half-up | → | net £223.21, tax £26.79, gross £250.00, rate 12%, band standard, valid from 2017-06-01 |
| last day of 10%: net of 250.00 gross is 227.27 | £250.00, true, standard, 2017-05-31, half-up | → | net £227.27, tax £22.73, gross £250.00, rate 10%, band standard, valid from 2016-10-01 |
| 9.5% on 199.99: 18.99905 rounds half-up to 19.00 | £199.99, false, standard, 2016-01-01, half-up | → | net £199.99, tax £19.00, gross £218.99, rate 9.5%, band standard, valid from 2015-11-01 |
| 9.5% on 199.99 rounded down is 18.99 | £199.99, false, standard, 2016-01-01, down | → | net £199.99, tax £18.99, gross £218.98, rate 9.5%, band standard, valid from 2015-11-01 |
| higher rate on its last day at 17.5% | £80.00, false, higher, 2011-01-03, half-up | → | net £80.00, tax £14.00, gross £94.00, rate 17.5%, band higher, valid from 1997-04-01 |
| higher rate on the first day of 20% | £80.00, false, higher, 2011-01-04, half-up | → | net £80.00, tax £16.00, gross £96.00, rate 20%, band higher, valid from 2011-01-04 |
| standard 6% from 4 January 2011: 33.33 gives 2.00 (1.9998) | £33.33, false, standard, 2011-01-04, half-up | → | net £33.33, tax £2.00, gross £35.33, rate 6%, band standard, valid from 2011-01-04 |
Show the other 13 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| standard 5% on the day before 6% | £105.00, true, standard, 2011-01-03, half-up | → | net £100.00, tax £5.00, gross £105.00, rate 5%, band standard, valid from 1999-07-01 |
| introductory 2.5% in 1995 | £100.00, false, standard, 1995-06-01, half-up | → | net £100.00, tax £2.50, gross £102.50, rate 2.5%, band standard, valid from 1994-10-01 |
| 4% in 1998 | £100.00, false, standard, 1998-01-01, half-up | → | net £100.00, tax £4.00, gross £104.00, rate 4%, band standard, valid from 1997-04-01 |
| the first day IPT existed | £40.00, false, standard, 1994-10-01, half-up | → | net £40.00, tax £1.00, gross £41.00, rate 2.5%, band standard, valid from 1994-10-01 |
| a return premium carries its IPT back: -33.33 at 12% is -4.00 of tax | -£33.33, false, standard, 2026-09-23, half-up | → | net -£33.33, tax -£4.00, gross -£37.33, rate 12%, band standard, valid from 2017-06-01 |
| gross 1.00 at 12%: net 0.89, tax 0.11 | £1.00, true, standard, 2026-09-23, half-up | → | net £0.89, tax £0.11, gross £1.00, rate 12%, band standard, valid from 2017-06-01 |
| a zero premium has no tax | £0.00, false, higher, 2026-09-23, half-up | → | net £0.00, tax £0.00, gross £0.00, rate 20%, band higher, valid from 2011-01-04 |
| no higher rate before 1 April 1997 | £100.00, false, higher, 1996-01-01, half-up | → | error: no IPT rule for higher on 1996-01-01 |
| no IPT before 1 October 1994 | £100.00, false, standard, 1994-09-30, half-up | → | error: no IPT rule for standard on 1994-09-30 |
| a malformed date is refused | £100.00, false, standard, 23/09/2026, half-up | → | error: onDate must be an ISO date |
| an unknown band is refused | £100.00, false, reduced, 2026-09-23, half-up | → | error: unknown IPT band |
| a date with a trailing newline is refused | £100.00, false, standard, 2026-09-23 , half-up | → | error: onDate must be an ISO date |
| a date in Arabic-Indic digits is refused | £100.00, false, standard, ٢٠٢٦-09-23, half-up | → | error: onDate must be an ISO date |
More from the author
The rates are dated rows in `data/ipt-rates.json`, never constants in the code, and the function takes the date. There are two bands:
- **standard**: most general insurance (motor, home, pet, commercial). - **higher**: 20% on travel insurance, insurance sold with mechanical or electrical appliances, and some vehicle insurance sold through a supplier of the vehicle (Finance Act 1994, s.51A; see Notice IPT1 for the detail). Which band a policy falls in is the caller's decision.
| from | standard | higher | |---|---|---| | 1 October 1994 | 2.5% | - | | 1 April 1997 | 4% | 17.5% | | 1 July 1999 | 5% | 17.5% | | 4 January 2011 | 6% | 20% | | 1 November 2015 | 9.5% | 20% | | 1 October 2016 | 10% | 20% | | 1 June 2017 | 12% | 20% |
A date before 1 October 1994 (or a higher-rate date before 1 April 1997) is an error, not zero.
## Which date
The rate is fixed by the date the insurer *receives* the premium (or, under the special accounting scheme, the date it is written), not the policy start date. Around each rate change there were anti-forestalling rules for premiums received early for cover starting after the change (HMRC IPT07855); applying those is the caller's job, by passing the date the rules say to use.
## Rounding
Only one figure is rounded, with the caller's mode: the tax when adding, the net when taking out (net = gross x 10000 / (10000 + rate)). The other figure is the difference, so net + tax is exactly the gross. IPT is charged on the whole premium the insurer receives, including administration charges and any charge for paying by instalments (HMRC IPT03400), so apply it after those are added.
Negative premiums (return premiums) are allowed and carry their IPT back.
A build installed with `history=current` keeps only the rates in force and refuses an older date with an explanation rather than answering it at today's rate.
## Sources
- HMRC, "Historical Insurance Premium Tax rates" (IPT bulletin), https://www.gov.uk/government/statistics/insurance-premium-tax-ipt-bulletin/historical-insurance-premium-tax-rates--2 (read 2026-09-23): every date and rate in the table above. - HMRC, "Insurance Premium Tax rates", https://www.gov.uk/government/publications/rates-and-allowances-insurance-premium-tax/insurance-premium-tax-rates (read 2026-09-23): the periods from 3 January 2011 onwards, agreeing. - HMRC Internal Manual IPT03400, "Overview and the law: the rate of tax", https://www.gov.uk/hmrc-internal-manuals/insurance-premium-tax/ipt03400 (read 2026-09-23): the full history of both rates, and that tax is due on the gross premium including administration and instalment charges.
1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in onDate; adds tests.
Files
| Path | Bytes |
|---|---|
| README.md | 3,080 |
| data/ipt-rates.json | 1,162 |
| impl/python.py | 2,738 |
| impl/rust.rs | 3,965 |
| impl/typescript.ts | 2,722 |
| vectors.json | 9,069 |