# DVF Property Sales API (Valeurs Foncières): Price/m², GPS (`actose/dvf-property-sales`) Actor

Official French property sales (DVF / valeurs foncières) by commune, postal code or department: date, price, surface, rooms, price per m², GPS, median €/m² per commune. No login. MCP-ready for Claude, Cursor, ChatGPT and AI agents. Pay per result, empty results free. $0.50 per 1,000 sales.

- **URL**: https://apify.com/actose/dvf-property-sales.md
- **Developed by:** [ABM](https://apify.com/actose) (community)
- **Categories:** Real estate, Lead generation, Agents
- **Security:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.43 / 1,000 property sales

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## DVF Property Sales API: France sold prices (Valeurs Foncières)

**$0.50 per 1,000 property sales.** Pay per result. No API key, no login, no monthly rental. Errors, unknown places and empty searches cost nothing.

This Actor returns **official French property sales** from DVF (*Demandes de valeurs foncières*, published by the tax administration DGFiP and geolocated by Etalab). Search by **commune, postal code or department**, filter by **year, property type, surface, price, rooms or price per m²**, and get one clean JSON object per sale: date, price, built surface, Carrez surface, rooms, land surface, **price per m²**, address, **GPS coordinates** and cadastral parcels. Optionally, it computes the **median price per m² per commune** for you.

Raw DVF files are hard to use: one sale is spread over several rows (one per unit, parcel and land type), the price is repeated on every row, and surfaces get double-counted. This Actor merges the rows into one sale, removes the duplicates, and only computes a price per m² when it makes sense.

### How much does it cost?

| What | Price |
|---|---|
| One sale delivered (`sale` event) | $0.0005 |
| 1,000 sales | **$0.50** |
| 10,000 sales (e.g. every apartment sold in Toulouse in two years) | $5.00 |
| Statistics per commune (`includeStatistics`) | free |
| Unknown commune, empty result, source error | free |

Example: all **apartment sales in Bordeaux in 2024** (about 4,000 sales) cost about **$2**. Set `maxResults` to cap your spend; the run stops cleanly when your budget is reached.

### What data do you get?

| Field | Description |
|---|---|
| `saleId` | Unique DVF transaction id |
| `saleDate`, `year` | Date of the deed |
| `saleType` | `sale`, `offPlan` (VEFA), `auction`, `buildingLand`, `exchange`, `expropriation` |
| `price` | Price in EUR (whole transaction) |
| `propertyType` | `apartment`, `house`, `commercial`, `outbuilding`, `land` |
| `builtSurface`, `carrezSurface`, `rooms`, `landSurface` | Surfaces in m², main rooms |
| `pricePerSqm` | Price / built surface (price / land surface for land) |
| `isMultiUnit`, `unitCount`, `outbuildingCount`, `lotCount` | Several flats sold together, parking spaces, cellars… |
| `address`, `fullAddress`, `postalCode`, `communeName`, `communeCode`, `departmentCode` | Location |
| `latitude`, `longitude` | Coordinates of the parcel |
| `parcelIds`, `units` | Cadastral parcels and the detail of every unit |
| `summary` | One plain-text sentence ready for LLMs and RAG |

### Input example

```json
{
  "postalCodes": ["31000"],
  "years": ["2024"],
  "propertyTypes": ["apartment"],
  "maxResults": 20
}
```

Other ways to search:

```json
{
  "communes": ["Saint-Étienne", "Lyon", "Saint-Denis (93)", "33063"],
  "departments": ["2A"],
  "years": ["2023", "2024"],
  "propertyTypes": ["house"],
  "minSurface": 80,
  "maxPrice": 400000,
  "minPricePerSqm": 500,
  "includeStatistics": true
}
```

- `communes`: names or INSEE codes. Paris, Lyon and Marseille include all their arrondissements. For homonyms, add the department: `"Saint-Denis (93)"`.
- `postalCodes`: only sales whose address has that postal code.
- `departments`: codes (`"31"`, `"2A"`, `"971"`) or names (`"Gironde"`).
- `years`: empty = latest year available. `dateFrom` / `dateTo` narrow it down to the day.
- Results are sorted newest first.

### Output example

```json
{
  "saleId": "2024-412873",
  "saleDate": "2024-12-20",
  "year": 2024,
  "saleType": "sale",
  "saleTypeLabel": "Vente",
  "price": 210000,
  "propertyType": "apartment",
  "propertyTypeLabel": "Appartement",
  "builtSurface": 46,
  "carrezSurface": 45.5,
  "rooms": 2,
  "landSurface": null,
  "pricePerSqm": 4565.22,
  "unitCount": 1,
  "outbuildingCount": 1,
  "lotCount": 3,
  "isMultiUnit": false,
  "address": "12B RUE DE METZ",
  "fullAddress": "12B RUE DE METZ, 31000 Toulouse",
  "postalCode": "31000",
  "communeCode": "31555",
  "communeName": "Toulouse",
  "departmentCode": "31",
  "latitude": 43.6003,
  "longitude": 1.4477,
  "parcelIds": ["31555000AB0001"],
  "units": [
    {"type": "apartment", "builtSurface": 46, "rooms": 2, "carrezSurface": 45.5, "parcelId": "31555000AB0001"},
    {"type": "outbuilding", "builtSurface": null, "rooms": 0, "carrezSurface": null, "parcelId": "31555000AB0001"}
  ],
  "summary": "Appartement sold on 2024-12-20 for 210 000 EUR, 46 m² built, 2 rooms, 4 565 EUR/m² - 12B RUE DE METZ, 31000 Toulouse.",
  "sourceFile": "https://files.data.gouv.fr/geo-dvf/latest/csv/2024/departements/31.csv.gz",
  "dataSource": "DVF - Demandes de valeurs foncières (DGFiP), geolocated by Etalab (geo-dvf)"
}
```

With `includeStatistics: true`, the key-value store record `STATISTICS` contains, per commune and property type:

```json
{
  "communeCode": "31555", "communeName": "Toulouse", "propertyType": "apartment",
  "salesCount": 1000, "salesUsedForPricePerSqm": 942,
  "medianPricePerSqm": 3610.5, "p25PricePerSqm": 2980.0, "p75PricePerSqm": 4312.4,
  "meanPricePerSqm": 3702.1, "medianPrice": 189000, "medianSurface": 52,
  "firstSaleDate": "2024-07-01", "lastSaleDate": "2024-12-31"
}
```

Statistics are computed on the sales delivered in the run (single-unit sales only for price per m²).

### Use cases

#### 1. Estate agent: comparable sales for a valuation

An agent in Toulouse values a 3-room flat of 65 m². Before the visit, they pull every comparable sale of the year in the same postal code.

Input:

```json
{"postalCodes": ["31400"], "years": ["2024"], "propertyTypes": ["apartment"], "minRooms": 3, "maxRooms": 3, "minSurface": 55, "maxSurface": 75, "excludeMultiUnitSales": true, "includeStatistics": true}
```

Output (extract):

```json
[{"saleDate": "2024-11-14", "price": 238000, "builtSurface": 66, "rooms": 3, "pricePerSqm": 3606.06, "address": "8 RUE ...", "latitude": 43.58, "longitude": 1.46}]
```

Next: about 150 sales, about $0.08. The median price per m² from `STATISTICS` goes into the valuation report, and the sales go on a map in Google Sheets or Looker Studio.

#### 2. Investor or proptech: monthly market watch on several cities

A rental investor follows small flats in 5 cities. A scheduled run with `onlyNew` only returns the sales added since the last DVF release (April and October).

Input:

```json
{"communes": ["Rennes", "Nantes", "Angers", "Le Mans", "Tours"], "years": ["2025"], "propertyTypes": ["apartment"], "maxSurface": 35, "onlyNew": true, "includeStatistics": true}
```

Output (extract):

```json
[{"communeName": "Angers", "saleDate": "2025-06-27", "price": 98000, "builtSurface": 28, "pricePerSqm": 3500}]
```

Next: about 3,000 new sales per release, about $1.50. A Make scenario writes them to Google Sheets and sends the median price per m² per city to Slack.

#### 3. Data analyst or local authority: land and house prices on a whole department

A planning office studies building land prices in Haute-Savoie over 3 years.

Input:

```json
{"departments": ["74"], "years": ["2022", "2023", "2024"], "propertyTypes": ["land"], "saleTypes": ["sale", "buildingLand"], "minSurface": 300, "maxResults": 0, "includeStatistics": true}
```

Output (extract):

```json
[{"communeName": "Annecy", "propertyType": "land", "landSurface": 612, "price": 245000, "pricePerSqm": 400.33, "parcelIds": ["74010000AK0123"]}]
```

Next: about 6,000 sales, about $3. The `STATISTICS` record gives the median land price per m² per commune for the report; coordinates and parcels go into QGIS.

#### 4. AI agent: answer "how much do flats sell for in Lyon 3e?"

An assistant calls the Actor through MCP with `{"postalCodes": ["69003"], "propertyTypes": ["apartment"], "maxResults": 200, "includeStatistics": true}` and answers from the `summary` fields and the median, for $0.10.

### Use via the API

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("<USERNAME>/dvf-property-sales").call(run_input={
    "postalCodes": ["31000"], "years": ["2024"], "propertyTypes": ["apartment"], "maxResults": 100,
})
for sale in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(sale["saleDate"], sale["price"], sale["builtSurface"], sale["pricePerSqm"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('<USERNAME>/dvf-property-sales').call({
    communes: ['Bordeaux'], years: ['2024'], propertyTypes: ['house'], maxResults: 100,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map((s) => `${s.saleDate} ${s.price} EUR ${s.pricePerSqm} EUR/m²`));
```

### Use with AI agents (MCP)

The Actor works as a tool in Claude, Cursor, ChatGPT and any MCP client through the Apify MCP server (`https://mcp.apify.com`). Add `<USERNAME>/dvf-property-sales` to your tools. The agent passes a commune, postal code or department and gets structured sales back, each with a plain-text `summary`.

### Integrations

- **Google Sheets**: export the dataset, or use the Apify Google Sheets integration to append new sales after each scheduled run.
- **Make / Zapier / n8n**: "Run Actor", then "Get dataset items". Example n8n flow: Schedule (1st of the month), Apify Run Actor (`onlyNew: true`), Google Sheets Append, then Slack with the median price per m².
- **Webhooks**: get notified when a run finishes.
- **Export formats**: JSON, CSV, Excel, XML, RSS, HTML table.

### FAQ

**Where does the data come from?** From the official open data "Demandes de valeurs foncières géolocalisées" (Etalab, data.gouv.fr), Licence Ouverte 2.0. It's the same data the tax administration publishes, with coordinates added.

**How recent is it?** DVF is updated twice a year (April and October) and covers the last 5 full years, currently 2021 to 2025. The most recent months are not available yet.

**Why is `pricePerSqm` null sometimes?** When the sale mixes several kinds of property (e.g. a house and a shop), or when there is no built surface. For multi-unit sales (`isMultiUnit: true`), the price per m² is calculated on the total surface but left out of the statistics.

**Why can a postal-code search miss a sale?** Bare land parcels often have no address in DVF. Search by commune to get them.

**Is it legal?** Yes. DVF is public open data published by the French State. Using it to re-identify people is forbidden by law (article R112 A-3 of the Livre des procédures fiscales), and the data contains no buyer or seller names.

**Do I pay for empty results?** No. You only pay for sales delivered to your dataset.

### Limits

- No data for Alsace-Moselle (departments 57, 67, 68) or Mayotte (976). DVF doesn't cover them.
- Years before the 5-year window (e.g. 2019) are not online anymore.
- The price is the price of the whole transaction, as declared at the notary (furniture and agency fees may or may not be included).
- Coordinates are those of the cadastral parcel, not the exact building entrance.
- A whole department returns tens of thousands of sales. Use filters and `maxResults` to control your cost.

# Actor input Schema

## `communes` (type: `array`):

City names or INSEE codes, e.g. "Toulouse", "Saint-Étienne", "31555". For homonyms add the department: "Saint-Denis (93)". Paris, Lyon and Marseille include all their arrondissements.

## `postalCodes` (type: `array`):

5-digit French postal codes, e.g. "31000", "75011". Only sales whose address has this postal code are returned.

## `departments` (type: `array`):

Department codes ("31", "2A", "971") or names ("Gironde"). Returns every sale of the department - combine with filters and Max results.

## `years` (type: `array`):

Years of sale, e.g. \["2024", "2025"]. DVF covers the last 5 years (currently 2021-2025). Empty = latest available year.

## `dateFrom` (type: `string`):

Optional. Only sales dated on or after this day. If Years is empty, the matching years are read automatically.

## `dateTo` (type: `string`):

Optional. Only sales dated on or before this day.

## `propertyTypes` (type: `array`):

Keep only these property types. Empty = all types.

## `saleTypes` (type: `array`):

Default: standard sales, off-plan sales (VEFA), auctions and building land. Exchanges and expropriations are excluded unless selected.

## `minPrice` (type: `integer`):

Only sales at or above this price.

## `maxPrice` (type: `integer`):

Only sales at or below this price.

## `minSurface` (type: `integer`):

Built surface (land surface for land-only sales).

## `maxSurface` (type: `integer`):

Built surface (land surface for land-only sales).

## `minRooms` (type: `integer`):

Minimum number of main rooms (pièces principales).

## `maxRooms` (type: `integer`):

Maximum number of main rooms.

## `minPricePerSqm` (type: `integer`):

Useful to drop symbolic or family transfers (e.g. 500).

## `maxPricePerSqm` (type: `integer`):

Useful to drop outliers.

## `excludeMultiUnitSales` (type: `boolean`):

Drop sales covering several apartments/houses at once (whole buildings, portfolios), whose price per m² is less comparable.

## `maxResults` (type: `integer`):

Maximum number of sales to return (newest first). 0 = no limit. You only pay for sales delivered.

## `includeStatistics` (type: `boolean`):

Also save median / quartile price per m², median price and surface per commune and property type (computed on the delivered sales) to the key-value store record STATISTICS. Free.

## `onlyNew` (type: `boolean`):

Skip sales already delivered by a previous run with the same area and types. DVF is refreshed twice a year (April and October).

## Actor input object example

```json
{
  "communes": [
    "Toulouse"
  ],
  "years": [
    "2024"
  ],
  "propertyTypes": [
    "apartment"
  ],
  "saleTypes": [
    "sale",
    "offPlan",
    "auction",
    "buildingLand"
  ],
  "excludeMultiUnitSales": false,
  "maxResults": 100,
  "includeStatistics": false,
  "onlyNew": false
}
```

# Actor output Schema

## `sales` (type: `string`):

One item per sale: date, price, surface, rooms, price per m², address, coordinates.

## `statistics` (type: `string`):

Median and quartile price per m² per commune and property type (JSON).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "communes": [
        "Toulouse"
    ],
    "years": [
        "2024"
    ],
    "propertyTypes": [
        "apartment"
    ],
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("actose/dvf-property-sales").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "communes": ["Toulouse"],
    "years": ["2024"],
    "propertyTypes": ["apartment"],
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("actose/dvf-property-sales").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "communes": [
    "Toulouse"
  ],
  "years": [
    "2024"
  ],
  "propertyTypes": [
    "apartment"
  ],
  "maxResults": 100
}' |
apify call actose/dvf-property-sales --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,actose/dvf-property-sales"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/jHAgYAVBwzNKHdtWS/builds/ZJ7zRV27U94J1jCqg/openapi.json
