Every UK rental property must have a valid Energy Performance Certificate (EPC) rating E or above. Properties with F or G ratings can't be legally let, and certificates expire after ten years. For letting agencies managing hundreds of properties, tracking EPC status manually creates compliance risk: expired certificates go unnoticed, rating requirements aren't verified, and portal listings lack mandatory energy efficiency information.
Week 38 integrated LetAdmin directly with the UK Government's EPC database, transforming EPC management from manual document handling to automated data retrieval with built-in compliance checking. This article explores how we built this integration whilst handling the complexities of address matching and data quality in government datasets.
What Your Team Will Notice
Finding a property's EPC becomes instant. Open the EPC modal, enter the property's postcode, and see all registered certificates at that address—no more requesting PDFs from landlords or searching government websites manually. Click the relevant certificate and LetAdmin imports full details: current rating, potential rating, energy efficiency scores, environmental impact ratings, certificate validity dates, and a direct link to the official government certificate.
The system highlights compliance status automatically. Properties with expired certificates show warning indicators. Those rated F or G display alerts that they can't be legally let. EPCs approaching expiry (within six months) prompt proactive renewal reminders, preventing last-minute scrambles when certificates lapse.
When advertising properties, EPC data synchronises to Rightmove automatically. The current rating, energy efficiency score, and environmental impact rating appear on portal listings without manual data entry. If certificates update or expire, portal listings reflect changes immediately through webhook-triggered synchronisation.
For properties lacking government EPC records (new builds awaiting assessment, recent renovations), staff can manually upload certificate PDFs with required details. The system calculates ratings automatically from energy scores, computes expiry dates (ten years minus one day from issue date), and validates data completeness before saving.
Under the Bonnet: Government API Integration
The UK Government provides EPC data through a public API, but the data quality and structure present challenges. Properties might have multiple historical certificates, addresses vary in format between EPC records and property databases, and not all certificate data is complete or current.
Address-Based Certificate Lookup
EPC lookup uses postcode as the primary search key:
class EpcLookupService
def initialize(postcode)
@postcode = normalise_postcode(postcode)
end
def find_certificates
# Query government API with postcode
# Returns array of all certificates at that address
# Includes current and historical EPCs
end
private
def normalise_postcode(postcode)
# Removes spaces, uppercases, handles format variations
# "WR1 2AB" → "WR12AB"
postcode.to_s.gsub(/\s+/, '').upcase
end
end
The API returns all certificates registered at addresses matching the postcode, which might include multiple properties (flats in a building, separate units at the same location). The lookup interface displays all results, letting staff select the correct certificate for the specific property.
Confidence Scoring for Address Matching
Since properties might not have exact address matches between LetAdmin's records and EPC database entries, we implement confidence scoring:
def calculate_match_confidence(property_address, epc_address)
score = 0
# Check postcode match (required)
return 0 unless postcodes_match?(property_address, epc_address)
score += 30
# Check building number match
if building_numbers_match?(property_address, epc_address)
score += 40
end
# Check street name similarity
street_similarity = calculate_street_similarity(property_address, epc_address)
score += (street_similarity * 30)
score.round
end
Certificates scoring above 70% confidence display prominently with "High Confidence Match" indicators. Lower-scoring matches show with caution indicators, prompting staff to verify details before importing.
Data Import and Normalisation
EPC certificates contain extensive data, but not all fields are consistently populated. Our import process handles this variability:
class EnergyPerformanceCertificate
def self.import_from_government_api(certificate_data)
# Extract core fields with fallback handling
current_rating = extract_rating(certificate_data, 'current-energy-rating')
potential_rating = extract_rating(certificate_data, 'potential-energy-rating')
# Parse dates with format handling
issue_date = parse_date(certificate_data['lodgement-date'])
expiry_date = calculate_expiry(issue_date)
# Import energy scores
current_score = certificate_data['current-energy-efficiency'].to_i
potential_score = certificate_data['potential-energy-efficiency'].to_i
# Environmental impact ratings (carbon emissions)
current_co2 = certificate_data['current-environmental-impact-rating']
potential_co2 = certificate_data['potential-environmental-impact-rating']
create!(
certificate_number: certificate_data['lmk-key'],
current_energy_rating: current_rating,
potential_energy_rating: potential_rating,
current_energy_efficiency: current_score,
potential_energy_efficiency: potential_score,
issue_date: issue_date,
expiry_date: expiry_date,
certificate_url: build_certificate_url(certificate_data['lmk-key'])
)
end
def self.calculate_expiry(issue_date)
# EPCs valid for 10 years minus 1 day
issue_date + 10.years - 1.day
end
end
This import approach ensures we capture all available data whilst gracefully handling missing or malformed fields.
Rating Calculation from Scores
Government EPC data includes numeric scores (1-100) and letter ratings (A-G). Sometimes ratings are missing but scores are present. We implement automatic rating calculation:
def calculate_rating_from_score(score)
return nil if score.blank?
case score.to_i
when 92..100 then 'A'
when 81..91 then 'B'
when 69..80 then 'C'
when 55..68 then 'D'
when 39..54 then 'E'
when 21..38 then 'F'
when 1..20 then 'G'
else nil
end
end
This ensures consistent rating display even when government data is incomplete.
Manual Upload Workflow
For properties not in the government database, staff can upload certificate PDFs with manual data entry:
<!-- Manual EPC upload form -->
<form action="/properties/<%= @property.id %>/epc" method="post" enctype="multipart/form-data">
<!-- Certificate PDF upload -->
<input type="file" name="certificate_pdf" accept="application/pdf" required />
<!-- Certificate number -->
<input type="text" name="certificate_number"
pattern="[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}" />
<!-- Energy scores (auto-calculate ratings) -->
<input type="number" name="current_energy_efficiency" min="1" max="100" />
<input type="number" name="potential_energy_efficiency" min="1" max="100" />
<!-- Dates (auto-calculate expiry) -->
<input type="date" name="issue_date" />
<p>Expiry: <span id="calculated-expiry"></span></p>
</form>
<script>
// Auto-calculate expiry date as issue_date + 10 years - 1 day
document.querySelector('[name="issue_date"]').addEventListener('change', (e) => {
const issueDate = new Date(e.target.value);
const expiryDate = new Date(issueDate);
expiryDate.setFullYear(expiryDate.getFullYear() + 10);
expiryDate.setDate(expiryDate.getDate() - 1);
document.getElementById('calculated-expiry').textContent =
expiryDate.toLocaleDateString('en-GB');
});
// Auto-calculate rating from energy efficiency score
document.querySelector('[name="current_energy_efficiency"]').addEventListener('input', (e) => {
const score = parseInt(e.target.value);
const rating = calculateRating(score);
document.getElementById('current-rating').textContent = rating;
});
</script>
This manual workflow matches the automatic import structure, ensuring consistent data regardless of source.
Compliance Tracking and Alerts
With EPC data in the system, compliance tracking becomes automated:
class Property
def epc_valid?
return false unless current_epc.present?
current_epc.expiry_date >= Date.today
end
def epc_allows_letting?
return false unless current_epc.present?
return false unless epc_valid?
# Ratings E, D, C, B, A are acceptable
# Ratings F and G cannot be legally let
%w[A B C D E].include?(current_epc.current_energy_rating)
end
def epc_expiring_soon?
return false unless current_epc.present?
return false if current_epc.expired?
# Alert if expiry within 6 months
current_epc.expiry_date < 6.months.from_now
end
end
These methods power dashboard alerts and status indicators, ensuring agencies never miss compliance requirements.
Webhook Integration for Portal Sync
When EPC data updates, webhooks notify external systems automatically:
class EnergyPerformanceCertificate
include Webhookable
WEBHOOK_ATTRIBUTES = %w[
certificate_number current_energy_rating potential_energy_rating
current_energy_efficiency issue_date expiry_date
].freeze
after_commit :trigger_property_epc_webhook, on: [:create, :update]
private
def trigger_property_epc_webhook
property.trigger_webhooks('property.epc.updated')
end
end
Rightmove and other portals subscribed to EPC webhooks receive updates immediately, keeping listing data current without manual intervention.
Testing EPC Integration
Comprehensive tests verify government API integration and data handling:
RSpec.describe EpcLookupService do
it "retrieves certificates for valid postcodes" do
service = described_class.new('WR1 2AB')
certificates = service.find_certificates
expect(certificates).not_to be_empty
expect(certificates.first).to have_key('current-energy-rating')
end
it "handles postcodes with no registered certificates" do
service = described_class.new('XX99 9XX')
expect(service.find_certificates).to eq([])
end
it "calculates address match confidence accurately" do
property_address = '123 High Street, Worcester, WR1 2AB'
epc_address = '123 HIGH STREET, WORCESTER, WR1 2AB'
confidence = service.calculate_match_confidence(property_address, epc_address)
expect(confidence).to be > 90
end
end
RSpec.describe EnergyPerformanceCertificate do
it "calculates ratings from energy scores correctly" do
epc = build(:epc, current_energy_efficiency: 75)
expect(epc.calculate_current_rating).to eq('C')
end
it "calculates expiry dates as issue + 10 years - 1 day" do
issue = Date.new(2025, 1, 1)
epc = create(:epc, issue_date: issue)
expect(epc.expiry_date).to eq(Date.new(2034, 12, 31))
end
it "identifies properties that cannot be legally let" do
property = create(:property)
create(:epc, property: property, current_energy_rating: 'F')
expect(property.epc_allows_letting?).to be false
end
end
These tests ensure compliance logic and API integration work correctly under various scenarios.
What's Next
The EPC foundation enables several advanced features: automatic expiry reminders sent to landlords 90 days before certificates lapse, EPC rating trends showing improvements over time as properties are upgraded, and energy efficiency recommendations pulled from government data to suggest practical improvements.
Integration with property advertising workflows prevents accidentally listing properties with invalid or F/G-rated EPCs, catching compliance issues before they reach portals. Bulk EPC lookup for entire portfolios identifies all properties needing attention, making compliance audits straightforward.
By connecting directly to authoritative government data, LetAdmin transforms EPC management from reactive paperwork handling to proactive compliance automation, ensuring every property meets legal requirements whilst minimising administrative burden.