# OfferUp Scraper - Listings, Prices, Sellers & Location Data (`abotapi/offerup-scraper`) Actor

Scrape OfferUp listings by keyword, location and radius. Returns 40+ fields per item: price, condition, GPS, full photo set, category tree and rich seller profiles (rating, items sold, join date, verification). Search and URL modes, 5 sort orders, price and condition filters.

- **URL**: https://apify.com/abotapi/offerup-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 10 total users, 1 monthly users, 97.2% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 listing 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

## OfferUp Scraper: Listings, Sellers, Prices & Category Data

OfferUp Scraper turns OfferUp, the US mobile-first local marketplace, into structured JSON. Search by keyword and location, or paste OfferUp search and item links, and get price, condition, geo coordinates, a full photo set, a three-level category tree and a complete seller profile per listing. Five native sort orders, price and condition filters, genuine price-drop detection, resumable runs and Incremental mode for recurring monitoring are all built in, ready to export to JSON, CSV or Excel, or pull through the API.

### Why This Scraper?

- **Two ways in.** Keyword search with location, radius, price and condition filters, or paste OfferUp search and item URLs directly.
- **40+ fields when Fetch full details is on**, including GPS coordinates, the full photo set, a three-level category tree and a complete seller profile (rating, items sold, join date, response time, verification).
- **Five native sort orders**, plus price range and multi-condition filtering, applied at the source rather than filtered after the fact.
- **Genuine price-drop detection.** Compares OfferUp's own recorded original price against the current price, not just a low asking price.
- **Walks the whole catalogue by default.** Max pages defaults to unlimited: the run walks every result page per query/URL until Max items, OfferUp's own last page, or a repeat-page guard stops it.
- **Resumable, delta-friendly runs.** Point Resume from a previous run at an earlier run or dataset to collect only what it doesn't already have, and Incremental mode tracks a recurring search itself and tags what changed.
- **Checkpointed automatically.** A platform migration or a manual restart of an interrupted run picks up where it left off instead of re-collecting and re-charging listings already pushed.

### Use Cases

- **Resale and arbitrage:** monitor a category or keyword for genuine seller price drops to spot flip opportunities before they're gone.
- **Local market research:** track pricing and condition mix for a product category across cities, ZIP codes or radii.
- **Recurring seller/category watch:** schedule a search with Incremental mode on and review only new, updated or disappeared listings.
- **Lead generation for local services:** pull listings in categories like furniture, electronics or vehicles alongside seller responsiveness and verification signals.
- **Marketplace data science:** build a dataset of local pricing behavior over time using scheduled Incremental runs.

### Data You Get

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

| Field | Example |
| --- | --- |
| `listingId` | `00000000-0000-0000-0000-000000000000` |
| `title` | `Sample Listing Title` |
| `price` | `799` |
| `priceText` | `"$799"` |
| `isFirmPrice` | `false` |
| `conditionText` | `Used` |
| `locationName` | `Miami, FL` |
| `url` | `https://offerup.com/item/detail/00000000-0000-0000-0000-000000000000` |
| `imageUrl` | `https://images.offerup.com/000000000-254x250.jpg` |
| `flags` | `["LOCAL_PICKUP"]` |
| `description` (when Fetch full details is on) | `Full item description text.` |
| `latitude` / `longitude` (when enabled) | `25.0000` / `-80.0000` |
| `distanceMiles` (when enabled) | `4.9` |
| `categoryL1Name` / `categoryL2Name` / `categoryL3Name` (when enabled) | `Electronics & Media` / `Cell phones & Accessories` / `Smartphones` |
| `photos` (when enabled) | `[{ "uuid": "0000", "url": "https://images.offerup.com/000-1440x1920.jpg" }]` |
| `postDate` (when enabled) | `2026-01-01T00:00:00.000Z` |
| `originalPrice` / `isOnSpecial` (when enabled) | `899` / `true` |
| `savingsAmount` / `discountPercent` (when enabled) | `100` / `11` |
| `localPickupEnabled` / `shippingEnabled` (when enabled) | `true` / `false` |
| `sellerName` (when enabled) | `Jane Seller` |
| `sellerRating` / `sellerReviewCount` (when enabled) | `5` / `89` |
| `sellerItemsSold` / `sellerDateJoined` (when enabled) | `564` / `2016-01-01T00:00:00Z` |
| `sellerResponseTime` (when enabled) | `Responds in a few minutes` |
| `sellerIsTruYou` (when enabled) | `true` |

