payroll.statutory-sick-pay
UK Statutory Sick Pay for sickness spells: waiting days, 8-week linking, 28-week limit and the rate by date.
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 19 tests, run in TypeScript, Python and Rust.
Not professional advice. This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal 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 payroll specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
Statutory Sick Pay (SSP) due from one employer for a run of sickness spells. Pass every spell of sickness in date order (earlier ones matter: they can link to later ones), the employee's qualifying days as ISO weekdays, and their normal weekly earnings. The answer is split into SSP weeks, which run Sunday to Saturday, because that is how the daily amount is set: the weekly rate divided by the number of qualifying days in the week.
## The rules, and when they changed
For example
statutory_sick_pay(periods ×1, 1, 2, 3, 4, 5, £500.00)→ total £166.25, paid days 7, waiting days 3, weeks ×2, entitlement exhausted false two working weeks off in 2025-26: three waiting days, then the daily ratestatutory_sick_pay(periods ×1, 1, 2, 3, 4, 5, £500.00)→ total £0.00, paid days 0, waiting days 0, weeks , entitlement exhausted false a three-day spell before April 2026 is not a period of incapacity and pays nothingstatutory_sick_pay(periods ×1, 1, 2, 3, 4, 5, £500.00)→ total £73.95, paid days 3, waiting days 0, weeks ×1, entitlement exhausted false the same three-day spell after the 2026 reforms is paid from day one
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 statutory_sick_pay(periods: Sequence[SicknessPeriod], qualifying_weekdays: Sequence[int], normal_weekly_earnings: Money) -> SickPay
| periods | SicknessPeriod[] | every spell of sickness with this employer, oldest first, including earlier spells that may link |
| qualifying_weekdays | int[] | ISO weekdays the employee is required to work, 1 = Monday ... 7 = Sunday |
| normal_weekly_earnings | Money | average weekly earnings over the 8-week relevant period, worked out by the caller |
| returns | SickPay |
The types it declares, generated into your project
@dataclass(frozen=True)
class SicknessPeriod:
"""A spell of days of incapacity for work, both ends inclusive."""
start: str
end: str
@dataclass(frozen=True)
class SickPayWeek:
"""What is due for one SSP week (Sunday to Saturday) under one set of rules."""
#: the Sunday the week begins
week_start: str
#: qualifying days paid in this week
paid_days: int
#: qualifying days served as waiting days in this week
waiting_days: int
amount: Money
@dataclass(frozen=True)
class SickPay:
total: Money
paid_days: int
waiting_days: int
#: in date order; a week spanning a rate change has one line per rule
weeks: List[SickPayWeek]
#: some qualifying day went unpaid because 28 weeks or 3 years were used up
entitlement_exhausted: bool
Your code names it in one line, in the file that uses it
from fune.payroll.statutory_sick_pay import statutory_sick_pay # payroll.statutory-sick-pay@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import List, Optional, Sequence
from .dates_add_days import add_days, days_between, parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_day_of_week import day_of_week ← from dates.day-of-week ^1.0.0 · built alongside by fune
from .math_round_div import round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
from .payroll_statutory_sick_pay_data import SSP_RULES, SSP_RULES_HISTORY, SspRule ← this capability’s own data, compiled from data/ssp-rules.json into the same file by fune build
from .payroll_statutory_sick_pay_types import SicknessPeriod, SickPay, SickPayWeek
# Periods of incapacity separated by no more than 8 weeks are one period (SSCBA 1992 s.152(3)).
LINK_GAP_DAYS = 56
# 28 weeks' entitlement, counted in 1/420ths of a week so any 1-7 qualifying-day week divides exactly.
WEEK_UNITS = 420
MAX_UNITS = 28 * WEEK_UNITS
def _rule_for(date: str) -> int:
for i, rule in enumerate(SSP_RULES):
if date >= rule.valid_from and (rule.valid_to is None or date <= rule.valid_to):
return i
if SSP_RULES_HISTORY != "full":
earliest = min((r.valid_from for r in SSP_RULES), default=None)
if earliest is not None and date < earliest:
raise ValueError(
"no SSP rule for %s: this build was installed with history=%s, so it only carries rules from %s. "
"Reinstall with history=full for older sickness." % (date, SSP_RULES_HISTORY, earliest)
)
raise ValueError("no SSP rule for %s" % (date,))
def statutory_sick_pay(periods: Sequence[SicknessPeriod], qualifying_weekdays: Sequence[int], normal_weekly_earnings: Money) -> SickPay:
"""Statutory Sick Pay due for a run of sickness spells.
Every day is judged by the rules in force on that day, which is how the
6 April 2026 reforms land: a spell straddling that date serves waiting days
before it and is paid from the first qualifying day after it.
"""
if len(qualifying_weekdays) == 0:
raise ValueError("there must be at least one qualifying day a week")
qualifying = set()
for d in qualifying_weekdays:
if isinstance(d, bool) or not isinstance(d, int) or d < 1 or d > 7:
raise ValueError("qualifying weekdays must be 1 (Monday) to 7 (Sunday), received %s" % (d,))
if d in qualifying:
raise ValueError("qualifying weekday %s is listed twice" % (d,))
qualifying.add(d)
per_week = len(qualifying)
if normal_weekly_earnings.currency != "GBP":
raise ValueError("SSP is paid in GBP, received %s" % (normal_weekly_earnings.currency,))
if normal_weekly_earnings.minor < 0:
raise ValueError("normalWeeklyEarnings must not be negative")
lines: List[dict] = []
def line_for(week_start: str, rule: int) -> dict:
for line in lines:
if line["week_start"] == week_start and line["rule"] == rule:
return line
line = {"week_start": week_start, "rule": rule, "paid": 0, "waiting": 0}
lines.append(line)
return line
previous_end: Optional[str] = None
last_piw_end: Optional[str] = None
series_end = ""
waiting_served = 0
units_used = 0
exhausted = False
for period in periods:
parse_iso_date(period.start)
parse_iso_date(period.end)
if period.end < period.start:
raise ValueError("sickness period %s to %s ends before it starts" % (period.start, period.end))
if previous_end is not None and period.start <= previous_end:
raise ValueError("sickness periods must be in date order and must not overlap")
previous_end = period.end
# Too short to be a period of incapacity under the rules of its first
# day: it is not paid and does not link anything.
length = days_between(period.start, period.end) + 1
if length < SSP_RULES[_rule_for(period.start)].minimum_period_days:
continue
if last_piw_end is None or days_between(last_piw_end, period.start) - 1 > LINK_GAP_DAYS:
# A new period of entitlement: waiting days and the 28 weeks start
# again, and it ends 3 years on at the latest (SSP (General) Regs 1982 reg 3(3)).
waiting_served = 0
units_used = 0
series_end = add_months(period.start, 36)
last_piw_end = period.end
day = period.start
while day <= period.end:
weekday = day_of_week(day)
current = day
day = add_days(day, 1)
if weekday not in qualifying:
continue
index = _rule_for(current)
rule = SSP_RULES[index]
if rule.lower_earnings_limit is not None and normal_weekly_earnings.minor < rule.lower_earnings_limit:
continue
week_start = add_days(current, -(weekday % 7))
if waiting_served < rule.waiting_days:
waiting_served += 1
line_for(week_start, index)["waiting"] += 1
continue
units = WEEK_UNITS // per_week
if current >= series_end or units_used + units > MAX_UNITS:
exhausted = True
continue
units_used += units
line_for(week_start, index)["paid"] += 1
lines.sort(key=lambda l: (l["week_start"], l["rule"]))
weeks = [
SickPayWeek(
week_start=l["week_start"],
paid_days=l["paid"],
waiting_days=l["waiting"],
amount=money(_week_amount(SSP_RULES[l["rule"]], normal_weekly_earnings.minor, l["paid"], per_week), "GBP"),
)
for l in lines
]
return SickPay(
total=money(sum(w.amount.minor for w in weeks), "GBP"),
paid_days=sum(w.paid_days for w in weeks),
waiting_days=sum(w.waiting_days for w in weeks),
weeks=weeks,
entitlement_exhausted=exhausted,
)
def _week_amount(rule: SspRule, earnings: int, days: int, per_week: int) -> int:
"""SSP for some days of one week: the weekly rate times days over
qualifying days in the week, rounded up to the penny, as HMRC's daily-rate
tables do. The weekly rate is kept exact (in ten-thousandths of a penny) so
that 80% of earnings is never rounded twice."""
rate = rule.weekly_rate * 10000
if rule.earnings_cap_basis_points is not None:
rate = min(rate, earnings * rule.earnings_cap_basis_points)
return round_div(rate * days, 10000 * per_week, "up")Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 5 dependencies, 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 payroll.statutory-sick-pay
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./payroll.statutory-sick-pay-1.0.0-python.fune, or fetch it from a terminal with fune pull payroll.statutory-sick-pay@1.0.0:python.
The whole function, every language, is one file too: payroll.statutory-sick-pay-1.0.0.fune, 50,582 bytes, sha256 8fbcbce9756114686bbd536f224af76c8e031da76ca1d2887d04f1a20b3e8c4b. 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 payroll.statutory-sick-pay
after — your function gets the result and the arguments, and returns the final result.
# fune: after payroll.statutory-sick-pay
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 dates.add-days in payroll.statutory-sick-pay
# fune: replace dates.add-months in payroll.statutory-sick-pay
# fune: replace dates.day-of-week in payroll.statutory-sick-pay
# fune: replace math.round-div in payroll.statutory-sick-pay
# fune: replace money.amount in payroll.statutory-sick-pay
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 payroll.statutory-sick-pay --steps.
# fune: step payroll.statutory-sick-pay 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 | |
|---|---|---|---|
| two working weeks off in 2025-26: three waiting days, then the daily rate | periods ×1, 1, 2, 3, 4, 5, £500.00 | → | total £166.25, paid days 7, waiting days 3, weeks ×2, entitlement exhausted false |
| a three-day spell before April 2026 is not a period of incapacity and pays nothing | periods ×1, 1, 2, 3, 4, 5, £500.00 | → | total £0.00, paid days 0, waiting days 0, weeks , entitlement exhausted false |
| the same three-day spell after the 2026 reforms is paid from day one | periods ×1, 1, 2, 3, 4, 5, £500.00 | → | total £73.95, paid days 3, waiting days 0, weeks ×1, entitlement exhausted false |
| from April 2026 low earners get 80% of earnings, kept exact and rounded up once | periods ×1, 1, 2, 3, 4, 5, £123.33 | → | total £19.74, paid days 1, waiting days 0, weeks ×1, entitlement exhausted false |
| earnings below the lower earnings limit before April 2026 earn no SSP | periods ×1, 1, 2, 3, 4, 5, £124.99 | → | total £0.00, paid days 0, waiting days 0, weeks , entitlement exhausted false |
| a spell 52 days after the last links to it and serves no new waiting days | periods ×2, 1, 2, 3, 4, 5, £500.00 | → | total £118.75, paid days 5, waiting days 3, weeks ×2, entitlement exhausted false |
| a gap of exactly 8 weeks still links | periods ×2, 1, 2, 3, 4, 5, £500.00 | → | total £71.25, paid days 3, waiting days 3, weeks ×3, entitlement exhausted false |
| a gap of 59 days does not link, so waiting days are served again | periods ×2, 1, 2, 3, 4, 5, £500.00 | → | total £47.50, paid days 2, waiting days 6, weeks ×2, entitlement exhausted false |
| a short spell between two linked ones is ignored before 2026 | periods ×3, 1, 2, 3, 4, 5, £500.00 | → | total £118.75, paid days 5, waiting days 3, weeks ×2, entitlement exhausted false |
| a spell across 6 April 2026 serves two old waiting days, then pays at the new rate | periods ×1, 1, 2, 3, 4, 5, £500.00 | → | total £123.25, paid days 5, waiting days 2, weeks ×2, entitlement exhausted false |
Show the other 9 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| seven qualifying days a week: one day is a seventh of the week, rounded up | periods ×1, 1, 2, 3, 4, 5, 6, 7, £500.00 | → | total £66.72, paid days 4, waiting days 3, weeks ×2, entitlement exhausted false |
| 2023-24 rate on a Tuesday-and-Thursday pattern: waiting days run into the second week | periods ×1, 2, 4, £400.00 | → | total £164.10, paid days 3, waiting days 3, weeks ×3, entitlement exhausted false |
| 28 weeks is the most SSP a period of entitlement pays | periods ×1, 1, 2, 3, 4, 5, £500.00 | → | total £3,451.00, paid days 140, waiting days 0, weeks ×28, entitlement exhausted true |
| no qualifying days is an error | periods ×1, , £500.00 | → | error: at least one qualifying day |
| a weekday outside 1-7 is an error | periods ×1, 0, 1, £500.00 | → | error: qualifying weekdays must be 1 (Monday) to 7 (Sunday) |
| a period that ends before it starts is an error | periods ×1, 1, 2, 3, 4, 5, £500.00 | → | error: ends before it starts |
| overlapping periods are an error | periods ×2, 1, 2, 3, 4, 5, £500.00 | → | error: must be in date order and must not overlap |
| sickness before the rules this capability carries is an error | periods ×1, 1, 2, 3, 4, 5, £500.00 | → | error: no SSP rule for 2020-06-01 |
| negative earnings are an error | periods ×1, 1, 2, 3, 4, 5, -£0.01 | → | error: must not be negative |
More from the author
Every day is judged by the rule row in force on that day (`data/ssp-rules.json`).
Up to 5 April 2026: - A spell is only a **period of incapacity for work** (PIW) if it lasts at least 4 consecutive days, counting every calendar day. Shorter spells pay nothing and link nothing. - The first **3 qualifying days** of a period of entitlement are waiting days and are not paid. - Earnings below the **lower earnings limit** (£123 in 2023-24 and 2024-25, £125 in 2025-26) mean no SSP at all. - The weekly rate is a flat £109.40 (2023-24), £116.75 (2024-25), £118.75 (2025-26).
From 6 April 2026 (Employment Rights Act 2025 ss.10 and 11, commenced by SI 2026/373 reg 2): - A PIW is **one day or more**; there are **no waiting days**; there is **no lower earnings limit**. - The weekly rate is the **lower of £123.25 and 80% of normal weekly earnings**. The 80% figure is kept exact and only the day amount is rounded, so a £123.33 earner gets £98.664 a week and £19.74 for one day of a five-day week.
Throughout: - PIWs separated by **8 weeks (56 days) or less** link into one (SSCBA 1992 s.152(3)); a gap of 57 days starts a new period of entitlement with fresh waiting days. "Separated by" counts the days in between: a spell ending on 5 June links to one starting on 1 August but not 2 August. - At most **28 weeks** of SSP per period of entitlement (s.155), counted as weeks of qualifying days, and a linked period of entitlement ends after **3 years** (SSP (General) Regulations 1982 reg 3(3)). `entitlementExhausted` says a qualifying day went unpaid for either reason. - The amount for k paid days in a week of q qualifying days is `ceil(weekly rate x k / q)` to the penny. That reproduces HMRC's published daily tables exactly (for example £47.50 for 2 of 5 days at £118.75, £16.68 for 1 of 7 days at £116.75).
## Spells across 6 April 2026
SI 2026/373 regs 3 to 5 have transitional rules. This capability models them only as far as "each day uses the rule in force on that day": waiting days served on or before 5 April stay waiting days, and qualifying days from 6 April are paid (which is what reg 3 provides). Reg 4 (employees earning £125 to £154.05 whose entitlement began before 6 April keep the flat £123.25 rather than 80%) and reg 5 (a period that did not qualify because of the LEL) are **not** modelled: the caller must handle those cases.
## What the caller must supply or decide
- Normal weekly earnings, worked out over the 8-week relevant period (SSP (General) Regulations 1982 reg 19). One figure is used for every spell. - The qualifying days: one fixed weekly pattern (at least one a week, s.154(3)). - Whether the employee qualifies at all in other respects (maternity disqualification, custody, contract ended, 12-week ESA link, agency rules): none of that is checked here. - A week in which the rule changes gets one line per rule, so a rate rise on a Monday gives the Sunday line under the old rule and the rest under the new.
A build installed with `history=current` keeps only the rule still in force and refuses earlier dates rather than paying them at today's rate.
## Sources
- HMRC, "Rates and thresholds for employers" 2023 to 2024, 2024 to 2025, 2025 to 2026 and 2026 to 2027 (SSP weekly rates, LELs and the daily-rate tables): https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2023-to-2024, https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2024-to-2025, https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2025-to-2026, https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2026-to-2027 - GOV.UK, "Statutory Sick Pay (SSP): employer guide" (£123.25 or 80% of AWE; linked periods 8 weeks or less apart; paid from the first full working day): https://www.gov.uk/employers-sick-pay - Social Security Contributions and Benefits Act 1992 ss.152-155 and 157 as amended: https://www.legislation.gov.uk/ukpga/1992/4/section/152, https://www.legislation.gov.uk/ukpga/1992/4/section/155 - Employment Rights Act 2025 ss.10 and 11: https://www.legislation.gov.uk/ukpga/2025/36/section/10, https://www.legislation.gov.uk/ukpga/2025/36/section/11 - The Employment Rights Act 2025 (Commencement No. 3 and Transitional Provisions) Regulations 2026 (SI 2026/373): https://www.legislation.gov.uk/uksi/2026/373/made - The Social Security Benefits Up-rating Order 2026 (SI 2026/148) art. 8 (£123.25): https://www.legislation.gov.uk/uksi/2026/148/made - Statutory Sick Pay (General) Regulations 1982 regs 3(3) and 19: https://www.legislation.gov.uk/uksi/1982/894/regulation/3
Files
| Path | Bytes |
|---|---|
| README.md | 5,134 |
| data/ssp-rules.json | 1,166 |
| impl/python.py | 6,359 |
| impl/rust.rs | 8,616 |
| impl/typescript.ts | 6,270 |
| vectors.json | 15,544 |