# KupujemProdajem API scraper — search Serbia's largest general classifieds site across all 89 categories (cars, property for sale and to rent, phones, furniture, clothing, services, jobs) by keyword, category, sub-group, town, seller, price band, condition and offer direction, with three sorts and up to 500 rows per page. Full ad detail returns the complete description, every photo, delivery options and the seller's public profile with its review counts and verification badges. Prices come back in the currency the SELLER chose (EUR or RSD) beside the source's own printed string, and an ad with no price — a job or a wanted ad — returns null instead of a fake zero. No account, logged-out public data only, no phone numbers.

> Search KupujemProdajem classifieds. Needs `query` (keywords) OR `category` (a numeric id from the `categories` action) — either alone is enough, and the rest of the filters narrow it: sub-group, town, seller, price band with currency, condition, offer direction, has-price, has-photo, accepts-swap, available-immediately. Returns the source's own total plus, when that total is capped at 20 000, its uncapped count in `total_available`.
> ReefAPI engine `kupujemprodajem` · 4 endpoints · clean JSON, no scraping or browsers to manage.

## How to call
- **Endpoint:** `POST https://api.reefapi.com/kupujemprodajem/v1/<action>` with a JSON body.
- **Auth:** header `x-api-key: <YOUR_REEFAPI_KEY>` — create one free (1,000 credits, no card): https://reefapi.com/signup
- **Response (every call):** `{ ok: boolean, data: ..., meta: { record_count, credits, ... }, error: { code, message } }` — branch on `ok`. Failed calls are free except verified SHEIN NOT_FOUND on product/detail and price (4 credits).
- **One key + one shared credit pool** across every ReefAPI API. Per-call credits are listed on each endpoint below.
- **Use it from an AI agent (MCP):** connect `https://api.reefapi.com/mcp` (remote streamable-http). Send the key as `Authorization: Bearer <key>`, or put it in the URL (`?key=<key>`) when the client has no header field, as ChatGPT does.

## Endpoints

### POST https://api.reefapi.com/kupujemprodajem/v1/search — 2 credits
Search KupujemProdajem classifieds. Needs `query` (keywords) OR `category` (a numeric id from the `categories` action) — either alone is enough, and the rest of the filters narrow it: sub-group, town, seller, price band with currency, condition, offer direction, has-price, has-photo, accepts-swap, available-immediately. Returns the source's own total plus, when that total is capped at 20 000, its uncapped count in `total_available`.

**Parameters:**
- `query` (string, optional) — Free-text keywords, exactly as typed into the site's own search box (Serbian Latin or Cyrillic both work). Give this or `category`, or both.
- `category` (integer, optional) — Numeric category id. 23 Mobilni telefoni, 2919 Automobili, 2821 Nekretnine | Prodaja, 2850 Nekretnine | Izdavanje, 2546 Poslovi, 1268 Nameštaj. The `categories` action lists all 89. Give this or `query`, or both.
- `group` (integer, optional) — Sub-group id inside a category (489 = Apple iPhone inside category 23). Group ids are not published as a standalone table; they come back on every search row as `group_id`/`group_name`. Measured bite: 4 595 → 721.
- `location` (integer, optional) — Town/city id (1 = Beograd). The source publishes no public location table (five endpoint spellings probed, all 404), so ids come from search rows' own `location_id`/`location_name`. Measured bite: 4 595 → 2 842.
- `seller_id` (integer, optional) — Every ad of one seller. The id comes back as `seller_id` on search rows and inside the `seller` block of `listing`.
- `price_min` (number, optional) — Minimum price. A band is only meaningful together with `currency` because sellers quote in EUR *or* RSD on the same page; when you omit `currency` the source's own default (`rsd`) is used and echoed back in `filters`. Measured bite: 4 595 → 137 for 100–200 EUR. A band excludes ads with no price.
- `price_max` (number, optional) — Maximum price. A band is only meaningful together with `currency` because sellers quote in EUR *or* RSD on the same page; when you omit `currency` the source's own default (`rsd`) is used and echoed back in `filters`. Measured bite: 4 595 → 137 for 100–200 EUR. A band excludes ads with no price.
- `currency` (enum, optional) — Currency the price band is expressed in. Does not translate the ads: each row still comes back in the currency its seller chose. [one of: eur, rsd]
- `condition` (enum, optional) — The source's own item condition. Measured bite on a 4 595-row control: new 481, used 1 960. An unknown value is rejected here because the source would answer it with a misleading empty page. [one of: new, as-new, used, damaged]
- `listing_type` (enum, optional) — Offer direction. Omit for both. Measured: sell 2 108, buy 2 487 of a 4 595-row control — wanted ads are a real part of this site, and they carry no price. [one of: sell, buy]
- `has_price` (boolean, optional, default false) — Only ads that actually carry a number (measured 4 595 → 1 989).
- `has_photo` (boolean, optional, default false) — Only ads with at least one photo (measured 4 595 → 4 565 — on this site almost everything has a photo, so it rarely narrows).
- `exchange_only` (boolean, optional, default false) — Only ads whose seller accepts a swap (measured 4 595 → 128).
- `immediate_available` (boolean, optional, default false) — Only ads flagged available right away (measured 4 595 → 1 249).
- `sort` (enum, optional, default "default") — Ordering. 🔴 The source sorts on the RAW price number and ignores the currency, so `price_desc` can put a 316 472 RSD ad above a 9 999 EUR one. Measured, and returned as the source orders it. [one of: default, newest, price_asc, price_desc]
- `page` (integer, optional, default 1) — 1-based page number. 🔴 The source stops at 20 000 rows per query no matter how many it reports available, so narrow the query (category, location, price band) rather than paging past it.
- `per_page` (integer, optional, default 30) — Rows per page, 1–500 (measured: 30 → 59 KB, 60 → 118 KB, 200 → 384 KB, 500 → 956 KB; the source honours all of them). Three other spellings the source silently ignores (`pageSize`, `limit`, `size`) are accepted here as aliases and translated to the one that works.