The remaining fields cover secondary detail: `conditionCode` (numeric), `quantity`, `photoCount`, `badges`, `discussionCount`, `isLocal`, `isMerchantItem`, `canShipToBuyer`, `shippingPrice`, `categoryL1Id`/`categoryL2Id`/`categoryL3Id`, `sellerId`, `sellerAvatar`, `sellerPrimaryBadge`/`sellerSecondaryBadge`, `sellerItemsPurchased`, `sellerPublicLocation`, `sellerIsAutosDealer`, `sellerIsBusinessAccount`, `sellerIsPremium`, `sellerWebsiteLink`, and `sellerPhone` when OfferUp exposes it on the seller's profile. All of these "when enabled" fields need Fetch full details on (search mode) or come from the detail page automatically (a pasted item URL, or Price drops only). `vehicleMiles` (vehicle listings only) comes from the search-result card instead, so it's populated in search mode without needing Fetch full details, and is `null` on a pasted item URL. One more distinction worth knowing: a pasted single item URL always reads the detail page itself, so `priceText`, `imageUrl`, `imageWidth`, `imageHeight` and `vehicleMiles` come back `null` and `flags` comes back `[]`, since those fields exist only on search-result cards, which a direct item-page fetch never produces.

### How to Use

1. Pick a **mode**: `search` (keyword + location) or `url` (paste OfferUp search or item links).
2. Fill in the fields for that mode, and switch on **Fetch full details** if you need description, GPS, category and seller data.
3. Set **Max items** to control run size and cost, then click **Start**.
4. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search a city with filters:**

```json
{
  "mode": "search",
  "searchQueries": ["iphone 14"],
  "location": "Miami, FL",
  "radiusMiles": 30,
  "priceMin": 200,
  "priceMax": 600,
  "condition": ["USED", "REFURBISHED"],
  "sortBy": "-price",
  "maxItems": 50
}
```

**Paste OfferUp URLs (a search page and an item page), with full details:**

```json
{
  "mode": "url",
  "urls": [
    "https://offerup.com/search?q=sofa&radius=20",
    "https://offerup.com/item/detail/00000000-0000-0000-0000-000000000000"
  ],
  "fetchDetails": true,
  "maxItems": 30
}
```

**Genuine price drops only (detail fetch is auto-enabled for this):**

```json
{
  "mode": "search",
  "searchQueries": ["couch"],
  "location": "Miami, FL",
  "priceDropOnly": true,
  "maxItems": 40
}
```

**Recurring monitoring of the same search (Incremental mode), including EXPIRED detection:**

```json
{
  "mode": "search",
  "searchQueries": ["iphone 14"],
  "location": "Miami, FL",
  "incrementalMode": true,
  "emitExpired": true,
  "maxItems": 0
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/offerup-scraper").call(run_input={
    "mode": "search",
    "searchQueries": ["iphone 14"],
    "location": "Miami, FL",
})
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(listing["title"], listing["price"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/offerup-scraper').call({
  mode: 'search',
  searchQueries: ['iphone 14'],
  location: 'Miami, FL',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### Max items is the real cap, Max pages is just a safety bound

`maxItems` is the single stopping point for a run (across every query/URL); set it to `0` for unlimited. `maxPages` (also `0` by default) is only an optional per-query ceiling on how many result pages get walked; it never raises the cap set by `maxItems`, it can only lower it. Leave `maxPages` at `0` to walk an entire query to its natural end.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted or earlier run: paste its run or dataset id and this run skips every listing it already holds, so you don't pay twice. Progress is also checkpointed automatically during a run, so a platform migration or a restart of an interrupted run resumes exactly where it stopped.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search. Each listing is classified `NEW`, `UPDATED` (with `changedFields`), or `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on) by default. A listing is only marked gone (and an `EXPIRED` row emitted) when `emitExpired` is on and the run completed a full, uncapped scan of the tracked search, never on a Resume, a Max items cap, or a run stopped by an error; `REAPPEARED` therefore requires a prior run like that, and never happens with `emitExpired` off. `stateKey` names or shares the stored baseline; leave it empty to auto-derive one from your search/URLs and filters. Five fields (`discussionCount`, `sellerLastActive`, `sellerResponseTime`, `distanceMiles`, `isLocal`) are excluded from change detection because they drift on their own between runs, independent of the listing itself; their real current values are still returned in every row. Rotating query strings on image URLs (`imageUrl`, `photos[].url`, `photos[].listUrl`, `sellerAvatar`) are also not compared, so a CDN parameter change is never an UPDATE. Add your own with `ignoreFieldsForChanges` (top-level names or dotted paths such as `photos.listUrl`); UNCHANGED is decided by re-checking the stored record with the current ignore list, so changing it does not cause a wave of UPDATED rows. A pasted URL starting at `?page=2` or later never produces EXPIRED rows. With Incremental mode off, output is exactly as before.

