# OfferUp Search API Documentation

Scrappa's `GET /api/offerup/search` endpoint returns OfferUp second-hand listings as structured JSON, so you can build marketplace search, price monitoring, and lead generation without maintaining a scraper.

## Searching OfferUp listings

A location is required: pass `zipcode`, or both `lat` and `lon`. Without one, OfferUp answers with listings geolocated to the proxy server's own area rather than yours, which makes results vary between calls. Use [Geocode](/docs/offerup-api/offerup_geocode) to turn a zip code or place name into coordinates first.

Filters cover `q` for keywords, `cid` for category browsing, `price_min` and `price_max`, `radius` in miles, `condition`, and `sort`. Invalid enum values are rejected with a 422 rather than silently ignored, because OfferUp returns unfiltered results instead of an error.

There are two category parameters and they are not interchangeable. `cid` browses the taxonomy on its own; `category_id` narrows a keyword search and therefore requires `q`. Sending `cid` together with `category_id` is rejected with a 422 rather than letting one silently outrank the other, because which filter OfferUp honours in that case was never something we measured.

## What search returns, and what it does not

Each result carries a listing id, title, price, location name, image URL and vehicle mileage where applicable. Ad placements are removed, and repeated listings are deduplicated within the page.

Four fields are detail-only on OfferUp's search resolver and come back as `null` with `hydrated: false`: `condition`, `post_date`, `category`, and `fulfillment_details`. Hydrating a full page would mean dozens of extra upstream requests behind a single charge, so this endpoint stays at one lookup per call. Use [Item Details](/docs/offerup-api/offerup_item_details) to fill those fields in.

There is no `limit` parameter and no result total. OfferUp controls page size and never reports a total count, so `total_results` is always `null` — derive completeness from the listings themselves.

## Delivery filtering is not available

OfferUp's search resolver does not support filtering by delivery or shipping method. Rather than accept a parameter that quietly returns everything, this endpoint omits it. Read `fulfillment_details` from [Item Details](/docs/offerup-api/offerup_item_details) to tell local pickup from shipping on a listing you have already selected.

## When OfferUp is throttling us

If the upstream rate limits the proxy pool, every endpoint here answers with a 503 and a JSON `error` object instead of a `Retry-After` 429. The `Retry-After` response header appears only when OfferUp itself sent one. When it did not, the body carries `suggested_retry_after` — our own measured backoff, offered as guidance rather than as an upstream instruction. Neither shape is billed.

## Paginating without an end signal

OfferUp repeats listings across pages and does not signal the last page. Pass the returned `page_cursor` to continue, and stop when a page adds no new `listing_id` values. Keep `search_session_id` stable for the whole sweep.

- **Documentation:** [https://scrappa.co/docs/offerup-api/offerup_search](https://scrappa.co/docs/offerup-api/offerup_search)
- **API group:** OfferUp API
- **Endpoint:** `GET https://scrappa.co/api/offerup/search`

## Authentication

Send your Scrappa API key in the `X-API-KEY` request header. Paid endpoints also support accountless x402 payments when called without an API key.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | No | Keyword search, e.g. "iphone". |
| `zipcode` | string | No | 5-digit US zip code to search around. Required unless lat and lon are given. |
| `lat` | string | No | Latitude. Must be sent together with lon. |
| `lon` | string | No | Longitude. Must be sent together with lat. |
| `radius` | string | No | Search radius in miles around the location. OfferUp only honours 5, 10, 20, 30 or 50; any other value is snapped to a different distance behind a 200, so it is rejected. |
| `cid` | string | No | Category id from the categories endpoint, e.g. "5.1". Use this for keyword-free category browsing. |
| `category_id` | string | No | Category filter. Requires q; without a keyword OfferUp drops it silently. Cannot be combined with cid. |
| `price_min` | string | No | Minimum price in USD. |
| `price_max` | string | No | Maximum price in USD. |
| `condition` | string | No | Condition filter: NEW, OPEN_BOX, REFURBISHED, USED, BROKEN, OTHER. |
| `sort` | string | No | Sort order: best_match, -posted, distance, price, -price. |
| `page_cursor` | string | No | Opaque cursor from a previous page. Never construct this yourself. |
| `search_session_id` | string | No | Session id from a previous page. Keep it stable across a paginated sweep. |

## Example request

```bash
#!/bin/bash

curl -X GET \
    -H "x-api-key: YOUR_API_KEY_HERE" \
    "https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted"
```

## Example response

```json
{
    "success": true,
    "query": "iphone",
    "location": {
        "zipcode": "77002",
        "lat": null,
        "lon": null,
        "radius": 30
    },
    "filters_applied": {
        "sort": "-posted"
    },
    "results": [
        {
            "listing_id": "9c1f6a2e-4b7d-4a51-9f0e-1d2c3b4a5e6f",
            "title": "iPhone 13 128GB Blue",
            "price": 320,
            "location_name": "Houston, TX",
            "is_firm_price": null,
            "flags": null,
            "image_url": "https://example.com/offerup/iphone-13.jpg",
            "vehicle_miles": null,
            "tile_id": "tile-1",
            "tile_type": "LISTING",
            "condition": null,
            "post_date": null,
            "category": null,
            "fulfillment_details": null,
            "hydrated": false
        }
    ],
    "count": 1,
    "page_cursor": "H4sIAAAA...",
    "search_session_id": "session-abc",
    "available_filters": [
        {
            "target": "CONDITION"
        },
        {
            "target": "PRICE"
        }
    ],
    "excluded_ad_tiles": 2,
    "hydrated": false,
    "pagination_note": "OfferUp repeats listings across pages and gives no end-of-results signal. Stop when a page returns no new listing_id values.",
    "detail_only_fields": [
        "condition",
        "post_date",
        "category",
        "fulfillment_details"
    ],
    "total_results": null,
    "total_results_note": "OfferUp never reports a result total, so none is returned. Do not infer one from page counts."
}
```

## More Scrappa resources

- [API documentation](https://scrappa.co/docs)
- [Full LLM-readable API reference](https://scrappa.co/llms-full.txt)
- [OpenAPI specification](https://scrappa.co/docs/api.json)
- [Pricing](https://scrappa.co/pricing)