**Returns:** total_results (the source's figure, capped at 20 000), total_capped, total_available (the source's uncapped count, filled only when capped), page, page_count, page_size, returned, page_clamped, has_more, dropped_non_listing_tiles, filter_id, query, filters (the query string actually sent) and listings[] with listing_id, url, title, description_excerpt, price, price_currency (EUR or RSD — the seller's own choice), price_kind (fixed | negotiable | not_priced | not_priced_job | not_priced_wanted), price_display (the source's own printed string), price_label, price_suffix, exchange_accepted, category_id/category_name, group_id/group_name, location_id/location_name, condition, listing_type (sell | buy), listing_kind (goods | service | job), posted_at, renewed_at, view_count, favorite_count, seller_id, promoted, highlighted, immediate_available, has_video, is_vehicle, image, image_thumbnail and attributes[].

**Example request body:**
```json
{
  "query": "iphone",
  "max_results": 20
}
```

### POST https://api.reefapi.com/kupujemprodajem/v1/listing — 1 credit
Full detail of one ad by id or URL: the complete description, every photo at full size and as a thumbnail, price with the source's own printed string, condition, location, posting and renewal time, view and favourite counts, delivery and local-pickup flags, vehicle/ISBN/OEM fields where the category has them, and the seller's public profile (display name, person or company, town, member-since, review counts, phone-verified and bank-account-verified badges, company tax and registry ids). A removed or non-existent ad returns NOT_FOUND. The seller's phone number is never requested and never returned — only the source's own `has_phone`.

**Parameters:**
- `listing_id` (string, optional) — Ad id — the number at the end of every ad URL and the `listing_id` of every search row. Give this or `url`.
- `url` (string, optional) — Full ad URL exactly as `search` returns it; the id is taken from its `/oglas/<id>` tail.

**Returns:** the same row shape as `search` plus description (full text), images[], image_thumbnails[], image_count, status, valid_until, has_phone, courier_delivery, local_pickup, promo_type, isbn, oem_code, vehicle_vin, vehicle_km, vehicle_registration, share_url, attributes[] and seller{} (seller_id, name, kind, location_id, location_name, member_since, reviews, reviews_positive, reviews_negative, phone_verified, bank_account_verified, company_tax_id, company_registry_id, response_info, profile_note, has_phone).

**Example request body:**
```json
{
  "url": "https://www.kupujemprodajem.com/mobilni-telefoni/apple-iphone/iphone-15/oglas/152902916"
}
```

### POST https://api.reefapi.com/kupujemprodajem/v1/count — 1 credit
How many live ads match a filter set, without downloading any of them. One ~60-byte upstream call, and it is the source's UNCAPPED number: the same query whose `search` total stops at 20 000 counts 84 718 here. Takes exactly the same filters as `search` (page and sort are ignored).