#### URL shapes (url mode)

Behaviour change: in url mode, leaving the URLs field untouched now scrapes one example of each supported shape below instead of nothing. Replace the list with your own links to scrape only those.

| Shape | Example |
|---|---|
| Keyword search | `https://offerup.com/search?q=sofa&radius=20` |
| Keyword search, trailing-slash form | `https://offerup.com/search/?q=sofa` |
| Bare search (Offers Nearby) | `https://offerup.com/search` |
| Paginated search (starts at that page) | `https://offerup.com/search?q=sofa&radius=20&page=2` |
| Search with category slug | `https://offerup.com/search?q=sofa&radius=20&category=home-and-garden` |
| Filtered / sorted search | `https://offerup.com/search?q=sofa&SORT=-posted&PRICE_MIN=10&PRICE_MAX=500&CONDITION=40&delivery_param=p` |
| Top-level category page | `https://offerup.com/c/home-and-garden` |
| Category keyword page | `https://offerup.com/c/furniture/sofa` |
| Legacy explore keyword | `https://offerup.com/explore/k/sofa` |
| Item detail page | `https://offerup.com/item/detail/<id>` |

Upper-case facet keys (`SORT`, `PRICE_MIN`, `PRICE_MAX`, `CONDITION`) are read the same as the lower-case ones. A pasted `&page=2` URL never produces EXPIRED rows. Bare listing ids can be given in `listingIds` (or inside `urls`); when only ids are given the nine default example URLs are not scraped. Listing details for ids and item URLs are read the same way as Fetch full details. Other OfferUp pages (for example rentals or jobs) and non-OfferUp URLs are skipped with a warning in the run log, never silently.

#### Views and output tabs

The dataset has three views: **Overview** (id, title, price, condition, location, image), **Detail** (description, category tree, GPS, seller profile, photos) and **Changes** (`changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt` for Incremental mode). Every field is typed and nullable in the dataset schema, so a field OfferUp does not provide is `null` rather than missing or malformed.

