# Bien'ici Property Scraper (France) (`scrapyx/bienici-properties-scraper`) Actor

Scrapes flats and houses for sale or rent from Bien'ici, France's #3 real estate portal. Search any city, district or postal code; returns price, surface area, rooms, bedrooms, floor, energy class, full description, photos and seller details.

- **URL**: https://apify.com/scrapyx/bienici-properties-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bien'ici Property Scraper (France)

Scrapes flats and houses for sale or rent from
**[Bien'ici](https://www.bienici.com)** — France's #3 real estate portal
(after SeLoger and Leboncoin Immo), and one of the few in this portfolio
where a single search call returns everything: no separate detail-page
fetch exists or is needed.

Public data only. No login, no cookies, no browser.

### What you get

Two record types share one dataset, told apart by `recordType`.

#### `PROPERTY` — one row per listing

Every field Bien'ici's own search response carries, passed through
verbatim: price, surface area, rooms, bedrooms, floor, energy/greenhouse-gas
classification, full description, the complete photo set, and the
seller/agency name — all in one row, no `includePropertyDetails` toggle
because there is nothing more to fetch.

#### `SEARCH_SUMMARY` — one row per location searched

Upstream's own match total, how many rows this run returned, and whether it
hit Bien'ici's `from`/`size` result-window ceiling.

### Input

| Field | What it does |
| --- | --- |
| **Buy / rent** | which side of the market |
| **Property type** | flat or house |
| **Locations** | free text resolved through Bien'ici's own geocoder: `Paris`, `Paris 15e`, `75015`, `Lyon`… one search per entry |
| **Price / bedrooms** | min–max filters, verified to actually shift the match count |
| **Max properties per location** | `0` = unlimited (up to Bien'ici's own ~2,400-row ceiling) |
| **Results per request** | page size, default 24 |

#### Example

```json
{
  "transactionType": "buy",
  "propertyType": "flat",
  "locations": ["Paris 11e", "Paris 20e"],
  "minPrice": 300000,
  "maxPrice": 600000,
  "minBedrooms": 2,
  "maxItems": 500
}
```

### Two things this actor is honest about

**1. An unrecognised `transactionType` is a silent-widen trap upstream, so
this actor refuses it instead.** Bien'ici does not error on a bad value —
it answers 200 with `total` equal to buy-count **plus** rent-count, quietly
mixing both transaction types into one result set. Confirmed live
(buy=7,591 + rent=1,234 ≈ bogus-value total of 8,826). Only `buy`/`rent` are
accepted; anything else is refused before the first request rather than
risk a dataset that looks filtered but isn't.

**2. Bien'ici stops around 2,400–2,500 rows per query, with an honest
error.** Past that, `realEstateAds.json` answers HTTP 400
`"Too many ads requested"` — not a silent empty page, not a clamp to the
last page. A run that hits it gets `resultCapReached: true` and a log line
telling you to narrow by price/bedrooms or search a smaller area:

```
WARN  buy/flat/paris: upstream reports 7592 matches but Bien'ici's realEstateAds.json
      refuses from > 2400 ('Too many ads requested') -- narrow by price/bedrooms
      or search a smaller area to reach the rest.
```

That is exactly why **Locations is a list** — several narrow searches
return far more data than one broad one.

### Notes on reliability

- **No WAF challenge.** Serves data cold on every TLS profile tested. The
  whole HTML site is a client-side SPA (every URL serves the same ~15KB
  shell) — the two JSON endpoints this actor calls are the same
  unauthenticated, unthrottled ones the SPA itself uses.
- **Runs unattended.** No human-captured session, no expiring token.
- **Unknown locations fail cleanly** as `not_found` — the geocoder answers
  an empty array, never a fallback to a nationwide search.
- **`minSurface` is not offered.** Tried during recon and confirmed
  silently ignored by the API (match count did not move) — not shipped as
  a broken-looking option.
- **Residential proxy by default** on cloud runs.

### Output envelope

Every record carries `_input`, `_source` and `_scrapedAt`. Upstream field
names pass through **verbatim** — no renaming, no nested `propertyDetails`
object (there is nothing to nest; the search row IS the full record).
`_source` is `S1-json-search` on every row.

Note `propertyType`/`transactionType` on a property row are upstream's own
fields; the location/filters you searched with are `propertyTypeQueried`/
`transactionTypeQueried`/`locationQueried` instead, so the two never
collide.

`propertyUrl` is constructed from the listing id
(`https://www.bienici.com/annonce/<id>`) as a convenience human link — it
resolves to the same client-side app shell as every other Bien'ici URL, so
fetching it yourself will not return more data than this actor already gives
you.

See [`CRAWLING_METHOD.md`](CRAWLING_METHOD.md) for the full reverse-engineering
trail, including the geocoder discovery and the pagination-ceiling bisection.

# Actor input Schema

## `transactionType` (type: `string`):

Which side of the market to search. An unrecognised value is NOT rejected upstream -- Bien'ici silently combines both buy and rent results -- so this actor validates strictly and refuses invalid input rather than risk a mislabelled dataset.

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

Only these two are verified real Bien'ici filter values this session.

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

Free-text place names -- resolved through Bien'ici's own geocoder, the same one their search box uses (city, district, postal code or arrondissement all work: 'Paris', 'Paris 15e', '75015', 'Lyon'). One search per entry, each with its own SEARCH\_SUMMARY row. A name that resolves to nothing returns a clean not\_found row -- verified live, never a silent fallback to a nationwide search.

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

Leave empty or 0 for no minimum. Verified to actually shift the match count (not a silent no-op).

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

Leave empty or 0 for no maximum. Verified to actually shift the match count.

## `minBedrooms` (type: `integer`):

Leave empty or 0 for any. Verified to actually shift the match count.

## `maxItems` (type: `integer`):

Stop paginating a location after this many properties. Set to 0 for unlimited -- still bounded by Bien'ici's own result-window cap, roughly 2,400-2,500 rows per query regardless of matches ('Too many ads requested' past that). Narrow by price/bedrooms or search a smaller area to reach further into a very broad result set.

## `pageSize` (type: `integer`):

How many listings to request per page (Bien'ici's own default is 24). Higher values mean fewer requests to cover the same ground.

## `maxConcurrency` (type: `integer`):

Upper bound on requests in flight at once, across independent locations.

## `proxyConfiguration` (type: `object`):

Bien'ici runs no active bot challenge and a direct connection worked cleanly across every probe this session, but Residential is the default for cloud runs -- container egress is a different posture than a home connection, and this portfolio has lost a full cloud run to that difference before (jobstreet-jobs-scraper, bayt-jobs-scraper).

## Actor input object example

```json
{
  "transactionType": "buy",
  "propertyType": "flat",
  "locations": [
    "Paris"
  ],
  "minPrice": 0,
  "maxPrice": 0,
  "minBedrooms": 0,
  "maxItems": 240,
  "pageSize": 24,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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": [
        "Paris"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/bienici-properties-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": ["Paris"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/bienici-properties-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": [
    "Paris"
  ]
}' |
apify call scrapyx/bienici-properties-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/bienici-properties-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/gq2grbysENmabkvtv/builds/WcOpQRveCxMcnXiG4/openapi.json