**Parameters:**
- `query` (string, optional) — Free-text keywords, exactly as typed into the site's own search box (Serbian Latin or Cyrillic both work). Give this or `category`, or both.
- `category` (integer, optional) — Numeric category id. 23 Mobilni telefoni, 2919 Automobili, 2821 Nekretnine | Prodaja, 2850 Nekretnine | Izdavanje, 2546 Poslovi, 1268 Nameštaj. The `categories` action lists all 89. Give this or `query`, or both.
- `group` (integer, optional) — Sub-group id inside a category (489 = Apple iPhone inside category 23). Group ids are not published as a standalone table; they come back on every search row as `group_id`/`group_name`. Measured bite: 4 595 → 721.
- `location` (integer, optional) — Town/city id (1 = Beograd). The source publishes no public location table (five endpoint spellings probed, all 404), so ids come from search rows' own `location_id`/`location_name`. Measured bite: 4 595 → 2 842.
- `seller_id` (integer, optional) — Every ad of one seller. The id comes back as `seller_id` on search rows and inside the `seller` block of `listing`.
- `price_min` (number, optional) — Minimum price. A band is only meaningful together with `currency` because sellers quote in EUR *or* RSD on the same page; when you omit `currency` the source's own default (`rsd`) is used and echoed back in `filters`. Measured bite: 4 595 → 137 for 100–200 EUR. A band excludes ads with no price.
- `price_max` (number, optional) — Maximum price. A band is only meaningful together with `currency` because sellers quote in EUR *or* RSD on the same page; when you omit `currency` the source's own default (`rsd`) is used and echoed back in `filters`. Measured bite: 4 595 → 137 for 100–200 EUR. A band excludes ads with no price.
- `currency` (enum, optional) — Currency the price band is expressed in. Does not translate the ads: each row still comes back in the currency its seller chose. [one of: eur, rsd]
- `condition` (enum, optional) — The source's own item condition. Measured bite on a 4 595-row control: new 481, used 1 960. An unknown value is rejected here because the source would answer it with a misleading empty page. [one of: new, as-new, used, damaged]
- `listing_type` (enum, optional) — Offer direction. Omit for both. Measured: sell 2 108, buy 2 487 of a 4 595-row control — wanted ads are a real part of this site, and they carry no price. [one of: sell, buy]
- `has_price` (boolean, optional, default false) — Only ads that actually carry a number (measured 4 595 → 1 989).
- `has_photo` (boolean, optional, default false) — Only ads with at least one photo (measured 4 595 → 4 565 — on this site almost everything has a photo, so it rarely narrows).
- `exchange_only` (boolean, optional, default false) — Only ads whose seller accepts a swap (measured 4 595 → 128).
- `immediate_available` (boolean, optional, default false) — Only ads flagged available right away (measured 4 595 → 1 249).

**Returns:** count (the number of live matching ads) and filters (the query string actually sent).

**Example request body:**
```json
{
  "query": "iphone 15"
}
```

### POST https://api.reefapi.com/kupujemprodajem/v1/categories — 1 credit
The site's live category table — the resolver `search` needs, because `category` takes a numeric id. 89 categories across three kinds (66 goods, 22 services, 1 jobs), each with the flags that tell you whether the condition and swap filters mean anything in it.

**Parameters:**
- `kind` (enum, optional) — Narrow the category list to one kind. Omit for all 89. [one of: goods, service, job]

**Returns:** category_count and categories[] with category_id, name, kind (goods | service | job), active, max_photos, shows_condition, shows_exchange, supports_wanted_ads.

**Example request body:**
```json
{
  "kind": "goods"
}
```

## At scale
- **Volume:** 5M+ requests a day, measured at 60 requests a second across the fleet with no
  central bottleneck. Per-key limits are raised for high-volume accounts; volume pricing on request.
- **Missing a source:** tell us a site we do not cover and it becomes an engine. A customer asked
  for bestprice.gr on 21 Sep 2026 and it was in the catalog on 22 Sep.
- **Support:** 2 minute median time from a question in the live chat to the first answer. Setup
  help included, no support tier to buy.
- **One key, one credit pool** across every API. No per-site plans, no separate subscriptions.

## More
- Try it live, no code: https://reefapi.com/playground?engine=kupujemprodajem
- Human docs page: https://reefapi.com/docs/kupujemprodajem
- Overview page: https://reefapi.com/kupujemprodajem-api
- Every ReefAPI API in one file (for your AI): https://reefapi.com/llms-full.txt