### 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 (price, condition, location, category, seller, URL) 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 haven't 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 | `search` | `search` (keyword + location) or `url` (paste OfferUp URLs); filter fields below are ignored in url mode. |
| `searchQueries` | array | `["iphone"]` | Keywords to search (search mode). Each query is scraped independently. |
| `location` | string | no default (prefill: `Miami, FL`) | City ('Miami, FL'), ZIP ('33101'), or raw 'lat,lon'. Empty searches near the connection's own location. |
| `radiusMiles` | integer | `50` | Radius in miles around the location (1 to 500). |
| `priceMin` | integer | no default | Only return listings priced at or above this amount (USD). |
| `priceMax` | integer | no default | Only return listings priced at or below this amount (USD). |
| `condition` | array | no default | Filter by condition: NEW, OPEN_BOX, REFURBISHED, USED, BROKEN, OTHER. Leave empty for any condition. |
| `priceDropOnly` | boolean | `false` | Keep only listings with a genuine seller price drop (search mode). Auto-enables `fetchDetails` (and its per-item charge) even if left off, since price-drop data only lives on the detail page. |
| `sortBy` | string | `best_match` | `best_match`, `-posted` (newest), `distance` (closest), `price` (low to high), `-price` (high to low). |
| `listingIds` | array | no default | Bare OfferUp listing ids or item detail URLs; each is read from its detail page (billed the detail-enrichment surcharge). Whenever ids are given the run reads them in URL mode, whatever Mode says; the default sample URLs and the default search keyword are not run, while URLs you typed yourself in `urls` are still scraped alongside the ids. |
| `urls` | array | one example of each supported shape (see URL shapes below) | OfferUp search, category or item detail URLs (url mode). Search URLs page forward automatically; a `&page=N` URL starts at page N. |
| `fetchDetails` | boolean | `false` | Visit each listing page for description, GPS, full photo set, category tree, fulfillment details and the complete seller profile. Adds a per-item charge. Always on when `priceDropOnly` is set. |
| `maxItems` | integer | `20` | Stop after this many listings total (across all queries/URLs). `0` = unlimited. |
| `maxPages` | integer | `0` | Optional safety cap on result pages walked per query/URL. `0` walks the whole catalogue until `maxItems`, the site's own last page, or a repeat-page guard stops it. |
| `resumeFromRunId` | string | no default | ID of a previous run (or dataset) of this actor. Listings already in that dataset are skipped, so this run returns only new ones. |
| `incrementalMode` | boolean | `false` | For a search you run on a schedule: tracks this search's own listings between runs and tags each row's `changeType` as NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED. |
| `stateKey` | string | no default | Optional label naming the tracked Incremental mode baseline. Leave empty to auto-derive one from your search/URLs and filters. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode: extra field names or dotted paths excluded from change detection (still returned in rows). |
| `emitUnchanged` | boolean | `false` | Incremental mode: also return and bill listings with no change since the last run. |
| `emitExpired` | boolean | `false` | Incremental mode: also return and bill a row for each previously tracked listing no longer found, only once a run completes a full scan (never on Resume, a Max items cap, or an error-stopped scan). |
| `proxy` | object | Residential US | Connection settings. OfferUp only serves US connections. Residential with country US is the default; search and category pages also load over a US datacenter connection. The actor reads your proxy choice first and falls back to a backup connection if it is refused. |
| `mcpConnectors` | array | no default | Optional: send a summary of each listing to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | no default | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items exported to each connector per run. Does not affect the dataset. |

### Output Example

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

```json
{
  "listingId": "00000000-0000-0000-0000-000000000000",
  "url": "https://offerup.com/item/detail/00000000-0000-0000-0000-000000000000",
  "title": "Sample Listing Title",
  "price": 799,
  "priceText": "$799",
  "isFirmPrice": false,
  "conditionText": "Used",
  "conditionCode": 40,
  "locationName": "Miami, FL",
  "latitude": 25.0000,
  "longitude": -80.0000,
  "distanceMiles": 4.9,
  "flags": ["LOCAL_PICKUP"],
  "categoryL1Name": "Electronics & Media",
  "categoryL2Name": "Cell phones & Accessories",
  "categoryL3Name": "Smartphones",
  "description": "Full item description text.",
  "originalPrice": 899,
  "isOnSpecial": true,
  "savingsAmount": 100,
  "discountPercent": 11,
  "photos": [{ "uuid": "0000", "url": "https://images.offerup.com/000-1440x1920.jpg", "width": 1440, "height": 1920 }],
  "photoCount": 1,
  "postDate": "2026-01-01T00:00:00.000Z",
  "localPickupEnabled": true,
  "shippingEnabled": false,
  "sellerName": "Jane Seller",
  "sellerRating": 5,
  "sellerReviewCount": 89,
  "sellerItemsSold": 564,
  "sellerDateJoined": "2016-01-01T00:00:00Z",
  "sellerResponseTime": "Responds in a few minutes",
  "sellerIsTruYou": true
}
```

With `incrementalMode` on, every row also carries `changeType` (`NEW` / `UPDATED` / `UNCHANGED` / `REAPPEARED` / `EXPIRED`), `changedFields`, `firstSeenAt` and `lastSeenAt`.

### Plan Requirement

OfferUp only serves US connections and geo-blocks non-US exits (a non-US connection gets a "Geolocation Unavailable" page). The default proxy is Apify Residential with country US, but it is not strictly required: in our tests search pages, category pages and the bare search page also loaded over an Apify datacenter connection that exits in the US. The run reads your proxy choice first. If that connection is refused for your account (for example residential on a free plan) the run logs the notice "The selected connection is unavailable to this run" and continues on a backup connection; pages may still be unavailable there. If no page can be loaded at all the run fails with a clear message instead of an empty dataset.

