# PAP.fr Scraper - France Properties, Prices & Listings (`abotapi/pap-fr-scraper`) Actor

Scrape PAP.fr real estate listings across France. Extract property prices, addresses, room counts, floor areas, full photo galleries, descriptions and location details including arrondissement, department and region. Ideal for property research, price comparison and market analysis.

- **URL**: https://apify.com/abotapi/pap-fr-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Real estate, Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 39 total users, 3 monthly users, 95.4% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

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

## PAP.fr Scraper: Owner Listed Property Data for France

PAP.fr, De Particulier a Particulier, is France's largest site for owner to owner property listings, with no estate agencies involved. This scraper turns it into structured data: price, rooms, living area, full address, arrondissement, department and region, energy ratings, and the complete photo gallery for sale and rental listings. Pick a city and property type from the form, or paste a PAP.fr search URL for finer filters, then export to JSON, CSV or Excel, or read the results through the API.

### Why This Scraper?

- **Owner listed only.** PAP.fr does not accept agency listings, so every record is a private seller or landlord, not an agent relisting the same property everywhere.
- **Two ways to search.** Type a French city or district name and pick filters from the form (price, living area, land area, rooms, bedrooms, property categories, features, sort), or paste any PAP.fr search, city or listing URL you already opened in your browser.
- **Detail depth on demand.** Turn on detail fetching for coordinates, energy and greenhouse gas ratings, street address, seller phone, reference number, bedrooms, floor, charges, property tax and the full photo gallery. Turn it off for a faster run with just the results page fields.
- **French geography, resolved.** Type a place the way you would on PAP.fr ("Paris", "Lyon 3e", "Saint-Malo"): it is looked up in PAP.fr's own location search, the department is derived when you leave it blank, a name shared by several departments is flagged in the log with the alternatives, and a name PAP.fr does not know is reported with its suggestions instead of being guessed.
- **Auto pagination.** Walks every result page forward to the limit you set, whether you start from the form or from a pasted URL that already points at page 10.
- **Built for monitoring.** Incremental mode returns only new, updated and reappeared listings on repeat runs of the same search, and resume support lets a large one time pull continue across separate runs without re collecting anything.

### Use Cases

- **Buyers and renters:** track new owner listed apartments or houses in specific cities without agency markups or duplicate listings.
- **Market research:** build a dataset of asking prices, living area and rooms across French cities to study pricing trends.
- **Investors and analysts:** monitor a set of departments for new listings, price drops or delisted properties over time.
- **Proptech and data teams:** feed owner listed inventory into a search tool, valuation model or lead pipeline.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `id` | `"400000001"` |
| `url` | `"https://www.pap.fr/annonces/appartement-paris-18e-75018-r400000001"` |
| `listingType` | `"Buy"` (also `"Rent"`) |
| `propertyType` | `"Apartment"` |
| `title` | `"Vente appartement 3 pieces 57 m2 Paris 18e"` |
| `priceValue` | `590000` |
| `priceDisplay` | `"590 000 €"` |
| `rooms` | `3` |
| `livingSpace` | `57` |
| `streetAddress` | `"Rue Muller, 75018 Paris"` |
| `city` | `"Paris 18e"` |
| `arrondissement` | `"Paris 18e"` |
| `postcode` | `"75018"` |
| `department` | `"Paris"` |
| `region` | `"Ile-de-France"` |
| `departmentCode` | `"75"` (from the postcode; `"2A"`/`"2B"` for Corsica, 3 digits overseas) |
| `latitude` / `longitude` | `48.8358` / `2.2899` (when `fetchDetails` is on) |
| `energyClass` / `ghgClass` | `"D"` / `"D"`, A to G (when `fetchDetails` is on) |
| `reference` | `"G20/2718"` |
| `imageCount` | `18` |
| `scrapedAt` | `"2026-04-22T10:00:00.000Z"` |

