Joining DVF, DPE and BDNB for one address: linking a French sale to its energy diagnosis and its building
France's DVF sales records (DGFiP), ADEME's energy performance diagnoses (DPE, the French equivalent of an EPC) and the CSTB's national building database (BDNB) share no identifier. Normi links them at the level of the sale: every DVF transaction gets a durable identity, then the nearest DPE within 200 metres and the nearest BDNB building within 200 metres. Today 13,578,762 of 18,102,579 priced sales (75%) have a linked DPE, and 17,813,826 DVF rows have a BDNB building, at a median distance of 13.3 m. All of it is queryable through a REST API and an MCP server.
It is a proximity link, not a dwelling-level identity: sound for statistics by energy class or construction period, not for asserting the rating of one particular flat. This page explains why, with a real example.
Published 24 September 2026 · example captured in production the same day · every figure links to its source at the bottom of the page · Lire en français
The problem: three databases, no shared key
Each of the three databases describes a different object, and that is exactly what makes the join hard.
DVF (DGFiP)
Describes a transfer: a date, a price, an address, one or more parcels and units. One sale often produces several rows (a flat, its cellar, its parking space), and the row id is regenerated every time the file is reloaded.
DPE (ADEME)
Describes a dwelling or a building at the date of a diagnosis. A block of flats carries dozens of them, one dwelling accumulates several across sales and lettings, and the method changed in July 2021.
BDNB (CSTB)
Describes a building, or a group of buildings, with its construction year, materials, number of dwellings and storeys. It knows nothing of sales or individual dwellings.
Addresses are not normalised the same way in the three files, the cadastral reference is filled in for only part of the DPEs and is unique only within a commune, and one sale can correspond to several diagnoses. So there is no exact join, only matches of varying reliability, which have to be documented.
How Normi does the join
1. A durable identity for every sale
The DVF row id changes on every reload, so using it as a join key means losing every match at the next reload. Normi computes a stable
transaction_group_idfrom the date, the normalised address and the price. DPE and BDNB matches are stored on that key, and a mapping table (18,119,513 rows) bridges it to the current id exposed by the API.2. Sale → DPE: the nearest diagnosis
For each geocoded sale, excluding bulk sales and price outliers, Normi keeps the nearest ADEME diagnosis in the same département, within 200 metres, issued between 10 years before and 1 year after the sale. One diagnosis per sale, so that a sale in a large building does not count as many times as the building has flats. Floor area and property type are not compared: that is a coverage choice, and its consequence is shown below.
3. Sale → BDNB: the nearest building
Same principle, with no date window: the nearest BDNB building within 200 metres, in the same département. The distance is stored and returned by the API as
match_distance_m. Across the 17,813,826 links, the median distance is 13.3 m and 95% are closer than 49.8 m. The table is only published once département coverage and the share of links that resolve to a sale pass their integrity thresholds.
- Proximity is not identity. In multi-dwelling buildings, the nearest diagnosis is often another flat's, or a neighbouring building's. The example below shows a real case.
- DPEs issued before July 2021 follow the old method, which is not comparable label for label with the current one.
- Alsace-Moselle and Mayotte have no data: DVF does not cover them (0 DVF communes in Moselle, Bas-Rhin, Haut-Rhin and Mayotte).
- The single-sale DPE lookup,
GET /v1/transactions/{id}/dpe, is being fixed: its cadastral-reference search does not yet filter by commune. Until the fix ships, this page does not rely on that call, and we advise against using it for a specific sale.
What the join makes possible
The match serves area-level statistics first, where individual errors cancel out. Four analyses follow directly, each available as a REST endpoint and as an MCP tool.
Green premium by energy class
Median price per square metre and quartiles for each rating from A to G over a postcode or a commune, with the gap to the local median.
GET /v1/stats/dpe-premium
MCP: analyze_dpe_price_premium · 10 credits
Documentation (French)Exposure to energy-inefficient homes
Share of sales rated F and G (passoires thermiques), by property type and year, where the phased rental ban weighs on prices.
GET /v1/dpe/passoires-thermiques
MCP: analyze_dpe_price_and_thermal_risk · 10 credits
Documentation (French)Effect of construction period
Median price per square metre by BDNB construction-year band, compared with the area median.
GET /v1/bdnb/age-stats
MCP: analyze_building_age_price_impact · 10 credits
Documentation (French)Renovation potential
Volume of sales in older buildings, their discount to the area and the dominant wall materials, summarised as a score.
GET /v1/bdnb/renovation-score
MCP: score_renovation_potential · 10 credits
Documentation (French)A real example: one sale in Rennes, three calls
Starting point: rue Gilles Languedoc in Rennes (35000). The responses below were captured in production on 24 September 2026, shortened but not altered. The example starts from a street rather than a house number, and DVF contains no buyer or seller names.
# 1. Every sale on the street → a transaction id (20 credits) curl -H "X-API-Key: $NORMI_API_KEY" \ "https://mcp.normi.fr/v1/property-history?address=rue%20Gilles%20Languedoc&code_postal=35000" # 2. The BDNB building linked to that sale (2 credits) curl -H "X-API-Key: $NORMI_API_KEY" \ "https://mcp.normi.fr/v1/transactions/18109193/bdnb" # 3. Price per m² by DPE class in the postcode (10 credits) curl -H "X-API-Key: $NORMI_API_KEY" \ "https://mcp.normi.fr/v1/stats/dpe-premium?code_postal=35000&type_local=Appartement"
1. The street history gives the sale's id
Twenty-two sales since 2014. The one we follow: a 4-room flat of 86.59 m², sold on 30 December 2025 at €6,045/m². The off-plan (VEFA) sales of 2022 and 2024 on the same street already point to a new development.
{
"address_normalized": "… RUE GILLES LANGUEDOC, 35000 RENNES",
"transactions": [
{
"id": 18109193,
"date": "2025-12-30",
"price_per_m2": 6045,
"surface": 86.59,
"type": "Appartement",
"rooms": 4,
"nature": "Vente"
},
{
"id": 16417807,
"date": "2024-11-29",
"price_per_m2": 5908,
"surface": 76,
"type": "Appartement",
"rooms": 3,
"nature": "Vente en l'état futur d'achèvement"
},
{
"id": 16409675,
"date": "2024-11-28",
"price_per_m2": 6453,
"surface": 86,
"type": "Appartement",
"rooms": 4,
"nature": "Vente en l'état futur d'achèvement"
}
],
"note": "Found 22 transaction(s) at this address"
}2. The BDNB building confirms a 2023 block
{
"building": {
"batiment_groupe_id": "bdnb-bg-7LTJ-8KUT-9D4U",
"annee_construction": 2023,
"nb_logements": 21,
"nb_etages": 6,
"materiaux_murs": "INDETERMINE",
"materiaux_toit": "INDETERMINE",
"usage_principal": "Résidentiel collectif",
"usage_niveau_1": "Résidentiel collectif",
"dvf_nb_mutations": 5,
"dvf_prix_m2_median": 6453,
"match_distance_m": 8
},
"transaction_id": "18109193",
"_credits": {
"used": 2,
"remaining": "…"
}
}Matched at 8 metres: a residential block of 21 dwellings over 6 levels, built in 2023. That is consistent with the off-plan sales in the history. Materials are undetermined: the BDNB does not record them yet for this recent building.
3. Price per square metre by energy class puts the sale in context
| Class | Sales | Median €/m² | Gap to median |
|---|---|---|---|
| A | 372 | 3,295 | +3.2% |
| B | 1,348 | 3,371 | +5.6% |
| C | 3,004 | 3,261 | +2.1% |
| D | 4,768 | 3,032 | -5% |
| E | 3,325 | 3,167 | -0.8% |
| F | 1,188 | 3,361 | +5.3% |
| G | 404 | 3,593 | +12.5% |
At €6,045/m², the sale sits well above every class median in 35000, as expected for a new-build flat. The table also shows why a green premium is never read raw: here, G-rated homes sell above the median per square metre. One plausible explanation, which this table alone cannot establish: in this central postcode, the least efficient homes are often small, old and well located, two factors that push the price per square metre up. Energy class is then entangled with location and size; compare like-for-like type, size and neighbourhood before drawing a conclusion.
The same question over MCP
From Claude, ChatGPT or any MCP client connected to Normi, a single request is enough. The assistant chains lookup_property_history, get_building_characteristics and analyze_dpe_price_premium, for the same 32 credits.
Find the sales on rue Gilles Languedoc in Rennes (35000). For the 4-room flat sold on 30 December 2025, give me the building's characteristics, then compare its price per m² with the median by DPE class for flats in 35000.
To connect the server: install the Normi MCP server.
Corpus figures
These figures are read from a corpus snapshot recomputed after every data load, never calculated on the fly. The DPE figure counts distinct transactions, not match pairs.
Methodology. Measured on 9 September 2026 against the Normi corpus itself. “Priced and typed” means the sale has a recorded price and a resolved property type — this is the population the API's statistics run on, not the raw file's line count. The energy figure counts distinct transactions linked to a diagnosis, not match pairs: each sale gets the nearest diagnosis within 200 metres, issued between ten years before and one year after the sale. It is a proximity link, not proof that the diagnosis covers the dwelling sold.
Sources: DVF (DGFiP), DPE (ADEME), BDNB (CSTB), SIRENE (INSEE), under open licence. None of these values is taken from a third party: each is recomputed on the data Normi has ingested.
- DVF rows linked to a BDNB building
- 17,813,826
- départements covered by the BDNB match
- 93 / 93
- median distance between the sale and the matched building
- 13.3 m
- 95th percentile of that distance
- 49.8 m
FAQ
Frequently asked questions on joining DVF, DPE and BDNB
The questions that come up before wiring the three databases into a tool or an agent.
Is there a shared identifier between DVF, DPE and BDNB?
No. DVF records transfers (a sale, a date, a price, one or more parcels), the DPE records a dwelling or building at the date of a diagnosis, and the BDNB records buildings. None of the three publishes the others' keys. They have to be matched, by cadastral reference, by address or by location, and every method has its errors. Normi links each sale to the nearest diagnosis and the nearest building, and publishes the match distance so users can judge it.
What share of DVF sales has a linked DPE?
Roughly three priced sales in four. The exact figure is read from the corpus snapshot shown on this page: it counts distinct transactions (a COUNT DISTINCT of the transaction key), not match pairs. The remaining sales are those with no ADEME diagnosis filed within 200 metres inside the date window, plus those the match deliberately leaves out: bulk sales, price outliers and sales without coordinates.
Is the DPE linked to a sale necessarily the one for the dwelling sold?
No, and this is the most important limit. The match keeps the nearest diagnosis within 200 metres without comparing floor area or property type. In a block of flats, that is often the diagnosis of another flat in the building, or of a neighbouring building. The link is reliable for per-class statistics over a postcode or a commune, where individual errors cancel out; it does not replace the property's own DPE for a specific sale.
Why is there no data for Alsace-Moselle?
Because DVF itself does not cover Moselle, Bas-Rhin and Haut-Rhin, which use the separate livre foncier land register rather than the DGFiP transfer file, nor Mayotte. With no DVF sale there is nothing to link: no commune in these départements appears in Normi's DVF location catalogue.
How do I get a transaction id to query the BDNB?
From an address history: GET /v1/property-history, or the MCP tool lookup_property_history, returns every sale at an address or on a street, each with its id field. Pass that id to GET /v1/transactions/{id}/bdnb or to the get_building_characteristics tool.
How many credits does this join cost?
The full example on this page costs 32 credits: 20 for the street history, 2 for the BDNB building of one sale, 10 for the price per square metre by DPE class in the postcode. Free accounts receive 500 credits every month, no card required. A call that finds no building is refunded.
Where the figures come from
The figures in the “Corpus figures” block come from the normi_corpus_stats table, with the measurement date shown. The others were measured in production with the queries below. Data sources: DVF (DGFiP), DPE (ADEME), BDNB (CSTB), under open licence.
- 18,119,513 DVF rows carrying a durable identity · measured on 2026-08-28
SELECT count(*) FROM maintenance.dvf_transaction_identity;
- 17,813,826 DVF rows linked to a BDNB building · measured on 2026-08-28
SELECT count(*) FROM public.bdnb_dvf_matches;
- 93 / 93 départements covered by the BDNB match · measured on 2026-08-28
SELECT count(DISTINCT code_departement) FROM public.bdnb_dvf_matches; -- publication gate: all DVF départements
- 13.3 m median distance between the sale and the matched building · measured on 2026-08-28
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY match_distance_m) FROM public.bdnb_dvf_matches;
- 49.8 m 95th percentile of that distance · measured on 2026-08-28
SELECT percentile_cont(0.95) WITHIN GROUP (ORDER BY match_distance_m) FROM public.bdnb_dvf_matches;
- 0 DVF communes in Moselle, Bas-Rhin, Haut-Rhin and Mayotte · measured on 2026-09-24
SELECT count(*) FROM public.dvf_locations WHERE code_departement IN ('57','67','68','976');
Going further
- Comparison of French DVF APIs: which solutions join energy ratings and building data, and which do not.
- API reference (French): address history, BDNB, DPE.
- Data notes (French): DPE, BDNB, methodology.
- MCP tool: get_building_characteristics.