Listing details (Fetch full details, Price drops only, pasted item URLs and listing ids) are read from OfferUp's own listing data, which in our tests loaded without a challenge over both Apify Residential and an Apify datacenter connection that exits in the US. If a listing's details cannot be read, the search-card fields are still returned where available and detail enrichment is not billed for that listing; if OfferUp serves the item page a bot challenge the run logs "A listing detail page was served a bot challenge on this connection; its details were skipped and no detail-enrichment charge applies to it."

### FAQ

#### How much does it cost?

Pricing is pay per event: a start event per run, a dataset-item event for each listing returned, and a detail-enrichment event for each listing whose detail page was actually read when Fetch full details (or Price drops only, which turns it on automatically, or a pasted item URL or listing id) is active. A listing whose details could not be read is not charged. The **Pricing** tab shows the current rates. Use **Max items** to cap the cost of any run.

#### Is it legal to scrape OfferUp?

This actor collects only publicly available marketplace listings. You are responsible for how you use the data: follow OfferUp's terms and the laws that apply to you, get legal advice before any commercial use, and never use seller names, locations or contact details in ways a seller didn't invite (for example unsolicited outreach).

#### 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 and UPDATED rows for that search by default; unchanged listings are skipped unless Emit unchanged listings is on, and turning on Emit expired listings also gets you EXPIRED rows once a run completes a full scan, plus REAPPEARED rows on a later run for a listing that was marked gone that way.

#### Why does pasting one item URL cost more than a plain search listing?

A pasted item URL can only be read from its own detail page, so that fetch always happens and is always billed the detail-enrichment surcharge, even though Fetch full details defaults to off for search mode. A search-mode listing only gets that fetch (and charge) when Fetch full details or Price drops only is turned on. A pasted item URL record also never carries `priceText`, `imageUrl`, the image dimension fields or `vehicleMiles`, and its `flags` comes back empty, because those only ever come from a search-result card, not the detail page.

#### Why did my run return an empty dataset instead of failing?

A run that cannot load any page ends as failed with a clear status message (a connection problem, or the detail-page challenge, not an empty search), so you can fix the connection and rerun rather than parse a generic error. The messages are: "The proxy connection was refused for this account, so no page could be read" (your plan has no proxy access), "No results: residential proxies are unavailable on this run ... and the backup connection could not load the requested OfferUp pages", "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search.", and "Listing details could not be read on this connection ... nothing was collected or charged." (a run that only read detail pages, for example listing ids, and none could be read: the listings may no longer exist, or retry later, or use a search or category URL) A run that returns zero rows without failing means the search genuinely matched nothing. Also check the run log for the detail-page challenge message described under Plan Requirement. The run does fail outright, with a clear message, when Resume from a previous run points at an id that can't be read, or when Incremental mode already has saved state for a search and you also set Resume from a previous run against it.

#### 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 e-commerce data?

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

<table>
<tr><td>🏷️ <a href="https://apify.com/abotapi/kijiji-scraper"><b>Kijiji.ca Scraper</b></a><br>Scrape Kijiji.ca listings across property, vehicles, jobs, electronics, furniture...</td><td>🏷️ <a href="https://apify.com/abotapi/marktplaats-nl-scraper"><b>Marktplaats.nl Scraper</b></a><br>Scrape Marktplaats.nl listings by keyword or URL. Extract title, price, condition...</td></tr>
<tr><td>🚗 <a href="https://apify.com/abotapi/cars-com-scraper"><b>Cars.com Scraper</b></a><br>Scrape cars.com listings by make, ZIP and radius. 90+ fields per car: price, MSRP...</td><td>🏷️ <a href="https://apify.com/abotapi/daangn-scraper"><b>Daangn Scraper</b></a><br>Scrape Daangn (당근) marketplace listings by keyword and region. Returns 35+ fields per...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/depop-scraper"><b>Depop Scraper</b></a><br>Scrape Depop search results, listings, shops, and reviews to JSON, CSV, or Excel. Extract...</td><td>🛒 <a href="https://apify.com/abotapi/mercari-jp-scraper"><b>Mercari Japan Scraper</b></a><br>Scrape Mercari Japan listings, sellers and reviews at scale. Extract names, prices...</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/offerup-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`):

