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 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 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 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.
Run this endpoint
Endpoint
Parameters
Start with the required fields, then add optional filters only when your use case needs them.
Runnable path
This endpoint has no required query parameters.
13 optional filters available.
string
Optional
Keyword search, e.g. "iphone".
iphone
string
Optional
5-digit US zip code to search around. Required unless lat and lon are given.
77002
string
Optional
Latitude. Must be sent together with lon.
example
string
Optional
Longitude. Must be sent together with lat.
example
string
Optional
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.
30
string
Optional
Category id from the categories endpoint, e.g. "5.1". Use this for keyword-free category browsing.
example
string
Optional
Category filter. Requires q; without a keyword OfferUp drops it silently. Cannot be combined with cid.
1234567890
string
Optional
Minimum price in USD.
example
string
Optional
Maximum price in USD.
example
string
Optional
Condition filter: NEW, OPEN_BOX, REFURBISHED, USED, BROKEN, OTHER.
example
string
Optional
Sort order: best_match, -posted, distance, price, -price.
-posted
string
Optional
Opaque cursor from a previous page. Never construct this yourself.
example
string
Optional
Session id from a previous page. Keep it stable across a paginated sweep.
1234567890
Request Examples
<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"x-api-key: YOUR_API_KEY_HERE"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
<?php
use Illuminate\Support\Facades\Http;
$response = Http::timeout(30)
->withHeaders(['x-api-key' => 'YOUR_API_KEY_HERE'])
->get('https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted');
if ($response->successful()) {
echo $response->body();
} else {
echo "Error: " . $response->status();
}
const options = {
method: 'GET',
headers: {
'x-api-key': 'YOUR_API_KEY_HERE'
}
};
fetch('https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted', options)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
const axios = require('axios');
const options = {
method: 'GET',
url: 'https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted',
headers: {
x-api-key: 'YOUR_API_KEY_HERE',
}
};
try {
const response = await axios(options);
console.log(response.data);
} catch (error) {
console.error('Error:', error.message);
}
require 'net/http'
require 'uri'
uri = URI.parse("https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Get.new(uri.request_uri)
request['x-api-key'] = 'YOUR_API_KEY_HERE'
begin
response = http.request(request)
puts response.body
rescue => e
puts "Error: #{e.message}"
end
import http.client
import json
conn = http.client.HTTPSConnection("scrappa.co")
headers = {
'x-api-key': 'YOUR_API_KEY_HERE',
}
try:
conn.request("GET", "/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted", headers=headers)
res = conn.getresponse()
data = res.read()
print(data.decode("utf-8"))
except Exception as e:
print(f"Error: {e}")
finally:
conn.close()
import requests
headers = {
'x-api-key': 'YOUR_API_KEY_HERE',
}
try:
response = requests.get('https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted', headers=headers)
response.raise_for_status()
print(response.text)
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class ApiExample {
public static void main(String[] args) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted")
.addHeader("x-api-key", "YOUR_API_KEY_HERE")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
System.out.println(response.body().string());
} else {
System.out.println("Error: " + response.code());
}
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
client := &http.Client{}
req, err := http.NewRequest("GET", "https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted", nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("x-api-key", "YOUR_API_KEY_HERE")
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
fmt.Println(string(body))
}
#!/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"
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("x-api-key", "YOUR_API_KEY_HERE");
try
{
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted"));
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
import axios from 'axios';
async function run(): Promise<void> {
try {
const response = await axios({
method: 'GET',
url: 'https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted',
headers: {
'x-api-key': 'YOUR_API_KEY_HERE',
},
});
console.log(response.data);
} catch (error) {
console.error('Error:', error);
}
}
void run();
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let response = client
.get("https://scrappa.co/api/offerup/search?q=iphone&zipcode=77002&radius=30&sort=-posted")
.header("x-api-key", "YOUR_API_KEY_HERE")
.send()
.await?;
println!("{}", response.text().await?);
Ok(())
}
Response Schema
Example response fields are illustrative; inspect the JSON before integrating.
Example response fields
Scan these fields before integrating.
success
query
location
filters_applied
results
count
page_cursor
search_session_id
+7 more
Common results fields
listing_id
title
price
location_name
{
"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."
}
Generate Code with AI
Copy a ready-made prompt with all the endpoint details, parameters, and example responses. Paste it into ChatGPT, Claude, or any AI assistant to instantly generate working code.
Try It Live
Test this endpoint in our interactive playground with real data.