The same record also carries `bedrooms`, `floor`, `hoaChargesYearly`, `propertyTaxYearly` and `heatingType` (parsed from the seller's description, so they can be missing), `isActive` (the listing is still on offer, from PAP.fr's own offer status), `sellerPhone` and `seller` (the "Particulier" tag PAP uses for private listings), `refusesCommercialSolicitation`, `isFurnished` (rentals: `true` for a furnished let, `false` for an unfurnished one, `null` for sales), the full `images` array and `additionalProperties` (PAP's own French key and value pairs, such as "Type de bien"). With `incrementalMode` on, each record also gets `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt`.

### How to Use

1. Pick a **search mode**: `filter` (city, department and property type) or `url` (paste PAP.fr search links).
2. Fill in the fields for that mode. In filter mode you can leave the department blank for well known cities, it is derived automatically.
3. Turn detail fetching on or off, and set **Max pages** and **Max listings** to control run size and cost.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Search one city:**

```json
{
  "mode": "filter",
  "locations": [{ "city": "Paris", "department": "75" }],
  "listingType": "Buy",
  "propertyType": "Apartment",
  "maxPages": 10
}
```

**Rentals with a rooms and sort filter:**

```json
{
  "mode": "filter",
  "locations": [{ "city": "Lyon", "department": "69" }],
  "listingType": "Rent",
  "propertyType": "Apartment",
  "roomsMin": 3,
  "sortBy": "PriceAsc",
  "maxPages": 10
}
```

**Several cities at once, department left to auto detect:**

```json
{
  "mode": "filter",
  "locations": [
    { "city": "Paris" },
    { "city": "Marseille" },
    { "city": "Bordeaux" }
  ],
  "listingType": "Buy",
  "propertyType": "House",
  "maxPages": 5
}
```

**A pasted URL with a rooms filter and price sort, resuming from page 3:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.pap.fr/annonce/vente-appartements-paris-75-g439-3-pieces-tri-prix-asc-3"
  ],
  "maxPages": 20
}
```

#### Supported URL shapes (URL mode)

Any URL copied straight from `pap.fr` passes through unchanged: PAP's own server accepts price, surface and bedroom filters as extra path segments, multi-type and generic searches, department/region level and nationwide searches, and the alternative spellings PAP.fr itself uses on its page links. A pasted listing/detail page is fetched as a single record, never run through the search/pagination path. A link missing `https://` is accepted, and an HTML-escaped `&amp;` is decoded.

