By the end of this tutorial you will have a short Python script that turns a leboncoin.fr search into structured rows: title, price in euros, city and postal code, seller name and type, the ad's attributes (condition, model, storage) and its publication date.
Leboncoin is a French classifieds site. Its search pages hold exactly what price trackers and resellers want, but a plain HTTP client does not get far: in a test on 2026-11-07, a plain client on a home connection was served 6 of 25 result pages before the site began refusing it, and a datacenter IP was served 0 of 25.
So we will use the Apify Actor Leboncoin Scraper (reventadata/leboncoin-scraper) from Python and let it deal with that.
⏱️ Time estimate: about 30 minutes.
Prerequisites
You need:
-
Python 3.11 or newer (the
apify-client3.x package on PyPI declares>=3.11). - An Apify account and API token. Copy the token from the API & Integrations page in Apify Console.
- The Apify client for Python:
pip install -U "apify-client~=3.0"
The script reads the token from an environment variable:
export APIFY_API_TOKEN="your_apify_token_here"
What you'll achieve
You will run one search ("iphone", phones category, newest first, 100 to 300 euros) and print each ad with its price, city, seller and condition. Along the way you will see how the rows are shaped, so you can swap in your own keyword.
Example use case: you watch second-hand phone prices in France and want fresh ads under a price ceiling, with the seller type next to each one. The same input works for any keyword and any leboncoin category number.
The rows come from the search result pages. The ad description and the seller's phone number are not in the Actor's output; hasPhone only says whether the ad has a number.
Step 1: Access Leboncoin Scraper
Open the Leboncoin Scraper page on Apify Store to read the input fields. This tutorial drives the Actor from Python, so the script below replaces clicking through a form.
Step 2: Configure your search
The main input is searchQueries: one search per entry. Everything else is optional:
-
category: a leboncoin category number, thecategory=value in a leboncoin.fr search URL.17is phones and connected devices. Leave it out to search every category. -
sort:relevance(default) ornewest. Withnewest, boosted (paid) ads can come first, ahead of newer unboosted ads, in the order the site returns them;isBoostedmarks them. In the 2026-11-10 sample rows the three boosted ads came first and were published earlier than the unboosted ads that follow. -
minPrice,maxPrice: price limits in euros. -
maxItems: cap on rows across all queries. -
maxPages: pages per query, 35 ads each.
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
run_input = {
"searchQueries": ["iphone"],
"category": "17",
"sort": "newest",
"minPrice": 100,
"maxPrice": 300,
"maxItems": 20,
"maxPages": 1,
}
Tip: start with maxPages: 1. Every page is a separate request made one after another, and the input schema describes one page as taking about 25 seconds.
Step 3: Optional settings
When both minPrice and maxPrice are set, they go to leboncoin in the search URL and the Actor checks them again on every ad. A single limit is applied only after the fetch. In both cases an ad with no price is dropped once a limit is set.
maxPages defaults to 3 and cannot go above 100. maxItems defaults to 100.
Step 4: Run the Actor
Start the run from Python and wait for it to finish:
print("Starting Leboncoin extraction...")
run = client.actor("reventadata/leboncoin-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
status = run.status if run else "unknown"
raise SystemExit(f"Run did not succeed (status: {status}).")
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} ads.\n")
call() blocks until the run ends, so the script waits for the Actor.
Step 5: View and download results
Print the ads:
for item in items:
attrs = item["attributes"]
price = "no price" if item["price"] is None else f'{item["price"]} EUR'
seller = item["ownerName"] or "unknown seller"
print(item["title"])
print(f' {price} | {item["city"]} ({item["zipcode"]}) | {item["ownerType"]}: {seller}')
print(f' condition: {attrs.get("condition")} | model: {attrs.get("phone_model")} | storage: {attrs.get("phone_memory")}')
print(f' {item["url"]}')
Two sample rows from a run on 2026-11-10 (imageUrls cut to one entry and attributes to four keys; the Actor returns them whole):
{
"listId": 3306570323,
"url": "https://www.leboncoin.fr/ad/telephones_objets_connectes/3306570323",
"title": "IPhone 11",
"price": 230,
"priceCents": 23000,
"currency": "EUR",
"categoryId": "17",
"categoryName": "Téléphones & Objets connectés",
"adType": "offer",
"status": "active",
"imageUrls": [
"https://img.leboncoin.fr/api/v1/lbcpb1/images/37/e4/8c/37e48c57804e286f2e6d43476995333e8e3ca448.jpg?rule=ad-large"
],
"imageCount": 6,
"attributes": {
"condition": "Très bon état",
"phone_model": "iPhone 11",
"phone_memory": "64 Go",
"shipping_type": "face_to_face"
},
"city": "Béning-lès-Saint-Avold",
"zipcode": "57800",
"department": "Moselle",
"region": "Lorraine",
"latitude": 49.139,
"longitude": 6.841,
"ownerName": "Deborah545",
"ownerType": "private",
"ownerSiren": null,
"hasPhone": false,
"isBoosted": false,
"firstPublishedAt": "2026-11-10 20:51:10",
"expiresAt": null,
"indexedAt": "2026-11-10 20:51:10",
"matchedQuery": "iphone",
"searchUrl": "https://www.leboncoin.fr/recherche?text=iphone&category=17&sort=time&order=desc&price=100-300&page=1"
}
{
"listId": 3178587975,
"url": "https://www.leboncoin.fr/ad/telephones_objets_connectes/3178587975",
"title": "IPhone 13 Pro Max & iPhone 12 Pro Max avec garantie",
"price": 250,
"priceCents": 25000,
"currency": "EUR",
"categoryId": "17",
"categoryName": "Téléphones & Objets connectés",
"adType": "offer",
"status": "active",
"imageUrls": [
"https://img.leboncoin.fr/api/v1/lbcpb1/images/e8/e5/69/e8e56900bea92208828d8aa9be006e1db5b3f1ae.jpg?rule=ad-large"
],
"imageCount": 2,
"attributes": {
"condition": "Bon état",
"phone_model": "iPhone 13 Pro Max",
"phone_memory": "128 Go",
"shippable": "false"
},
"city": "Marseille",
"zipcode": "13001",
"department": "Bouches-du-Rhône",
"region": "Provence-Alpes-Côte d'Azur",
"latitude": 43.293,
"longitude": 5.371,
"ownerName": "DBC MARSEILLE",
"ownerType": "pro",
"ownerSiren": "993294982",
"hasPhone": true,
"isBoosted": true,
"firstPublishedAt": "2026-10-26 19:24:32",
"expiresAt": "2026-12-25 19:24:32",
"indexedAt": "2026-11-10 19:24:33",
"matchedQuery": "iphone",
"searchUrl": "https://www.leboncoin.fr/recherche?text=iphone&category=17&sort=time&order=desc&price=100-300&page=1"
}
items is a plain list of dictionaries, so you can load it into a spreadsheet, a database or pandas from here.
Understanding your results
-
listId,url,title: the ad's id, link and title. -
price,priceCents,currency: price in euros and in cents;priceisnullwhen the ad has none.currencyis alwaysEUR. -
categoryId,categoryName: the category number and its French name. -
attributes: the ad's attributes by key, with the site's own values: French labels such asTrès bon état, but also raw codes such asface_to_faceorfalse. Phone ads usually carrycondition,phone_modelandphone_memory; a car ad carries mileage, fuel and so on. -
city,zipcode,department,region,latitude,longitude: location. -
ownerName,ownerType,ownerSiren: the public seller name,privateorpro, and the SIREN of a professional seller. -
hasPhone,isBoosted: whether a number is on the ad, and whether the ad is promoted. -
firstPublishedAt,expiresAt,indexedAt: dates as the site gives them;expiresAtisnullwhen the site gives none. -
matchedQuery,searchUrl: the search that found the ad.
Pro tips
-
The Actor does not filter by text. It returns what the site returns. In the page 25 capture dated 2026-11-07, one of 35 ads was a car (a Volkswagen Polo at 8990 EUR in the Voitures category), and phone-category rows included a cable at 4 EUR and a repair service at 55 EUR. Set
category: "17", as above, and filter further in Python. -
Promoted ads repeat across pages. Each ad is returned once per run. In the page 1 capture dated 2026-11-07 all 35 ads had
isBoosted: true; in the page 25 capture none did. - Private sellers only:
private_phones = [
item for item in items
if item["categoryId"] == "17" and item["ownerType"] == "private" and item["price"] is not None
]
private_phones.sort(key=lambda item: item["price"])
for item in private_phones[:5]:
print(item["price"], item["title"], item["city"])
-
Several keywords in one run: pass more than one entry in
searchQueries;maxItemscaps the total. -
Know the ceiling. A search shows at most 100 pages of 35 ads, so 3,500 ads. The page 1 capture reports 377,398 results for "iphone"; narrow with
categoryand price limits.
Troubleshooting
-
Run FAILED on input: an empty
searchQuerieslist, acategorythat is not 1 to 3 digits, orminPriceabovemaxPricefails the run with a status message naming the problem. - Run SUCCEEDED with no rows: every requested page loaded normally, but nothing matched (or price limits dropped every ad). The status message says so; loosen the keyword or price limits.
- Run FAILED with zero rows and a message about unreadable pages: the site blocked a request, a page failed the shape check, or the run clock stopped the run before any row was stored.
-
Fewer rows than
maxItems: the search ran out of pages,maxPageswas reached, or a price limit dropped ads.
The log's summary lines are Skipped N: reason (when ads were skipped) and Pushed N row(s).; read it with client.log(run.id).get().
Next steps
For current pricing and details, see the Store page.
To repeat a search automatically, save it as a task and add an Apify schedule, which starts Actor or task runs on a cron expression. For event-driven pipelines, an Apify webhook that fires when a run status reaches SUCCEEDED sends the run object, which carries defaultDatasetId, to your endpoint.
What are you planning to scrape on leboncoin? Drop a comment! 💬
Top comments (0)