Piotr VassevPiotr Vassev

How to Scrape Carousell Listings and Compare Secondhand Prices

On Carousell, a useful price comparison often starts with ordinary secondhand listings rather than a broad product search. The URL you choose can change the mix of results, and the scraping depth determines whether you get just cards or the descriptions needed to judge condition.

The Carousell Listings Scraper collects search and listing data from Carousell Singapore. I maintain the Actor. This guide starts with a small card-level sample, then fetches details only when they help you compare the items.

Carousell secondhand listing guide

Check what the search page contains

For a secondhand desk-lamp comparison, start by opening https://www.carousell.sg/desk-lamp/q/. The Actor's documentation distinguishes this keyword-browse pattern from /search/?query=..., which can surface a different mix, including certified products.

Inspect the page before collecting it. Are the results ordinary seller listings, certified products, accessories, or a mixture? The scraper can structure the results, but it cannot make unlike items comparable.

This walkthrough uses the Singapore domain and the Actor's Singapore residential proxy default. It does not establish equivalent coverage for every other Carousell market.

Collect cards before paying for detail pages

Open the Actor in Apify, choose JSON in Input, and use:

{
  "startUrls": [
    {"url": "https://www.carousell.sg/desk-lamp/q/"}
  ],
  "scrapeDetails": false,
  "maxRequestsPerCrawl": 2,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "SG"
  }
}

Set scrapeDetails explicitly. The current schema defaults it to false, although an older README table describes a different default.

maxRequestsPerCrawl counts requests, not listings. Search pages and detail pages consume that allowance. Two search-page requests can produce more than two listing records. Review Apify's run charge limit as well; a request cap is not a result-based spending cap.

Click Start, then inspect the returned titles, prices, conditions, and listing URLs. Use the log to see whether the run stopped at its request limit or encountered a page failure.

The existing screenshot shows an earlier dataset, not a fresh collection:

Historical Carousell dataset

Card mode is useful for finding candidates, but seller identity and other fields can be blank. If a field is absent at card level, that does not establish that it is unavailable on the listing page.

Open a shortlisted listing in detail mode

Choose a few relevant URLs from your card results. For one listing, the input looks like this:

{
  "startUrls": [
    {"url": "https://www.carousell.sg/p/ikea-black-adjustable-desk-lamp-1428851262/"}
  ],
  "scrapeDetails": true,
  "maxRequestsPerCrawl": 1,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "SG"
  }
}

This is a historical listing URL. Replace it with one that is still accessible in your own results. If you enable details on a search-page run, allow for the search request as well as the additional listing-page requests. A very small request cap can leave part of the detail queue unprocessed.

Detail mode is intended to add the description, more images, and seller information. It does not guarantee that every engagement field is available. This abbreviated historical record illustrates both useful data and missing values:

{
  "listingId": "1428851262",
  "title": "IKEA Black Adjustable Desk Lamp",
  "price": 9,
  "currency": "SGD",
  "condition": "Well used",
  "description": "S$9 | Selling to clear due to house move",
  "seller": {
    "username": "sol.handembroidery",
    "profileUrl": "https://www.carousell.sg/u/sol.handembroidery/"
  },
  "views": null,
  "likes": null,
  "scrapedAt": "2026-03-28T09:55:58.496Z"
}

The null values for views and likes mean missing information in this sample, not zero interest. Keep them missing in your export.

Understand the two event prices

Pricing as of September 6, 2026: the configured events are $0.01 per shallow listing and $0.04 per detailed listing, plus an Actor-start event of $0.00005 per GB of allocated memory, with at least one event. The older README price table is lower than the current pricing configuration; use the live pricing page.

For the two-stage workflow above, ten shallow results contribute $0.10 in shallow event fees. Three later detail results contribute $0.12 in detail event fees. That is $0.22 across the two stages before start charges or any other events shown in the actual run breakdown. Rechecking a selected listing in a later run is additional collection work.

Inspect a small run's charges before estimating a large crawl. Do not translate maxRequestsPerCrawl: 100 into “100 products” or a guaranteed total price.

Compare condition before calculating a median

Export CSV for the shortlist and JSON if you need nested seller data or the complete image list. Keep listingId, url, title, price, currency, condition, and scrapedAt. Add a column for your own review of the description and images.

Separate ordinary used items from certified offers and new stock. Then group by model and condition. For lamps, check whether bulbs, power adapters, or other required parts are included. A cheaper incomplete item may cost more to make usable.

Remove duplicate listing IDs before counting the sample. Do not merge different sellers' offers simply because their titles match. Exclude deposit-only prices, requests to buy, bundles, and unrelated accessories if they do not belong in your comparison.

You can now calculate a median asking price for each comparable group. Label it as an asking-price sample, with the date and number of listings. It does not tell you the negotiated price or prove what sold.

For repeat snapshots, match listing IDs and retain old values. A removed listing can reflect a sale, deletion, or a collection gap; do not label it sold without further evidence.

Treat posting times and blanks carefully

The current documentation says timePosted can be a relative string such as “5 hours ago.” When isBumped is true, the displayed time can refer to the bump rather than the original posting.

Store the raw string alongside scrapedAt. Do not use it as an exact original listing date or claim that a recently bumped item is newly listed. Older output samples may have the field blank.

If results are empty, check the URL pattern and the visible result mix, then inspect the proxy settings and run log. If descriptions or sellers are missing, confirm that the row came from detail mode and that the request was actually processed before the cap stopped the run.

The Carousell Node.js example is available for a coded workflow. Keep the distinction between request limits and listing counts when adapting it.

Questions before you run

Does maxRequestsPerCrawl limit the number of listings?

No. It counts search-page and detail-page requests. One search page can return several listings, so the request cap is not a product-count cap.

Why are views or likes missing?

Those values may not be exposed in the collected page. Detail mode can add information, but it does not guarantee every field. Preserve missing values rather than treating them as zero.

Does timePosted mean the original listing date?

Not always. The documented value can be a relative time, and a bumped listing can show its bump time instead of its original posting time. Keep scrapedAt and isBumped alongside it.

Piotr Vassev

Piotr Vassev

Founder of FalconScrape. Building production-grade web scraping systems and data automation pipelines for businesses worldwide.

Connect on LinkedIn