How to start the scrape. 'search' builds OfferUp searches from your keywords + location + filters. 'url' walks the OfferUp search or detail URLs you paste (filter fields below are ignored in URL mode).

## `searchQueries` (type: `array`):

One or more keywords to search OfferUp for (e.g. 'iphone 14', 'mountain bike', 'sofa'). Each query is scraped independently.

## `location` (type: `string`):

Center the search on a place: a US city ('Miami, FL'), a ZIP code ('33101'), or raw coordinates ('25.7617,-80.1918'). Leave empty to search near the connection's location (roughly nationwide).

## `radiusMiles` (type: `integer`):

Radius in miles around the location to include listings from.

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

Only return listings priced at or above this amount.

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

Only return listings priced at or below this amount.

## `condition` (type: `array`):

Filter by item condition. Select one or more. Leave empty for any condition.

## `priceDropOnly` (type: `boolean`):

Only keep listings where the seller has lowered the price from OfferUp's own recorded original price (a genuine markdown, not just a low asking price). Only applies when mode = search. Price-drop data lives on the listing detail page, not the search results page, so turning this on auto-enables 'Fetch full details' for the run (and its per-item charge) even if left off below.

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

Order OfferUp returns results in.

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

Paste OfferUp URLs: search (https://offerup.com/search?q=...), a search page such as \&page=2 (the walk starts there), category pages (/c/<category>, /c/<category>/<keyword>), legacy /explore/k/<keyword>, or item detail URLs (https://offerup.com/item/detail/<id>). Multi-URL supported. Search URLs page forward automatically; filter-mode fields are ignored. Other pages (/rentals, /jobs) and non-OfferUp URLs are skipped with a warning. Left untouched, the field scrapes one example of each supported shape.

## `listingIds` (type: `array`):

Bare OfferUp listing ids (for example 0b6f6c5e-1a2b-4c3d-8e9f-0123456789ab) or item detail URLs. Each id is read from OfferUp's listing data. Whenever ids are given the run reads those listings in URL mode, whatever the Mode field says; the sample URLs and search keywords that are filled in by default are not run. URLs you typed yourself in the URLs field are still scraped alongside the ids.

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

Visit each listing page to add description, GPS coordinates, full photo set, category tree, fulfillment details and the complete seller profile (rating, items sold, join date, verification). Slower and adds a small per-item charge; leave off for fast basic listings. Always on when 'Price drops only' is set.

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

The single cap on this run: stop after collecting this many listings (across all queries/URLs). Set 0 for unlimited.

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

Optional safety limit on result pages walked per query. Leave at 0 to walk the whole catalogue: the run stops at Max items, the site's own last page, or a repeat-page guard, not an artificial page cap.

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

Connection settings. A US exit is required (OfferUp geo-blocks other countries). Apify Residential with country US is the default; search, category and listing detail requests also load over a US datacenter connection.

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

Optional. ID of a previous run of this actor (or a dataset ID). Listings already in that dataset are skipped, so this run returns only NEW listings (a delta). Combine both runs' datasets for the full set. Max items then counts only the new listings.

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

For a search you run on a schedule (e.g. daily): remembers this search's own listings between runs and tags each output row's changeType as NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED, so you only need to look at what changed. Compared: every returned field (price, title, condition, status, photos, seller, etc.). Excluded from the comparison, but still returned in every row: discussionCount, sellerLastActive, sellerResponseTime, distanceMiles, isLocal (they drift with time or your exit location), and the query string of image URLs (imageUrl, photos\[].url/listUrl, sellerAvatar) so rotating CDN parameters never count as a change. State is kept by this actor itself (a hash of your search/URLs + filters, or 'State key' below) — unlike 'Resume from a previous run' above, you never paste a run/dataset id. Off by default so ordinary runs are unaffected.

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

Optional label naming the tracked baseline for Incremental mode. Leave empty to auto-derive one from your search/URLs + filters (two identical setups then share state automatically). Set a value to name a monitoring campaign explicitly, or to run two setups with otherwise-identical filters as separate baselines.

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

When on, Incremental mode also returns and BILLS listings with no change since the last run (changeType UNCHANGED), instead of only NEW/UPDATED/REAPPEARED/EXPIRED. Off by default to keep recurring runs cheap.

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

When on, Incremental mode also returns and BILLS a row for each previously-tracked listing no longer found (changeType EXPIRED) — only once a run completes a full scan of the tracked search (never on Resume, a Max items cap, or an error-stopped scan). Off by default.

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

Optional. Field names to exclude from UPDATED change detection, in addition to the built-in exclusions listed under Incremental mode. Use a top-level name (e.g. sellerName) or a dotted path into nested data (e.g. photos.listUrl); lists are checked element by element. Ignored fields are still returned in every row and never appear in changedFields.

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

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify → Settings → API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip; never changes the dataset output. 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": "search",
  "searchQueries": [
    "iphone"
  ],
  "location": "Miami, FL",
  "radiusMiles": 50,
  "priceDropOnly": false,
  "sortBy": "best_match",
  "urls": [
    "https://offerup.com/search?q=sofa&radius=20",
    "https://offerup.com/search/?q=sofa",
    "https://offerup.com/search",
    "https://offerup.com/search?q=sofa&radius=20&page=2",
    "https://offerup.com/search?q=sofa&radius=20&category=home-and-garden",
    "https://offerup.com/search?q=sofa&SORT=-posted&PRICE_MIN=10&PRICE_MAX=500&CONDITION=40&delivery_param=p",
    "https://offerup.com/c/home-and-garden",
    "https://offerup.com/c/furniture/sofa",
    "https://offerup.com/explore/k/sofa"
  ],
  "fetchDetails": false,
  "maxItems": 20,
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "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 = {
    "mode": "search",
    "searchQueries": [
        "iphone"
    ],
    "location": "Miami, FL",
    "radiusMiles": 50,
    "urls": [
        "https://offerup.com/search?q=sofa&radius=20",
        "https://offerup.com/search/?q=sofa",
        "https://offerup.com/search",
        "https://offerup.com/search?q=sofa&radius=20&page=2",
        "https://offerup.com/search?q=sofa&radius=20&category=home-and-garden",
        "https://offerup.com/search?q=sofa&SORT=-posted&PRICE_MIN=10&PRICE_MAX=500&CONDITION=40&delivery_param=p",
        "https://offerup.com/c/home-and-garden",
        "https://offerup.com/c/furniture/sofa",
        "https://offerup.com/explore/k/sofa"
    ],
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    },
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/offerup-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 = {
    "mode": "search",
    "searchQueries": ["iphone"],
    "location": "Miami, FL",
    "radiusMiles": 50,
    "urls": [
        "https://offerup.com/search?q=sofa&radius=20",
        "https://offerup.com/search/?q=sofa",
        "https://offerup.com/search",
        "https://offerup.com/search?q=sofa&radius=20&page=2",
        "https://offerup.com/search?q=sofa&radius=20&category=home-and-garden",
        "https://offerup.com/search?q=sofa&SORT=-posted&PRICE_MIN=10&PRICE_MAX=500&CONDITION=40&delivery_param=p",
        "https://offerup.com/c/home-and-garden",
        "https://offerup.com/c/furniture/sofa",
        "https://offerup.com/explore/k/sofa",
    ],
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/offerup-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 '{
  "mode": "search",
  "searchQueries": [
    "iphone"
  ],
  "location": "Miami, FL",
  "radiusMiles": 50,
  "urls": [
    "https://offerup.com/search?q=sofa&radius=20",
    "https://offerup.com/search/?q=sofa",
    "https://offerup.com/search",
    "https://offerup.com/search?q=sofa&radius=20&page=2",
    "https://offerup.com/search?q=sofa&radius=20&category=home-and-garden",
    "https://offerup.com/search?q=sofa&SORT=-posted&PRICE_MIN=10&PRICE_MAX=500&CONDITION=40&delivery_param=p",
    "https://offerup.com/c/home-and-garden",
    "https://offerup.com/c/furniture/sofa",
    "https://offerup.com/explore/k/sofa"
  ],
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/offerup-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/offerup-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/IfuMG5ZLN6OkcqRa2/builds/FQ3GSngHWIWBm6pXm/openapi.json
