# Classic Driver — long-form API reference > Companion to . Worked > multi-step queries, full request + response payload shapes, and > the edge cases you will hit if you ship a real integration. This document is for AI agents and developers who need more than the summary contract in `llms.txt`. Everything here is observed from the live API on 2026-05-15; the canonical machine-readable contracts remain the OpenAPI doc at and the runtime config at . Snapshots in this doc (TIDs, counts, examples) drift over time. If a specific TID or count disagrees with what the live endpoint returns, trust the endpoint. ## Contents 1. Two-call resolution pattern, end to end 2. `/options/makes` response shape 3. `/options/models` response shape 4. `/search/cars` request + response shape 5. `/search/bikes` — same shape, different inventory 6. `/search/count` and its facet breakdown 7. `/details/car/{nid}` and `/details/bike/{nid}` response shape 8. Price field shapes (fixed / range / P.O.R.) 9. Mileage field shape and unit handling 10. Auction lots vs dealer listings vs private-seller listings 11. Sold listings — how to find one 12. Generation splitting in the make/model taxonomy 13. Multi-step worked query — "find the cheapest 1970s Porsche, then get full details" 14. JSON-LD parsing tips 15. Pagination 16. Rate limits + error semantics 17. Edge cases worth knowing --- ## 1. Two-call resolution pattern, end to end `make` and `model` parameters on `/search/*` accept numeric IDs only. For "1970s Porsche 911 G-series" the steps are: **Step 1 — resolve the make name to a TID.** ``` GET https://www.classicdriver.com/en/api/options/makes?type=car ``` Response (truncated): ```json { "items": [ { "id": 209, "label": "Porsche", "alias": "porsche", "count": 1336, "model_count": 69 }, { "id": 145, "label": "Ferrari", "alias": "ferrari", "count": 1173, "model_count": 65 } ] } ``` Match `label = "Porsche"` → `id = 209`. The match should be case-insensitive but otherwise exact. Fuzzy match is your problem (see § 17 for tips). If you have an alias-table memory of common mappings (`Bimmer → BMW`, `VW → Volkswagen`) apply it before this call. **Step 2 — resolve the model name to a TID, scoped to the make.** ``` GET https://www.classicdriver.com/en/api/options/models?type=car&make=209 ``` Response (truncated): ```json { "items": [ { "id": 3020, "label": "911 (1963 - 1973)", "alias": "911-1963-1973", "count": 157 }, { "id": 3021, "label": "911 \"G\" (1973 - 1989)", "alias": "911-g-1973-1989", "count": 153 }, { "id": 5707, "label": "911 \"Turbo\" (1974 - 1989)", "alias": "911-turbo-1974-1989", "count": 71 }, { "id": 2419, "label": "911 GT3 (1999 - )", "alias": "911-gt3-1999-", "count": 66 } ] } ``` Match `label` containing `"911 \"G\""` → `id = 3021`. The year-range parenthetical at the end of the label tells you which generation each TID covers — see § 12 for the generation-splitting rules. **Step 3 — run the search with both TIDs plus your filters.** ``` GET https://www.classicdriver.com/en/api/search/cars?make=209&model=3021&year_min=1970&year_max=1979&nit=20 ``` Returns up to 20 results in the response shape documented in § 4. --- ## 2. `/options/makes` response shape ``` GET /en/api/options/makes?type=car GET /en/api/options/makes?type=bike ``` Response: ```json { "items": [ { "id": 209, "label": "Porsche", "alias": "porsche", "count": 1336, "model_count": 69, "description": "optional, may be missing", "image_uri": "optional, https://www.classicdriver.com/sites/.../logo.jpg" } ] } ``` Fields: - `id` — taxonomy term ID (TID). Stable; safe to cache. - `label` — display name in the locale you requested. - `alias` — URL-safe slug; this is what appears under `/cars/` on the marketplace. - `count` — active listing count for this make. - `model_count` — distinct models with at least one active listing. - `description`, `image_uri` — populated for a curated subset (typically the top brands shown on the homepage); absent otherwise. 309 car makes; 95 bike makes. Yacht and watch makes share the same vocabulary but use different `type` values; they're not the focus of this doc. --- ## 3. `/options/models` response shape ``` GET /en/api/options/models?type=car&make=209 ``` Response: ```json { "items": [ { "id": 3020, "label": "911 (1963 - 1973)", "alias": "911-1963-1973", "count": "157", "model_year_from": 1963, "model_year_to": 1973 }, { "id": 12953, "label": "121 LM", "alias": "121-lm", "count": "1" } ] } ``` Fields: - `id` — model term TID. Always a child of the `make` term. - `label` — model name, with a `(YYYY - YYYY)` year-range suffix when the make/model term has been split by generation. A trailing hyphen with no second year (`(2018 - )`) means "still in production". - `count` — active listing count (string-encoded in the response; parse to int on your side). - `model_year_from`, `model_year_to` — first and last year the manufacturer produced the model. These are model-level metadata (when the model existed); the listing-level `year_min` / `year_max` filters on `/search/cars` filter against the specific car's manufacture year. The two often coincide but are not the same field. Models without a year-range suffix typically span the model's entire production run (`121 LM`) or are non-time-bound categories. See § 12 for generation splitting. --- ## 4. `/search/cars` request + response shape ``` GET /en/api/search/cars? ``` Common params: `make`, `model`, `year_min`, `year_max`, `price_min`, `price_max`, `mileage_min`, `mileage_max`, `location`, `colour`, `car_type`, `condition`, `drive`, `gearbox`, `fuel_type`, `seller_type`, `sort`, `sort_order`, `nit`, `offset`. See `llms.txt` § "Common search parameters" + § "Enum-valued parameters" for the full param + value list. Response: ```json { "count": 150, "items": [ { "nid": 1109963, "alias": "car/porsche/911-g/1973/1109963", "seller_id": 234528, "seller_type": "Private", "seller_name": null, "uid": 234528, "type": "car", "img": "https://www.classicdriver.com/sites/default/files/users/234528/cars_images/234528-1109963-car-20260514_150600-1000000037.jpg", "has_video": false, "country": "DE", "price": [ { "type": "fixed", "string": "Listing price", "amount": 169000, "currency": "EUR" } ], "editor_choice": false, "sold": false, "past_item": false, "field_vat_reclaimable": false, "favorite_type": "wishlist", "mileage": { "quantity": 4000, "unit": "kilometer" }, "colour": 12, "condition": 1, "competition": 0, "make": 209, "model": 3021, "manufactured_year": 1973, "analytics": { "make": "Porsche", "model": "911 \"G\"", "country": "DE", "listing_type": "Coupé" }, "title": "1973 Porsche 911 \"G\" - Carrera 2.7" } ] } ``` Notes: - `count` is the total matching the query (NOT the number of items returned — that's `nit`). - `alias` is the path component; the canonical URL is `https://www.classicdriver.com//`. - `seller_name` is `null` for private sellers by design — see § 10. For dealers and auction houses, `seller_name` is the business name (the human contact name is only in `/details/`). - `price` is always an array (typically length 1). Each element is one of three shapes — see § 8. - `mileage.unit` is the seller-entered unit (`kilometer` or `mile`). Do NOT convert on your side; if you need a single unit for sort comparison, do it at presentation time. - `colour`, `condition`, `car_type` (via `analytics.listing_type` or the response facets) are numeric codes — resolve via `llms.txt` enum tables. - `country` is ISO 3166-1 alpha-2. --- ## 5. `/search/bikes` — same shape, different inventory Identical request and response structure. Substitute `bike` for `car` in `type`, and `bike_type` for `car_type` in the enum-valued filters. `mileage` for bikes is typically populated in `kilometer`; `gearbox` and `drive` rarely apply. ``` GET /en/api/search/bikes?make=883&nit=20 ``` (Make 883 = Ducati.) --- ## 6. `/search/count` and its facet breakdown `/search/count` is the cheap "preview" endpoint. Call it before paginating a search so you can render the result count and a filter UI without pulling 50 listings. ``` GET /en/api/search/count?type=car&make=189&location=DE ``` **`type=car` (or `type=bike`) is REQUIRED** on `/search/count`; without it the count returns all listing types (including collectibles and yachts). Response: ```json { "count": 412, "location": [ { "id": "DE", "count": 412 } ], "colour": [ { "id": 2, "count": 67 }, { "id": 12, "count": 58 }, { "id": "null", "count": 31 } ], "car_type": [ { "id": 2, "count": 180 }, { "id": 3, "count": 95 } ], "drive": [ { "id": 1, "count": 380 }, { "id": 2, "count": 25 }, { "id": "null", "count": 7 } ], "competition": [ { "id": 0, "count": 380 } ], "type_of_seller": [ { "id": 2, "count": 350 }, { "id": 1, "count": 12 } ], "condition": [ { "id": 5, "count": 280 } ], "tag": [], "make": [{ "count": 412, "id": 189 }], "model": [] } ``` Each facet array shows the count breakdown for each enum value **within** the rest of your filter set. You can use this to render "of your 412 results, 67 are black and 58 are white" without a second search call. Notes: - `id: "null"` (literal string) means the listing has no value set for that facet — sellers can leave colour/drive/condition blank. - Facet keys use slightly older naming in places — `type_of_seller` instead of `seller_type`. Values are still consistent. - `make` and `model` facet arrays are populated when you DON'T filter by make/model — they tell you the top makes/models matching your filter set. When you do filter by make+model they collapse to a single self-referential entry. --- ## 7. `/details/car/{nid}` and `/details/bike/{nid}` response shape ``` GET /en/api/details/car/1109891 ``` Response: ```json { "details": { "type": "car", "title": [ { "name": "1970 Ferrari 365 - 365 GTB/4 Daytona", "model_variant": "365 GTB/4 Daytona" } ], "price": [ { "type": "string", "string": "P.O.R." } ], "summary": { "Year of manufacture": "1970", "Mileage": "28 500 km / 17 710 mi", "Car type": "Coupé", "Exterior colour": "Red", "Location": "Switzerland", "Interior colour": "Black" }, "advanced_summary": { "Competition car": "No", "Condition": "N/A", "Number of doors": "2", "Number of seats": "2", "Location": "Switzerland", "Drivetrain": "2wd", "Fuel type": "Petrol", "Gearbox": "Manual" }, "node_info": { "nid": "1109891", "uid": "42606", "status": "1" }, "seller": { "id": "42606", "type": "dealer", "first_name": "Paolo", "last_name": "Mora", "phone": "+41919804191" }, "make": 145, "model": 3103, "analytics": { "make": "Ferrari", "model": "365", "country": "CH", "listing_type": "Coupé" }, "title": "...", "favorite_type": "wishlist", "images": "...", "equipment": "...", "description": "..." } } ``` Notes: - `summary` and `advanced_summary` are the same display blocks shown on the listing detail page; keys are localised (call `/de/api/...` to get German keys). - `seller` is present for dealer + private listings; auction lots return an `auction` block instead (see § 10). - `images` is an array of image-style variants per photo. Use the `large` or `card` style for browse, `original` for the full resolution. - `equipment` is a list of seller-entered equipment tags. - `description` is HTML; agents should strip tags or sanitise before showing to the end user. Calling `/details//` for a non-existent or unpublished NID returns HTTP 404 — see § 16 for the exact error semantics (the recent fix in commit `380c7c5356` made unpublished + non-existent NIDs indistinguishable in the response). --- ## 8. Price field shapes The `price` field is always an array of one element (occasionally two for listings with both a sale and a rental price). Each element is one of three shapes: **Fixed price** — the normal case. ```json { "type": "fixed", "string": "Listing price", "amount": 169000, "currency": "EUR" } ``` **Range / estimate** — auction lots typically. ```json { "type": "range", "string": "Estimate", "low": 75000, "high": 85000, "currency": "CHF" } ``` **P.O.R. — "Price on Request"** — seller hides the price. ```json { "type": "string", "string": "P.O.R." } ``` For sort-by-price queries, `field_total_price_def` is used internally to compare across currencies (it's the EUR-converted default). The displayed price object still reflects the seller's chosen currency. P.O.R. listings sort to the end of price-ascending queries. --- ## 9. Mileage field shape Listing summaries return: ```json { "mileage": { "quantity": 28500, "unit": "kilometer" } } ``` `unit` is one of: - `kilometer` — kilometres - `mile` — miles (US / UK / sometimes restorations imported with miles) The corresponding JSON-LD on the listing detail page uses UN/CEFACT codes: - `KMT` — kilometres - `SMI` — miles The detail page's `summary.Mileage` block usually shows both conversions ("28 500 km / 17 710 mi") so users see the seller's original value plus a courtesy conversion. **Do not normalise on sort or filter — `mileage_min` / `mileage_max` on the search endpoint are always kilometres.** A listing with mileage stored in miles will still be matched by a `mileage_max=50000` query as if its mileage were in km. (This is a known sharp edge; we surface the raw value in the JSON-LD so agents can render correctly.) --- ## 10. Auction lots vs dealer listings vs private-seller listings `seller_type` (in `/search/cars` items) takes one of three values: `"Dealer"`, `"Private"`, `"Auction House"`. Dealer listings: - Returned `seller_name` is the business name. - Detail-page `seller` block has dealer first/last name + phone. - JSON-LD `Offer.seller` is `AutomotiveBusiness`. - Listing has a `//dealer/` profile. Auction-house lots: - Returned `seller_name` is the auction house business name. - Detail-page `auction` block is populated with the parent event (auction ID, name, city, country, date). - JSON-LD `Offer.seller` is `Organization`. - Listing has a `//auction-house/` and a parent `//auction/`. - Price is typically a `range` shape (`Estimate`). Private-seller listings: - `seller_name` is `null` by design (private sellers are not named publicly). The JSON-LD `Offer.seller.name` reads literally `"Private Seller"`. - Detail-page `seller` block exposes contact information **only when the seller has opted-in to display** — agents must never assume the email or phone number is present. - JSON-LD `Offer.seller` is `Person` with `name: "Private Seller"`. The role-based detection is canonical: do not infer seller type from text patterns in the title or description. --- ## 11. Sold listings — how to find one Active search endpoints (`/search/cars`, `/search/bikes`) by default filter out sold listings. To verify a specific NID is sold (e.g. because the user is asking about a listing they bookmarked): ``` GET /en/api/details/car/{nid} ``` If the listing still exists, the response is returned and the listing's Vehicle JSON-LD will carry `availability: "https://schema.org/SoldOut"`. If the listing has been removed (rare; usually only when the seller asks), the endpoint returns HTTP 404. There is no "sold archive" search endpoint exposed publicly. --- ## 12. Generation splitting in the make/model taxonomy The make/model taxonomy is one make per parent term, one model per child term, with a single year-range suffix on labels. Generations are partially modelled by splitting popular models into multiple year-bounded terms: | Model TID | Label | |-----------|----------------------------------| | 3020 | `911 (1963 - 1973)` | | 3021 | `911 "G" (1973 - 1989)` | | 5707 | `911 "Turbo" (1974 - 1989)` | | 3022 | `911 / 964 Carrera (1989 - 1994)`| | 3023 | `911 / 993 Carrera (1994 - 1998)`| | 2419 | `911 GT3 (1999 - )` | | 14407 | `911 / 992 Carrera (2018 - )` | | … | … | This is inconsistent across the catalogue. Some popular models are split (911, BMW M3); others are a single term covering the entire production run. Generation codes (E30, 991.2, 964) appear: 1. In the model label (as in the table above) 2. In the listing's `field_model_variant` (free-text "variant" field) 3. In the listing title and description For an agent asked "find me a 964", the best strategy is usually: - Try matching the user's generation code against the model labels for the most popular makes first - If no match: filter by `make` + year range (964 → 1989–1994), then let the user pick Search of `field_model_variant` is via the keyword/full-text path in the existing API; that path is undocumented externally today. --- ## 13. Multi-step worked query User asks: "What's the cheapest 1970s Porsche 911 in the catalogue right now, and where is it?" **Step A** — resolve Porsche to a TID: ``` GET /en/api/options/makes?type=car ``` → `Porsche = 209` **Step B** — find which 911-family model terms cover the 1970s: ``` GET /en/api/options/models?type=car&make=209 ``` → `3020 (1963-1973)` and `3021 (1973-1989)` both overlap 1970-1979. `5707 (1974-1989)` also overlaps. **Step C** — search across all generations by omitting the model and filtering by year range; sort by price ascending: ``` GET /en/api/search/cars?make=209&year_min=1970&year_max=1979&sort=field_total_price_def&sort_order=asc&nit=5 ``` Returns up to 5 results. The first item with a `price.type == "fixed"` is the cheapest with a public price. Listings with `price.type == "string"` (P.O.R.) sort to the end of ascending price. **Step D** — get the full details for the chosen listing: ``` GET /en/api/details/car/ ``` → `summary.Location` answers "where is it"; `seller` answers "who's selling"; the canonical listing URL is `https://www.classicdriver.com//`. **Step E** — direct the user to the listing URL in a browser to view photos and contact the seller. Do **not** attempt to compose an inquiry programmatically. --- ## 14. JSON-LD parsing tips Every active listing detail page emits a `