| Shape | Example | Notes |
| --- | --- | --- |
| Default search | `https://www.pap.fr/annonce/vente-appartements-paris-75-g439` | City + department + PAP's own location id |
| Rent (singular estate slug) | `https://www.pap.fr/annonce/locations-appartement-paris-75-g439` | `locations-appartement`, not `-appartements` |
| Rooms filter | `.../vente-appartements-paris-75-g439-3-pieces` | `N-pieces` suffix |
| Sort order | `.../vente-appartements-paris-75-g439-tri-prix-asc` | `tri-<dim>-asc\|desc` suffix |
| Start page | `.../vente-appartements-paris-75-g439-2` | Trailing `-N`; scraping starts at page N |
| Multiple property types | `https://www.pap.fr/annonce/vente-maisons-appartements-paris-75-g439` | Each listing keeps its own correct type |
| Generic (all types) | `https://www.pap.fr/annonce/vente-immobilier-paris-75-g439` | PAP's catch all search |
| Price / surface / bedroom filters | `.../vente-appartements-paris-7e-g37774-studio-a-partir-de-1-chambres-jusqu-a-150000-euros` | The same filters filter mode builds from its typed inputs |
| Alternative spelling (PAP.fr's own page links) | `https://www.pap.fr/annonce/appartement-a-louer-paris-75-g439-2` | Also `appartement-a-vendre-…`, `ventes-appart-…`, `offre-location-…` and the like; sale or rent is read from the wording |
| Department only (no city) | `https://www.pap.fr/annonce/vente-appartements-75` | Whole department |
| Region | `https://www.pap.fr/annonce/vente-appartements-ile-de-france` | Whole region |
| Nationwide | `https://www.pap.fr/annonce/vente-appartements` | All of France |
| City page | `https://www.pap.fr/immobilier/paris-75-g439` | Converted to the full all-types search for that place, sale or rent per `listingType` |
| Tracking parameters | `...-g439?utm_source=newsletter` | Query string is ignored, not an error |
| Listing / detail page | `https://www.pap.fr/annonces/appartement-paris-18e-75018-r464203311` | Fetched directly as one record, keyed on its `r<id>` |

**Default behaviour (changed):** in URL mode, leaving the URLs field untouched scrapes the built-in sample list (one URL per supported shape above, 11 in all) in order, up to Max total listings (default 20; each search is walked page by page until the cap, so the default usually returns 20 listings from the first sample search only, page 1 and part of page 2), instead of nothing. To sample every shape, set a small `maxPages` (for example 1) together with a higher `maxListings`, because without `maxPages` the first search alone can use up any cap. Replace the list with your own URLs, or set `maxListings`/`maxPages` low for a cheap trial. The `m.pap.fr` mobile host is accepted and rewritten to `www.pap.fr`.

Anything else, the bare `pap.fr` homepage, any other PAP page, or a non PAP.fr URL, is **skipped with a warning naming the URL**, never silently scraped as an empty result. If none of the pasted URLs is usable, the run ends with an error saying so.

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/pap-fr-scraper").call(run_input={
    "mode": "filter",
    "locations": [{"city": "Paris", "department": "75"}],
    "listingType": "Buy",
    "propertyType": "Apartment",
    "maxPages": 5,
})
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(listing["title"], listing["priceDisplay"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/pap-fr-scraper').call({
    mode: 'filter',
    locations: [{ city: 'Paris', department: '75' }],
    listingType: 'Buy',
    propertyType: 'Apartment',
    maxPages: 5,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Filter mode's full filter set

Filter mode builds price, living-area, land-area, rooms, bedrooms, property-type and feature filters directly from typed inputs, and can search sales and rentals together (`listingTypes`) (`priceMin`/`priceMax`, `surfaceMin`/`surfaceMax`, `landSurfaceMin`/`landSurfaceMax`, `roomsMin`/`roomsMax`, `bedroomsMin`/`bedroomsMax`, `propertyTypes`, `features`, `excludeNewBuild`, `strictLocation`), no need to paste a URL just to apply those. URL mode is still the right tool for a department-, region- or France-wide search with no single city, PAP's own "all property types" catch-all search, or any search you already refined and copied straight from your browser.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted, large, one time pull: paste its run or dataset ID and the actor skips every listing id already saved there, so you only pay for the new ones. The actor also checkpoints its own progress automatically, so a platform restart or a manual resurrect of a failed run picks back up without re saving what it already collected.
- **Incremental mode** (`incrementalMode`) is for scheduling the same search again and again. Each listing is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED`, `REAPPEARED`, or `EXPIRED`. By default only `NEW`, `UPDATED` and `REAPPEARED` rows are returned and billed. Turn on `emitUnchanged` or `emitExpired` only if you also want those rows. `EXPIRED` rows only appear after a run that scanned the entire tracked search, not one capped by `maxPages` or `maxListings`, blocked partway through, run with `resumeFromRunId`, or a URL-mode search that started mid-catalogue (a pasted URL whose trailing page number is greater than 1). Even then, EXPIRED rows never exceed the run's own remaining `maxListings` room; the rest are retried on a later run. `REAPPEARED` can only ever be produced for a listing that was previously marked `EXPIRED`, so it is only reachable once `emitExpired` has been turned on for at least one earlier run. `stateKey` names or deliberately shares the tracked state; left blank, a key is derived from the search itself, so two different searches never mix state. `ignoreFieldsForChanges` lets you exclude extra fields (beyond the built-in `scrapedAt`, `searchPage` and `searchPosition`, which change on every run or with PAP.fr's result order) from change detection, using a bare name, a nested path (`seller.phone`), or a nested wildcard (`seller.*`, `additionalProperties.*`); ignored fields are still returned in every row, only the change/diff logic ignores them. When an update of this actor corrects how an existing field is read (for example coordinates, `isActive` and `title` in October 2026), a monitoring campaign started before it does not see a one-time wave of `UPDATED` rows for that correction; new fields such as `departmentCode` and `isFurnished` likewise never count as a change on their first run.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape, the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human readable **summary** of each listing, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify -> Settings -> Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. If the picker is empty, you have not authorized a connector yet.
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `filter` | `filter` for city and department inputs, or `url` for pasted PAP.fr search URLs. |
| `locations` | array | prefill: Paris | Places to search (filter mode only): a city, town or district name as on PAP.fr (`Paris`, `Lyon 3e`), optionally with its department code. Looked up in PAP.fr's own location search; the department is auto derived if left blank, ambiguous names are flagged in the log, unknown names are reported with PAP.fr's suggestions. Falls back to Paris, department 75, if the array is left empty. |
| `urls` | array | the built-in sample list, one URL per supported shape (11 URLs), scraped in order up to Max total listings | PAP.fr search, city or listing URLs (url mode only). A trailing `-N` page number is honored as the starting page. |
| `references` | array | (none) | Optional, both modes. PAP.fr listing reference codes as printed on the listing ("Réf. : G35/3579"). Each one is looked up and returned as a single listing. Leave Locations empty in filter mode to look up references only. |
| `listingType` | string | `Buy` | `Buy` (Vente) or `Rent` (Location). Filter mode only. |
| `listingTypes` | array | (none) | Optional. Tick both `Buy` and `Rent` to search sales and rentals for every location in one run; each listing keeps its own `listingType`. When set, this replaces `listingType`. Filter mode only. |
| `propertyType` | string | `Apartment` | `Apartment`, `House`, `Plot`, `Parking`, or `CommercialProperty`. Filter mode only. |
| `roomsMin` | integer | (none) | Minimum number of rooms (pieces), e.g. `3` returns 3, 4, 5... rooms. `6` means "6 or more". Filter mode only. |
| `roomsMax` | integer | (none) | Maximum number of rooms (pieces). Combine with Min rooms for a range, e.g. 2-3. `6` or more means no upper limit. Filter mode only. |
| `bedroomsMin` | integer | (none) | Minimum number of bedrooms (chambres), applied by PAP.fr's own search. Combine with Max bedrooms for a range. Filter mode only. |
| `bedroomsMax` | integer | (none) | Maximum number of bedrooms (chambres). PAP.fr's own search has no working maximum, so this is checked on each result instead: a listing without a printed bedroom count is left out, and a 1-room studio counts as 0 bedrooms. Filter mode only. |
| `priceMin` | integer | (none) | Minimum price in euros (sale price, or monthly rent for rentals). Filter mode only. |
| `priceMax` | integer | (none) | Maximum price in euros (sale price, or monthly rent for rentals). Filter mode only. |
| `surfaceMin` | integer | (none) | Minimum living area in m². Filter mode only. |
| `surfaceMax` | integer | (none) | Maximum living area in m². Filter mode only. |
| `landSurfaceMin` | integer | (none) | Minimum land/garden area in m² (houses and plots). Filter mode only. |
| `landSurfaceMax` | integer | (none) | Maximum land/garden area in m² (houses and plots). Filter mode only. |
| `propertyTypes` | array | (none) | Optional. One or more of PAP.fr's own property categories, including ones `propertyType` doesn't have (whole buildings, life-annuity sales, houseboats, offices...). When set, this replaces `propertyType`. Values PAP.fr doesn't offer for the chosen Buy/Rent are ignored with a warning. Filter mode only. |
| `features` | array | (none) | Optional. Only return listings tagged with ALL of these PAP.fr features (lift, balcony/terrace, pool, furnished...). Some apply to sales only or rentals only; ones that don't fit the chosen Buy/Rent are ignored with a warning. Filter mode only. |
| `excludeNewBuild` | boolean | `false` | Hide new-build developer programmes from sale results. Sales only. Filter mode only. |
| `strictLocation` | boolean | `false` | Keep only listings inside the exact place you searched. Off by default: PAP.fr also includes nearby places when the exact one has few listings, and each of those carries its distance in `distanceFromSearchKm`. Filter mode only. |
| `sortBy` | string | `Default` | `Default`, `PriceAsc`, `PriceDesc`, `SurfaceAsc`, `SurfaceDesc`, or `DateAsc`. Filter mode only, ignored in url mode. |
| `maxPages` | integer | (empty, unlimited) | Maximum result pages to walk per search. Leave empty to walk every result page. |
| `maxListings` | integer | `20` | Hard cap on listings across the whole run. `0` means unlimited, still bounded by `maxPages`. |
| `fetchDetails` | boolean | `true` | Visit each listing's page for coordinates, energy ratings, street address, full description, reference number, bedrooms, floor, charges, property tax, heating type and the full photo gallery. Each detail page that loads bills the `detail-enrichment` event (Pricing tab). |
| `resumeFromRunId` | string | (none) | A previous run ID or dataset ID to continue a large walk across separate runs. Listings already saved there are skipped. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring of the same search. Later runs return only new, updated and reappeared listings by default. |
| `stateKey` | string | (none) | Optional name for an incremental mode monitoring campaign. Left blank, a key is derived automatically from the search itself. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return, and bill, listings unchanged since the last run. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return, and bill, listings no longer found, after a run that fully scanned the tracked search. Capped at the run's own `maxListings` headroom; any excess is retried on a later run. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Extra field names/paths to exclude from change detection, on top of the built-in defaults (`scrapedAt`, `searchPage`, `searchPosition`). Supports a nested leaf (`seller.phone`) or a nested wildcard (`seller.*`). Ignored fields are still returned in the output. |
| `proxy` | object | Apify Proxy, RESIDENTIAL (prefill pins IT) | Connection settings. A residential proxy is needed for this site. |
| `mcpConnectors` | array | (none) | Optional. Send a summary of each listing to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only. Page under which listing pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on listings exported to each connector per run. Does not affect the dataset. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "id": "400000001",
  "url": "https://www.pap.fr/annonces/appartement-paris-18e-75018-r400000001",
  "listingType": "Buy",
  "propertyType": "Apartment",
  "title": "Vente appartement 3 pieces 57 m2 Paris 18e",
  "priceValue": 590000,
  "priceCurrency": "EUR",
  "priceDisplay": "590 000 €",

  "rooms": 3,
  "livingSpace": 57,
  "bedrooms": 2,
  "floor": 4,
  "hoaChargesYearly": 2766,
  "propertyTaxYearly": 1450,
  "heatingType": "individuel gaz",
  "description": "Bel appartement lumineux au 4e etage avec ascenseur, proche commerces et metro.",
  "streetAddress": "Rue Muller, 75018 Paris",
  "city": "Paris 18e",
  "postcode": "75018",
  "arrondissement": "Paris 18e",
  "department": "Paris",
  "region": "Ile-de-France",
  "departmentCode": "75",
  "latitude": 48.8358,
  "longitude": 2.2899,
  "energyClass": "D",
  "ghgClass": "D",
  "isActive": true,
  "reference": "G20/2718",
  "sellerPhone": "01 99 00 12 34",
  "seller": { "@type": "Person", "name": "Particulier" },
  "refusesCommercialSolicitation": false,
  "isFurnished": null,
  "imageCount": 18,
  "images": ["https://cdn.pap.fr/photos/example-1.jpg", "https://cdn.pap.fr/photos/example-2.jpg"],
  "additionalProperties": { "Type de bien": "Appartement", "Ascenseur": "Oui" },
  "scrapedAt": "2026-04-22T10:00:00.000Z"
}
```

With `incrementalMode` on, each record also carries `changeType` (`NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED` or `EXPIRED`), `changedFields`, `firstSeenAt` and `lastSeenAt`. These four fields are absent entirely when `incrementalMode` is off, the default output shape is unchanged.

#### Output views

Next to the default **Listings** table (every field in one row), two narrower views give you the same rows with just the columns for one job: **Listing detail** (the fields `fetchDetails` adds, plus location and seller contact) and **Changes since the last run** (`id`, `url`, `title`, `priceValue`, `changeType`, `changedFields`, `firstSeenAt`/`lastSeenAt` for incremental runs). Switch between them on the run's **Dataset** tab, or fetch one directly through the API with `?view=detail` or `?view=changes`.

### Plan Requirement

PAP.fr sits behind a bot-protection challenge. In our latest checks, plain datacenter-style connections were always refused, so a datacenter proxy does not work for this site. We have not measured that a residential connection alone is enough: the scraper relies on its built-in challenge handling on top of the residential connection, and acceptance varies per exit, so some attempts are retried on fresh exits. Keep the default connection setting, it already points at a residential proxy group. For a run that supplies its own proxy under Connection, use a residential provider. Apify free plans do not include residential proxies: a free run automatically continues on a backup connection instead, with a one-time warning that the selected connection is unavailable and some pages may still be missed, and a note in the log whenever a page is actually served over that backup connection. If the backup connection cannot load any page for the search either, a free run ends with an error pointing at upgrading or supplying your own proxy URLs, rather than finishing silently empty. The built-in challenge handling itself depends on a session service on our side; when that service is temporarily unavailable, the run switches to the backup connection for its remaining reads and says so in the log, instead of failing every page on a service that cannot answer.

### FAQ

#### How much does it cost?

You pay per listing returned. The **Pricing** tab shows the current rates. Use **Max listings** and **Max pages** to cap the cost of any run.

With **Visit each listing's detail page** on, every listing whose detail page loads also bills the actor's `detail-enrichment` event — its price is on the **Pricing** tab. Listings whose detail page could not be read ship with just the SERP-level fields and are never billed for it, and rows suppressed as unchanged by an incremental (scheduled) run are not billed for it either.

#### Is it legal to scrape PAP.fr?

This actor collects only publicly available listing data. You are responsible for how you use it: follow PAP.fr's terms and the laws that apply to you, and get legal advice before any commercial redistribution. Seller phone numbers and descriptions are personal data in some jurisdictions, handle exported records accordingly.

#### Can I get only new or changed listings on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and reappeared listings, and unchanged ones are not returned or billed unless you turn `emitUnchanged` on.

#### Can I filter by price, living area or bedrooms?

Yes, in filter mode: `priceMin`/`priceMax`, `surfaceMin`/`surfaceMax`, `landSurfaceMin`/`landSurfaceMax`, `roomsMin`/`roomsMax` and `bedroomsMin`/`bedroomsMax` are applied by PAP.fr's own search (`bedroomsMax` is checked on each result). A search you already refined on PAP.fr can also be pasted as-is in url mode.

#### Why did my run fail instead of returning an empty dataset?

The run fails for a configuration problem, such as a `resumeFromRunId` that does not match a run or dataset you can read, or combining `resumeFromRunId` with an incremental campaign that already has saved state. It also fails when it could not load a single usable results page for the whole run: either every connection lane was refused outright (your plan or your own proxy credentials don't give this run proxy access at all), or every lane it could reach never got back a page it could read. It also fails when none of the places or URLs you gave could be used (every location unknown to PAP.fr, or every pasted URL unsupported); the warnings above that error name each one. Each case ends with a clear message naming which happened and what to do next, instead of a silent empty dataset. A genuinely empty search is different: once at least one results page loaded and PAP.fr itself reported no matching listings, the run still finishes normally with an empty dataset, that means nothing matched, not that the run malfunctioned. Check the run log either way, it explains which case happened.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push listing summaries into Notion, Linear or Airtable.

### 🔗 Want more real estate data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🏷️ <a href="https://apify.com/abotapi/leboncoin-fr-scraper"><b>Leboncoin FR</b></a><br>Pull public leboncoin.fr listings across cars, property, jobs, electronics, furniture...</td><td>🏠 <a href="https://apify.com/abotapi/seloger-france-scraper"><b>SeLoger Scraper</b></a><br>Scrape SeLoger.com properties for sale and rent. Search by location with price, room and...</td></tr>
<tr><td>📇 <a href="https://apify.com/abotapi/pagesjaunes-fr-scraper"><b>PagesJaunes FR</b></a><br>From $0.8/1K. Scrapes business listings from PagesJaunes.fr (French Yellow Pages)...</td><td>💼 <a href="https://apify.com/abotapi/francetravail-fr-scraper"><b>France Travail Scraper</b></a><br>Scrape job offers from France Travail with titles, companies, locations, salaries...</td></tr>
<tr><td>🥦 <a href="https://apify.com/abotapi/auchan-fr-scraper"><b>Auchan France Scraper</b></a><br>Scrape Auchan.fr products: grocery, drinks, household and general merchandise. Real price...</td><td>🛒 <a href="https://apify.com/abotapi/castorama-fr-scraper"><b>Castorama.fr</b></a><br>Scrape Castorama.fr DIY and home-improvement products by keyword, category, or URL...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/pap-fr-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Filter mode = pick property type + city, plus optional price / surface / land / rooms / bedrooms / feature filters. URL mode = paste any pap.fr search or listing URL already refined in a browser.

## `locations` (type: `array`):

Type a French city, town or district name as you would on pap.fr (e.g. Paris, Lyon, Paris 15e, Saint-Malo). It is looked up in pap.fr's own location search, so no site id is needed. The department code is auto-derived and only needed to pick between places that share a name (e.g. Saint-Denis exists in both 93 and 974); the run log names the alternatives when a name is ambiguous, and a name pap.fr does not know is reported with its suggestions, never guessed.

## `listingType` (type: `string`):

Pick Vente (Buy) or Location (Rent). Filter mode only.

## `listingTypes` (type: `array`):

Optional. Tick both to search sales AND rentals for every location in the same run (each listing keeps its own Buy/Rent label). When set, this replaces "Buy or rent?". Leave empty to use "Buy or rent?" alone. Filter mode only.

## `propertyType` (type: `string`):

What kind of property to search for. Filter mode only.

## `roomsMin` (type: `integer`):

Minimum number of rooms (pièces), e.g. 3 returns 3, 4, 5… rooms. 6 means "6 or more". Applied as pap.fr's own rooms filter. Filter mode only.

## `roomsMax` (type: `integer`):

Maximum number of rooms (pièces). Combine with Min rooms for a range, e.g. 2–3. 6 or more means no upper limit. Filter mode only.

## `bedroomsMin` (type: `integer`):

Minimum number of bedrooms (chambres), applied by pap.fr's own search. Combine with Max bedrooms for a range. Filter mode only.

## `bedroomsMax` (type: `integer`):

Maximum number of bedrooms (chambres). pap.fr's own search has no working maximum, so this is checked on each result: the listing's printed bedroom count must be at or below this value (a 1-room studio counts as 0 bedrooms; a listing that prints neither count is left out). Filter mode only.

## `priceMin` (type: `integer`):

Minimum price in euros (sale price, or monthly rent for rentals). Applied as pap.fr's own price filter. Filter mode only.

## `priceMax` (type: `integer`):

Maximum price in euros (sale price, or monthly rent for rentals). Filter mode only.

## `surfaceMin` (type: `integer`):

Minimum living area in m². Filter mode only.

## `surfaceMax` (type: `integer`):

Maximum living area in m². Filter mode only.

## `landSurfaceMin` (type: `integer`):

Minimum land/garden area in m² (houses and plots). Filter mode only.

## `landSurfaceMax` (type: `integer`):

Maximum land/garden area in m² (houses and plots). Filter mode only.

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

Optional. Pick one or more of pap.fr's own property categories, including ones the single Property type list doesn't have (whole buildings, life-annuity sales, houseboats, offices…). When set, this replaces Property type. Values pap.fr doesn't offer for the chosen Buy/Rent are ignored with a warning. Filter mode only.

## `features` (type: `array`):

Optional. Only return listings pap.fr tags with ALL of these features. Some apply to sales only or rentals only; ones that don't fit the chosen Buy/Rent are ignored with a warning. Filter mode only.

## `excludeNewBuild` (type: `boolean`):

Hide new-build developer programmes from sale results. Sales only. Filter mode only.

## `strictLocation` (type: `boolean`):

When a place has few listings, pap.fr also shows listings from nearby places, marked “à N km de …”. Turn this on to keep only listings inside the place you searched. Off (default): nearby listings are included and each one carries its distance in distanceFromSearchKm. Filter mode only.

## `sortBy` (type: `string`):

How to order results. Filter mode only — in URL mode the sort is taken from your pasted URL. Default = most recent first (PAP's own default).

## `urls` (type: `array`):

One or more pap.fr URLs: search pages, city pages or listing/detail pages (e.g. https://www.pap.fr/annonces/appartement-paris-18e-75018-r464203311). Tip: open pap.fr in your browser, pick property type + location + any price/surface/bedroom filters, then paste the resulting URL here. Every search path shape pap.fr emits passes through unchanged, including the alternative spellings pap.fr uses on its own page links ("appartement-a-louer-…", "ventes-appart-…"); see the README's URL shapes table. A city page (https://www.pap.fr/immobilier/<place>-g<id>) is converted to the full search for that place, sale or rent per Listing type. A pasted detail URL is fetched as a single listing. If a search URL ends with `-N` (page number), scraping starts at page N and walks forward. Any other URL (e.g. the bare pap.fr homepage) is skipped with a warning, not silently scraped as empty.

## `references` (type: `array`):

Optional. pap.fr listing reference codes as printed on the listing ("Réf. : G35/3579"). Each one is looked up on pap.fr and returned as a single listing, in either mode. Leave Locations empty in Filter mode to look up references only.

## `maxPages` (type: `integer`):

How many result pages to walk per location / URL. PAP returns 12 listings per page (+3 featured). Leave empty to walk every result page.

## `maxListings` (type: `integer`):

Hard cap across the whole run. 0 = unlimited (bounded only by Max pages).

## `fetchDetails` (type: `boolean`):

Adds lat/lng, DPE/GES energy classes, street address, full seller description, reference number, bedrooms, floor, HOA charges, property tax, heating type, and the full high-res photo gallery. Turn off for a faster run with just the SERP-level fields (price, rooms, m², city, primary photo). Each listing whose detail page loads also bills the detail-enrichment charge event (see the Pricing tab); listings whose detail page could not be read are not billed for it.

## `proxy` (type: `object`):

Residential proxy recommended. Some exits are refused and the scraper retries on fresh exits automatically. The default pins an Italian exit, which worked most reliably in our tests; if runs keep failing, clear the country or try another.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a full-catalogue walk across separate runs. Listings already collected there (matched by listing id) are skipped, so this run only saves the new ones. Leave empty for a normal fresh run. For recurring monitoring of the same search instead, use Incremental mode below.

## `incrementalMode` (type: `boolean`):

Turn this on for daily/weekly recurring monitoring of the same search. The first run returns all matching listings as NEW. Later runs return only NEW, UPDATED, and REAPPEARED listings by default. Turn on "Emit unchanged listings" or "Emit expired listings" only when you also want those rows returned (and billed). State is kept separately per search (location/URL, listing/property type, rooms, sort, and detail-fetch setting) — use State key below to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search inputs above (mode, locations/URLs, listing/property type, rooms, sort, detail-fetch setting) — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return listings that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return listings that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max pages/Max listings capped it, a block stopped it early, or Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking. Reappeared listings (a previous EXPIRED id seen again) can only ever occur once this has been turned on for at least one earlier run.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names to exclude from change detection on top of the built-in defaults (currently just the per-fetch `scrapedAt` timestamp) — use this if you notice a field flipping every run for a reason you don't care about. A bare name ignores a top-level field (e.g. `priceDisplay`); a dotted path ignores one nested field (`seller.phone`); a path ending in `.*` ignores an entire nested object (`seller.*`, `additionalProperties.*`). Ignored fields are still returned in every row — only `changeType`/`changedFields` detection ignores them. Leave empty to use only the built-in defaults.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "filter",
  "locations": [
    {
      "city": "Paris"
    }
  ],
  "listingType": "Buy",
  "propertyType": "Apartment",
  "excludeNewBuild": false,
  "strictLocation": false,
  "sortBy": "Default",
  "urls": [
    "https://www.pap.fr/annonce/vente-appartements-paris-75-g439",
    "https://www.pap.fr/annonce/locations-appartement-paris-75-g439",
    "https://www.pap.fr/annonce/vente-appartements-paris-75-g439-3-pieces-tri-prix-asc-2",
    "https://www.pap.fr/annonce/appartement-a-louer-paris-75-g439-2",
    "https://www.pap.fr/annonce/vente-maisons-appartements-paris-75-g439",
    "https://www.pap.fr/annonce/vente-immobilier-paris-75-g439",
    "https://www.pap.fr/annonce/vente-appartements-paris-7e-g37774-studio-a-partir-de-1-chambres-jusqu-a-150000-euros",
    "https://www.pap.fr/annonce/vente-appartements-75",
    "https://www.pap.fr/annonce/vente-appartements-ile-de-france",
    "https://www.pap.fr/annonce/vente-appartements",
    "https://www.pap.fr/immobilier/paris-75-g439",
    "https://www.pap.fr/annonces/appartement-paris-18e-75018-r464203311",
    "https://www.pap.fr/annonce/vente-appartements-paris-75-g439?utm_source=newsletter"
  ],
  "maxListings": 20,
  "fetchDetails": true,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "IT"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `changes` (type: `string`):

No description

# 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 = {
    "locations": [
        {
            "city": "Paris"
        }
    ],
    "urls": [
        "https://www.pap.fr/annonce/vente-appartements-paris-75-g439",
        "https://www.pap.fr/annonce/locations-appartement-paris-75-g439",
        "https://www.pap.fr/annonce/vente-appartements-paris-75-g439-3-pieces-tri-prix-asc-2",
        "https://www.pap.fr/annonce/appartement-a-louer-paris-75-g439-2",
        "https://www.pap.fr/annonce/vente-maisons-appartements-paris-75-g439",
        "https://www.pap.fr/annonce/vente-immobilier-paris-75-g439",
        "https://www.pap.fr/annonce/vente-appartements-paris-7e-g37774-studio-a-partir-de-1-chambres-jusqu-a-150000-euros",
        "https://www.pap.fr/annonce/vente-appartements-75",
        "https://www.pap.fr/annonce/vente-appartements-ile-de-france",
        "https://www.pap.fr/annonce/vente-appartements",
        "https://www.pap.fr/immobilier/paris-75-g439",
        "https://www.pap.fr/annonces/appartement-paris-18e-75018-r464203311",
        "https://www.pap.fr/annonce/vente-appartements-paris-75-g439?utm_source=newsletter"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "IT"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/pap-fr-scraper").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 = {
    "locations": [{ "city": "Paris" }],
    "urls": [
        "https://www.pap.fr/annonce/vente-appartements-paris-75-g439",
        "https://www.pap.fr/annonce/locations-appartement-paris-75-g439",
        "https://www.pap.fr/annonce/vente-appartements-paris-75-g439-3-pieces-tri-prix-asc-2",
        "https://www.pap.fr/annonce/appartement-a-louer-paris-75-g439-2",
        "https://www.pap.fr/annonce/vente-maisons-appartements-paris-75-g439",
        "https://www.pap.fr/annonce/vente-immobilier-paris-75-g439",
        "https://www.pap.fr/annonce/vente-appartements-paris-7e-g37774-studio-a-partir-de-1-chambres-jusqu-a-150000-euros",
        "https://www.pap.fr/annonce/vente-appartements-75",
        "https://www.pap.fr/annonce/vente-appartements-ile-de-france",
        "https://www.pap.fr/annonce/vente-appartements",
        "https://www.pap.fr/immobilier/paris-75-g439",
        "https://www.pap.fr/annonces/appartement-paris-18e-75018-r464203311",
        "https://www.pap.fr/annonce/vente-appartements-paris-75-g439?utm_source=newsletter",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "IT",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/pap-fr-scraper").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 '{
  "locations": [
    {
      "city": "Paris"
    }
  ],
  "urls": [
    "https://www.pap.fr/annonce/vente-appartements-paris-75-g439",
    "https://www.pap.fr/annonce/locations-appartement-paris-75-g439",
    "https://www.pap.fr/annonce/vente-appartements-paris-75-g439-3-pieces-tri-prix-asc-2",
    "https://www.pap.fr/annonce/appartement-a-louer-paris-75-g439-2",
    "https://www.pap.fr/annonce/vente-maisons-appartements-paris-75-g439",
    "https://www.pap.fr/annonce/vente-immobilier-paris-75-g439",
    "https://www.pap.fr/annonce/vente-appartements-paris-7e-g37774-studio-a-partir-de-1-chambres-jusqu-a-150000-euros",
    "https://www.pap.fr/annonce/vente-appartements-75",
    "https://www.pap.fr/annonce/vente-appartements-ile-de-france",
    "https://www.pap.fr/annonce/vente-appartements",
    "https://www.pap.fr/immobilier/paris-75-g439",
    "https://www.pap.fr/annonces/appartement-paris-18e-75018-r464203311",
    "https://www.pap.fr/annonce/vente-appartements-paris-75-g439?utm_source=newsletter"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "IT"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/pap-fr-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/pap-fr-scraper"
        }
    }
}
```

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/XgIbs7Xhlg08vKaLX/builds/egt4AWJzOp1b8i9CG/openapi.json
