{
  "name": "TheCarApi API reference",
  "contract_version": "2026-10-05",
  "api_base": "https://api.thecarapi.com",
  "canonical": "https://thecarapi.com/docs",
  "endpoint_count": 76,
  "authentication": {
    "methods": [
      "X-API-Key: <key>",
      "Authorization: Bearer <key>",
      "?api_key=<key> (disabled by default)"
    ],
    "scopes": [
      "search",
      "catalog",
      "seo",
      "auctions",
      "details",
      "top-offers",
      "theparking",
      "market",
      "calculator",
      "ops"
    ]
  },
  "formats": {
    "markdown_page": "https://thecarapi.com/docs/{section}.md",
    "markdown_full": "https://thecarapi.com/docs/thecarapi-api-reference.md",
    "text_full": "https://thecarapi.com/docs/thecarapi-api-reference.txt",
    "json_full": "https://thecarapi.com/docs/thecarapi-api-reference.json",
    "openapi": "https://thecarapi.com/openapi.json",
    "llms_txt": "https://thecarapi.com/llms.txt",
    "docs_index": "https://thecarapi.com/docs/llms.txt"
  },
  "guides": [
    {
      "id": "getting-started",
      "label": "Getting started",
      "summary": "What the API serves, the two datasets it exposes, the shape of a request, and a first call you can paste into a terminal.",
      "canonical": "https://thecarapi.com/docs",
      "blocks": [
        {
          "type": "p",
          "text": "A REST API over normalized auction inventory from Auto1, OpenLane, Schadeautos, Copart Germany, Encar, KB ChaChaCha and the Japanese auction houses, plus a European retail classifieds network of some 680 portals. Data responses are JSON, and reads use `GET`, and the only required credential is an API key in a header."
        },
        {
          "type": "p",
          "text": "This reference covers the stable customer integration surface and nothing else. Website-only helpers and internal publishing operations exist but are not public, are not versioned for you, and are not documented here."
        },
        {
          "type": "h2",
          "text": "The mental model"
        },
        {
          "type": "p",
          "text": "A vehicle is addressed by the pair `site_name` + `auction_id_str` — for example `encar/38112900`. That pair is stable for the life of the listing and is the only identifier you need to persist. Everything else in the API is either a way of *finding* that pair (search, facets, catalog, top offers) or a way of *expanding* it (detail, images, price history, VIN history)."
        },
        {
          "type": "ol",
          "items": [
            "**Discover.** `/api/search` with filters, or `/api/facets` to populate the filter controls themselves. You get result cards carrying `site_name` and `auction_id_str`.",
            "**Expand.** `/api/auction/{site}/{id}` returns the full record for one card — specification, condition, the normalized `vehicle_details` block, and the image gallery embedded as `vault_gallery`.",
            "**Enrich.** Price history, VIN history, market reference prices and the landed-cost calculator all take the same pair, or figures from the detail response."
          ]
        },
        {
          "type": "callout",
          "tone": "tip",
          "title": "Fresh where it counts",
          "text": "Eligible running OpenLane auctions receive a live bid overlay on detail requests. A recent bid may be reused within its TTL; other prices reflect the latest successful source refresh. Knowing which is which is the single most useful thing in this reference — see [Live prices](/docs/live-prices)."
        },
        {
          "type": "callout",
          "tone": "info",
          "title": "Two datasets, two surfaces",
          "text": "Auction inventory is queried through `/api/search` with a `site` slug. The classifieds network is retail listings — no bidding, no end date, no detail payload — so it is never merged into the auction feed, never appears in `/api/sites`, and is queried through `/api/classifieds/*` instead."
        },
        {
          "type": "h2",
          "text": "Base URL"
        },
        {
          "type": "code",
          "lang": "text",
          "code": "https://api.thecarapi.com"
        },
        {
          "type": "p",
          "text": "HTTPS is required; plain HTTP is refused rather than redirected. Send JSON request bodies with `Content-Type: application/json`. There is no URL version prefix — the schema contract is carried in the `contract_version` field of most responses (see [Responses & pagination](/docs/conventions) for the handful that omit it) and declared in full by [/api/contract](/docs/ops)."
        },
        {
          "type": "h2",
          "text": "Your first request"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS \\\n  -H \"X-API-Key: $API_KEY\" \\\n  -H \"Accept-Encoding: gzip\" \\\n  --compressed \\\n  \"https://api.thecarapi.com/api/search?brand=bmw&fuel=Diesel&year_from=2018&sort=price_low&limit=24\"",
          "caption": "Authenticated endpoints take the same header; liveness, readiness and report delivery are exceptions. `--compressed` is worth having from the first call — see Responses & pagination."
        },
        {
          "type": "p",
          "text": "A successful response carries `success: true`, the payload under a route-specific key, and the correlation fields described in [Responses & pagination](/docs/conventions). Store `request_id` — it is the only thing that lets support trace a specific call."
        },
        {
          "type": "h2",
          "text": "Anatomy of a request"
        },
        {
          "type": "table",
          "head": [
            "Header",
            "Value",
            "Why"
          ],
          "rows": [
            [
              "`X-API-Key`",
              "Your key",
              "Required, unless you send `Authorization` instead. One auth method per request."
            ],
            [
              "`Authorization`",
              "`Bearer <your key>`",
              "Equivalent to `X-API-Key`; use whichever your HTTP client makes easier."
            ],
            [
              "`Accept-Encoding`",
              "`gzip`",
              "Responses over 2 KB come back gzipped — roughly an eighth of the bytes on a full search page."
            ],
            [
              "`If-None-Match`",
              "A previously received `ETag`",
              "Unchanged pages answer `304` with no body."
            ],
            [
              "`Content-Type`",
              "`application/json`",
              "Only for the routes that accept a POST body."
            ]
          ]
        },
        {
          "type": "table",
          "head": [
            "Response header",
            "Meaning"
          ],
          "rows": [
            [
              "`X-Request-ID`",
              "Correlation id, mirrored in the body as `request_id`. Log it."
            ],
            [
              "`ETag`",
              "Weak validator for conditional requests."
            ],
            [
              "`X-Cache`",
              "`HIT`, `MISS` or `STALE`."
            ],
            [
              "`X-Cache-Age`",
              "Age in seconds of a cached `/api/car-details` payload. Sent only on a cache hit there."
            ],
            [
              "`X-Data-Source`",
              "Where a `/api/car-details` body came from: `auctions` (our stored record), `car_details_cache`, or `scraped` (fetched from the source during your request)."
            ],
            [
              "`X-RateLimit-Limit` / `-Remaining` / `-Reset`",
              "Quota state for the current window. **Sent only for keys that carry a quota** — a key with no configured limit gets no rate-limit headers at all."
            ],
            [
              "`Retry-After`",
              "Sent with `429`. Wait this long, do not guess."
            ],
            [
              "`X-Live-Price`",
              "`pending` when a bid refresh missed the request budget."
            ],
            [
              "`X-Details-Fetch`",
              "Set while `/api/car-details` is still fetching upstream."
            ]
          ]
        },
        {
          "type": "p",
          "text": "Responses may also carry `X-Perf-*` and `X-Local-Source` headers. Those are diagnostic, are not part of this contract, and may change or disappear without notice — do not branch on them."
        },
        {
          "type": "h2",
          "text": "What this API is not"
        },
        {
          "type": "ul",
          "items": [
            "**Not a bidding interface.** It observes auction prices; it never places, raises or withdraws a bid, and it grants no purchase rights on any source platform.",
            "**Not a fee quote.** Auction fees, transport and delivery figures are quoted to *our* buyer account and *our* address, so they are stripped at the response boundary. Use the [import calculator](/docs/calculator) for landed cost on your own route.",
            "**Report files are served through the API.** Normalized document and report URLs use /report-vault paths. Files are fetched and cached; unsupported hosts or non-file pages can redirect to the source."
          ]
        },
        {
          "type": "h2",
          "text": "Read these before you render anything"
        },
        {
          "type": "p",
          "text": "Three pages answer the questions that cause the most rework, and all three are about what the data *means* rather than how to request it."
        },
        {
          "type": "table",
          "head": [
            "Page",
            "The question it answers"
          ],
          "rows": [
            [
              "[Live prices](/docs/live-prices)",
              "Which prices are read at the moment you ask and which reflect the latest successful refresh — and why a detail page can legitimately show a higher price than the card it was opened from."
            ],
            [
              "[Fields by source](/docs/fields-by-source)",
              "Which of the supported sources actually publishes damages, service history, documents and equipment — and which do not. Read this before you promise a feature."
            ],
            [
              "[CO2 & emissions](/docs/co2)",
              "Why there are two CO2 fields, why an estimate always ships with a test standard attached, and why a 2019 figure is not comparable with a 2022 one."
            ]
          ]
        },
        {
          "type": "callout",
          "tone": "tip",
          "title": "Reading this reference with an agent",
          "text": "Every page here is available as Markdown, plain text and JSON, and the whole endpoint surface as an OpenAPI document (OpenAPI 3.0.3 at `/openapi.json`). See [Agents & machine formats](/docs/agents)."
        }
      ],
      "markdown": "# Getting started\n\nWhat the API serves, the two datasets it exposes, the shape of a request, and a first call you can paste into a terminal.\n\nA REST API over normalized auction inventory from Auto1, OpenLane, Schadeautos, Copart Germany, Encar, KB ChaChaCha and the Japanese auction houses, plus a European retail classifieds network of some 680 portals. Data responses are JSON, and reads use `GET`, and the only required credential is an API key in a header.\n\nThis reference covers the stable customer integration surface and nothing else. Website-only helpers and internal publishing operations exist but are not public, are not versioned for you, and are not documented here.\n\n## The mental model\n\nA vehicle is addressed by the pair `site_name` + `auction_id_str` — for example `encar/38112900`. That pair is stable for the life of the listing and is the only identifier you need to persist. Everything else in the API is either a way of *finding* that pair (search, facets, catalog, top offers) or a way of *expanding* it (detail, images, price history, VIN history).\n\n1. **Discover.** `/api/search` with filters, or `/api/facets` to populate the filter controls themselves. You get result cards carrying `site_name` and `auction_id_str`.\n2. **Expand.** `/api/auction/{site}/{id}` returns the full record for one card — specification, condition, the normalized `vehicle_details` block, and the image gallery embedded as `vault_gallery`.\n3. **Enrich.** Price history, VIN history, market reference prices and the landed-cost calculator all take the same pair, or figures from the detail response.\n\n> **Fresh where it counts** — Eligible running OpenLane auctions receive a live bid overlay on detail requests. A recent bid may be reused within its TTL; other prices reflect the latest successful source refresh. Knowing which is which is the single most useful thing in this reference — see [Live prices](https://thecarapi.com/docs/live-prices).\n\n> **Two datasets, two surfaces** — Auction inventory is queried through `/api/search` with a `site` slug. The classifieds network is retail listings — no bidding, no end date, no detail payload — so it is never merged into the auction feed, never appears in `/api/sites`, and is queried through `/api/classifieds/*` instead.\n\n## Base URL\n\n```text\nhttps://api.thecarapi.com\n```\n\nHTTPS is required; plain HTTP is refused rather than redirected. Send JSON request bodies with `Content-Type: application/json`. There is no URL version prefix — the schema contract is carried in the `contract_version` field of most responses (see [Responses & pagination](https://thecarapi.com/docs/conventions) for the handful that omit it) and declared in full by [/api/contract](https://thecarapi.com/docs/ops).\n\n## Your first request\n\n```bash\ncurl -sS \\\n  -H \"X-API-Key: $API_KEY\" \\\n  -H \"Accept-Encoding: gzip\" \\\n  --compressed \\\n  \"https://api.thecarapi.com/api/search?brand=bmw&fuel=Diesel&year_from=2018&sort=price_low&limit=24\"\n```\n\n_Authenticated endpoints take the same header; liveness, readiness and report delivery are exceptions. `--compressed` is worth having from the first call — see Responses & pagination._\n\nA successful response carries `success: true`, the payload under a route-specific key, and the correlation fields described in [Responses & pagination](https://thecarapi.com/docs/conventions). Store `request_id` — it is the only thing that lets support trace a specific call.\n\n## Anatomy of a request\n\n| Header | Value | Why |\n| --- | --- | --- |\n| `X-API-Key` | Your key | Required, unless you send `Authorization` instead. One auth method per request. |\n| `Authorization` | `Bearer <your key>` | Equivalent to `X-API-Key`; use whichever your HTTP client makes easier. |\n| `Accept-Encoding` | `gzip` | Responses over 2 KB come back gzipped — roughly an eighth of the bytes on a full search page. |\n| `If-None-Match` | A previously received `ETag` | Unchanged pages answer `304` with no body. |\n| `Content-Type` | `application/json` | Only for the routes that accept a POST body. |\n\n| Response header | Meaning |\n| --- | --- |\n| `X-Request-ID` | Correlation id, mirrored in the body as `request_id`. Log it. |\n| `ETag` | Weak validator for conditional requests. |\n| `X-Cache` | `HIT`, `MISS` or `STALE`. |\n| `X-Cache-Age` | Age in seconds of a cached `/api/car-details` payload. Sent only on a cache hit there. |\n| `X-Data-Source` | Where a `/api/car-details` body came from: `auctions` (our stored record), `car_details_cache`, or `scraped` (fetched from the source during your request). |\n| `X-RateLimit-Limit` / `-Remaining` / `-Reset` | Quota state for the current window. **Sent only for keys that carry a quota** — a key with no configured limit gets no rate-limit headers at all. |\n| `Retry-After` | Sent with `429`. Wait this long, do not guess. |\n| `X-Live-Price` | `pending` when a bid refresh missed the request budget. |\n| `X-Details-Fetch` | Set while `/api/car-details` is still fetching upstream. |\n\nResponses may also carry `X-Perf-*` and `X-Local-Source` headers. Those are diagnostic, are not part of this contract, and may change or disappear without notice — do not branch on them.\n\n## What this API is not\n\n- **Not a bidding interface.** It observes auction prices; it never places, raises or withdraws a bid, and it grants no purchase rights on any source platform.\n- **Not a fee quote.** Auction fees, transport and delivery figures are quoted to *our* buyer account and *our* address, so they are stripped at the response boundary. Use the [import calculator](https://thecarapi.com/docs/calculator) for landed cost on your own route.\n- **Report files are served through the API.** Normalized document and report URLs use /report-vault paths. Files are fetched and cached; unsupported hosts or non-file pages can redirect to the source.\n\n## Read these before you render anything\n\nThree pages answer the questions that cause the most rework, and all three are about what the data *means* rather than how to request it.\n\n| Page | The question it answers |\n| --- | --- |\n| [Live prices](https://thecarapi.com/docs/live-prices) | Which prices are read at the moment you ask and which reflect the latest successful refresh — and why a detail page can legitimately show a higher price than the card it was opened from. |\n| [Fields by source](https://thecarapi.com/docs/fields-by-source) | Which of the supported sources actually publishes damages, service history, documents and equipment — and which do not. Read this before you promise a feature. |\n| [CO2 & emissions](https://thecarapi.com/docs/co2) | Why there are two CO2 fields, why an estimate always ships with a test standard attached, and why a 2019 figure is not comparable with a 2022 one. |\n\n> **Reading this reference with an agent** — Every page here is available as Markdown, plain text and JSON, and the whole endpoint surface as an OpenAPI document (OpenAPI 3.0.3 at `/openapi.json`). See [Agents & machine formats](https://thecarapi.com/docs/agents)."
    },
    {
      "id": "authentication",
      "label": "Authentication & scopes",
      "summary": "Three interchangeable ways to present a key, the restrictions a key can carry, and the scope groups that gate each endpoint.",
      "canonical": "https://thecarapi.com/docs/authentication",
      "blocks": [
        {
          "type": "p",
          "text": "Every endpoint except the two liveness probes (`/api/health/live` and `/api/health/ready`) requires an API key. Present the key in **one** of these ways:"
        },
        {
          "type": "table",
          "head": [
            "Method",
            "Header or parameter",
            "Notes"
          ],
          "rows": [
            [
              "Header",
              "`X-API-Key: <key>`",
              "Recommended for server-to-server calls."
            ],
            [
              "Bearer",
              "`Authorization: Bearer <key>`",
              "Equivalent to `X-API-Key`."
            ],
            [
              "Query string",
              "`?api_key=<key>`",
              "Disabled by default; use only when explicitly enabled for your key. Keys in URLs end up in logs and referrers."
            ]
          ]
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "# Header — the normal case\ncurl -H \"X-API-Key: $API_KEY\" \"https://api.thecarapi.com/api/sites\"\n\n# Bearer — identical behaviour, useful when your HTTP layer already speaks OAuth\ncurl -H \"Authorization: Bearer $API_KEY\" \"https://api.thecarapi.com/api/sites\""
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "Never ship a full-access key to a browser or mobile app",
          "text": "A key in client code is a public key, whatever the header is called. Issue a separate key with an origin allowlist and the minimum scopes, or proxy through your own backend."
        },
        {
          "type": "h2",
          "text": "Key restrictions"
        },
        {
          "type": "table",
          "head": [
            "Restriction",
            "Effect when violated"
          ],
          "rows": [
            [
              "IP or CIDR allowlist",
              "Requests from outside the list return `403`."
            ],
            [
              "Origin allowlist",
              "Browser requests from other origins return `403`."
            ],
            [
              "Expiry date",
              "Requests after the date return `401`."
            ],
            [
              "Status (revoked, suspended)",
              "All requests return `403`."
            ],
            [
              "Scope set",
              "A request to a route your key does not cover returns `403` with `{\"success\": false, \"error\": \"API key is not authorized for this route\"}`. There is no machine-readable error code in the body — branch on the status, not on the message text."
            ]
          ]
        },
        {
          "type": "p",
          "text": "Five failed authentication attempts from one IP trigger a temporary brute-force lockout: every subsequent request from that address answers `429` for **15 minutes**, including requests carrying a valid key. A successful authentication clears the counter, so a client that fixes its key on the first or second failure never reaches the threshold. Do not retry a `401` in a loop."
        },
        {
          "type": "h2",
          "text": "Published scope groups"
        },
        {
          "type": "table",
          "head": [
            "Scope",
            "Published access"
          ],
          "rows": [
            [
              "`search`",
              "Inventory search, filter facets (individually or combined via `/api/facets`), sources, and the full model catalog"
            ],
            [
              "`catalog`",
              "Manufacturer and model-group catalog"
            ],
            [
              "`seo`",
              "Popular searches and brand/model slug resolution"
            ],
            [
              "`auctions`",
              "Auction detail, batch detail, images, price history, and VIN history"
            ],
            [
              "`details`",
              "Full vehicle details, including upstream fetches"
            ],
            [
              "`top-offers`",
              "Auctions priced below their market reference"
            ],
            [
              "`classifieds`",
              "European classifieds feed, facets, models and origin-URL resolution (`theparking` remains a legacy alias). Bulk export is not included"
            ],
            [
              "`classifieds-export`",
              "Bulk classifieds export. Granted only to approved accounts"
            ],
            [
              "`archive`",
              "Historical inventory search, stored detail, coverage, facets and closing-price statistics"
            ],
            [
              "`market`",
              "Market valuation, plus the Cars.bg and auction market price snapshots"
            ],
            [
              "`ships`",
              "Vessel search, position and track, and the key’s own shipments"
            ],
            [
              "`watches`",
              "Saved vehicle searches with new-match polling and webhooks"
            ],
            [
              "`carvalue`",
              "The CarValue tool: valuation by VIN or make/model, and VIN scan from a photo. The key’s account must also be granted CarValue"
            ],
            [
              "`vehiclescan`",
              "The Vehicle Scan tool: saved vehicle requests and their matching offers. The key’s account must also be granted Vehicle Scan"
            ],
            [
              "`calculator`",
              "Import cost calculator and supported countries"
            ],
            [
              "`ops`",
              "Service health, the contract, and the API index"
            ]
          ]
        },
        {
          "type": "p",
          "text": "`*` grants every route. A scope may also be a literal URL rule rather than a group. `public` is a legacy compatibility bundle — search facets, catalog, SEO, auctions, calculator and health — preserved for older integrations; new keys are issued against the groups above."
        },
        {
          "type": "callout",
          "tone": "info",
          "title": "Endpoint, source and field permissions are separate",
          "text": "A managed key can restrict routes, allowed_sites and allowed_fields independently. Search and facets reflect the sources granted to the key; direct detail for a disallowed source returns 403. Optional price, market-reference and source-data fields may be omitted by the field grant. A missing field is not necessarily unavailable upstream. /api/contract describes the service schema, not an introspection of every permission on your key. Archive access needs an explicit archive grant."
        },
        {
          "type": "h2",
          "text": "Field grants"
        },
        {
          "type": "table",
          "head": [
            "Group",
            "Fields",
            "Default"
          ],
          "rows": [
            [
              "`listing`",
              "Identity, specifications, images, timing and VAT scheme",
              "Always retained"
            ],
            [
              "`source_prices`",
              "current_price, buy_now_price, start_price, final_price",
              "Included"
            ],
            [
              "`landed_prices`",
              "current_final, buynow_final, public_price_eur, current_bgn, buynow_bgn",
              "Included"
            ],
            [
              "`house_valuation`",
              "estimated_value_eur",
              "Included where available"
            ],
            [
              "`vehicle_identity`",
              "chassis_number, car_id, vin_ocr",
              "Included where available; VIN-history route access is separate"
            ],
            [
              "`crawl_meta`",
              "created_at, updated_at, first_seen_at, last_seen_at, last_available_at, last_changed_at",
              "Included where available"
            ],
            [
              "`landed_breakdown`",
              "current_tax, buynow_tax, current_tax_delivery, buynow_tax_delivery",
              "Separate grant"
            ],
            [
              "`market_reference`",
              "is_top_offer, top_offer_savings, top_offer_savings_pct, market_reference",
              "Separate grant"
            ],
            [
              "`korea_merge`",
              "source_comparison, canonical_offer",
              "Separate grant"
            ]
          ]
        },
        {
          "type": "p",
          "text": "These are provisioning defaults, not a promise about an existing key. Endpoint access does not automatically grant every response field: for example, top-offers can be callable while its market_reference block is omitted. Examples show illustrative fields; use the OpenAPI schema and handle omitted optional fields. Private bidder data and account credentials remain excluded regardless of field grants."
        },
        {
          "type": "h2",
          "text": "What a new key gets by default"
        },
        {
          "type": "p",
          "text": "Scopes are granted per endpoint, and a new customer key is issued with the common surface enabled and the rest switched off. If a route answers `403` on your key, it is not broken — it was not granted, and the operator can enable it."
        },
        {
          "type": "table",
          "head": [
            "",
            "Endpoints"
          ],
          "rows": [
            [
              "**Enabled by default**",
              "`/api/search`, `/api/brands`, `/api/models`, `/api/types`, `/api/years`, `/api/production-years`, `/api/fuels`, `/api/countries`, `/api/gearboxes`, `/api/sites`, `/load-models`, `/api/facets`, `/api/catalog/*`, `/api/seo/*`, `/api/auction/*` (including `/api/auction/batch`), `/api/auction-images/*`, `/api/vin/*`, `/api/archive/*`, `/api/calculator/*`, `/api/top-offers`, `/api/classifieds/*` (except export), `/api/car-details`, `/api/contract`, `/api/health`, `/`"
            ],
            [
              "**Granted per contract**",
              "`/api/market/valuation`, `/api/vessels/*`, `/api/shipments`, `/api/watches`, `/api/carvalue/*`, `/api/vehiclescan/*`"
            ],
            [
              "**Granted on request**",
              "`/api/cars-bg-market`, `/api/auction-market`, `/api/listVehicles` (and `/listVehicles`), `/api/search/auction-ids`"
            ],
            [
              "**Locked — approved accounts only**",
              "`/api/classifieds/export` (bulk export). Not available on a standard key; contact us to apply."
            ]
          ]
        },
        {
          "type": "p",
          "text": "`/api/health/live` and `/api/health/ready` need no key and are unaffected by scopes."
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "A scope can reach further than this reference documents",
          "text": "Some groups — `ops` among them — also cover internal routes that are not part of the contract. Those are unversioned, undocumented and may change or disappear without a changelog entry. Treat the endpoints in this reference as the whole of what your key is meant to call."
        },
        {
          "type": "callout",
          "tone": "info",
          "title": "Check scopes at startup, not per request",
          "text": "A missing scope is a deployment problem, not a runtime condition. Read `/api/contract` for schema capabilities, then make a small read request to each endpoint your integration needs. The contract does not list your key’s permissions; a 403 on the actual endpoint identifies a missing grant."
        }
      ],
      "markdown": "# Authentication & scopes\n\nThree interchangeable ways to present a key, the restrictions a key can carry, and the scope groups that gate each endpoint.\n\nEvery endpoint except the two liveness probes (`/api/health/live` and `/api/health/ready`) requires an API key. Present the key in **one** of these ways:\n\n| Method | Header or parameter | Notes |\n| --- | --- | --- |\n| Header | `X-API-Key: <key>` | Recommended for server-to-server calls. |\n| Bearer | `Authorization: Bearer <key>` | Equivalent to `X-API-Key`. |\n| Query string | `?api_key=<key>` | Disabled by default; use only when explicitly enabled for your key. Keys in URLs end up in logs and referrers. |\n\n```bash\n# Header — the normal case\ncurl -H \"X-API-Key: $API_KEY\" \"https://api.thecarapi.com/api/sites\"\n\n# Bearer — identical behaviour, useful when your HTTP layer already speaks OAuth\ncurl -H \"Authorization: Bearer $API_KEY\" \"https://api.thecarapi.com/api/sites\"\n```\n\n> **Never ship a full-access key to a browser or mobile app** — A key in client code is a public key, whatever the header is called. Issue a separate key with an origin allowlist and the minimum scopes, or proxy through your own backend.\n\n## Key restrictions\n\n| Restriction | Effect when violated |\n| --- | --- |\n| IP or CIDR allowlist | Requests from outside the list return `403`. |\n| Origin allowlist | Browser requests from other origins return `403`. |\n| Expiry date | Requests after the date return `401`. |\n| Status (revoked, suspended) | All requests return `403`. |\n| Scope set | A request to a route your key does not cover returns `403` with `{\"success\": false, \"error\": \"API key is not authorized for this route\"}`. There is no machine-readable error code in the body — branch on the status, not on the message text. |\n\nFive failed authentication attempts from one IP trigger a temporary brute-force lockout: every subsequent request from that address answers `429` for **15 minutes**, including requests carrying a valid key. A successful authentication clears the counter, so a client that fixes its key on the first or second failure never reaches the threshold. Do not retry a `401` in a loop.\n\n## Published scope groups\n\n| Scope | Published access |\n| --- | --- |\n| `search` | Inventory search, filter facets (individually or combined via `/api/facets`), sources, and the full model catalog |\n| `catalog` | Manufacturer and model-group catalog |\n| `seo` | Popular searches and brand/model slug resolution |\n| `auctions` | Auction detail, batch detail, images, price history, and VIN history |\n| `details` | Full vehicle details, including upstream fetches |\n| `top-offers` | Auctions priced below their market reference |\n| `classifieds` | European classifieds feed, facets, models and origin-URL resolution (`theparking` remains a legacy alias). Bulk export is not included |\n| `classifieds-export` | Bulk classifieds export. Granted only to approved accounts |\n| `archive` | Historical inventory search, stored detail, coverage, facets and closing-price statistics |\n| `market` | Market valuation, plus the Cars.bg and auction market price snapshots |\n| `ships` | Vessel search, position and track, and the key’s own shipments |\n| `watches` | Saved vehicle searches with new-match polling and webhooks |\n| `carvalue` | The CarValue tool: valuation by VIN or make/model, and VIN scan from a photo. The key’s account must also be granted CarValue |\n| `vehiclescan` | The Vehicle Scan tool: saved vehicle requests and their matching offers. The key’s account must also be granted Vehicle Scan |\n| `calculator` | Import cost calculator and supported countries |\n| `ops` | Service health, the contract, and the API index |\n\n`*` grants every route. A scope may also be a literal URL rule rather than a group. `public` is a legacy compatibility bundle — search facets, catalog, SEO, auctions, calculator and health — preserved for older integrations; new keys are issued against the groups above.\n\n> **Endpoint, source and field permissions are separate** — A managed key can restrict routes, allowed_sites and allowed_fields independently. Search and facets reflect the sources granted to the key; direct detail for a disallowed source returns 403. Optional price, market-reference and source-data fields may be omitted by the field grant. A missing field is not necessarily unavailable upstream. /api/contract describes the service schema, not an introspection of every permission on your key. Archive access needs an explicit archive grant.\n\n## Field grants\n\n| Group | Fields | Default |\n| --- | --- | --- |\n| `listing` | Identity, specifications, images, timing and VAT scheme | Always retained |\n| `source_prices` | current_price, buy_now_price, start_price, final_price | Included |\n| `landed_prices` | current_final, buynow_final, public_price_eur, current_bgn, buynow_bgn | Included |\n| `house_valuation` | estimated_value_eur | Included where available |\n| `vehicle_identity` | chassis_number, car_id, vin_ocr | Included where available; VIN-history route access is separate |\n| `crawl_meta` | created_at, updated_at, first_seen_at, last_seen_at, last_available_at, last_changed_at | Included where available |\n| `landed_breakdown` | current_tax, buynow_tax, current_tax_delivery, buynow_tax_delivery | Separate grant |\n| `market_reference` | is_top_offer, top_offer_savings, top_offer_savings_pct, market_reference | Separate grant |\n| `korea_merge` | source_comparison, canonical_offer | Separate grant |\n\nThese are provisioning defaults, not a promise about an existing key. Endpoint access does not automatically grant every response field: for example, top-offers can be callable while its market_reference block is omitted. Examples show illustrative fields; use the OpenAPI schema and handle omitted optional fields. Private bidder data and account credentials remain excluded regardless of field grants.\n\n## What a new key gets by default\n\nScopes are granted per endpoint, and a new customer key is issued with the common surface enabled and the rest switched off. If a route answers `403` on your key, it is not broken — it was not granted, and the operator can enable it.\n\n|  | Endpoints |\n| --- | --- |\n| **Enabled by default** | `/api/search`, `/api/brands`, `/api/models`, `/api/types`, `/api/years`, `/api/production-years`, `/api/fuels`, `/api/countries`, `/api/gearboxes`, `/api/sites`, `/load-models`, `/api/facets`, `/api/catalog/*`, `/api/seo/*`, `/api/auction/*` (including `/api/auction/batch`), `/api/auction-images/*`, `/api/vin/*`, `/api/archive/*`, `/api/calculator/*`, `/api/top-offers`, `/api/classifieds/*` (except export), `/api/car-details`, `/api/contract`, `/api/health`, `/` |\n| **Granted per contract** | `/api/market/valuation`, `/api/vessels/*`, `/api/shipments`, `/api/watches`, `/api/carvalue/*`, `/api/vehiclescan/*` |\n| **Granted on request** | `/api/cars-bg-market`, `/api/auction-market`, `/api/listVehicles` (and `/listVehicles`), `/api/search/auction-ids` |\n| **Locked — approved accounts only** | `/api/classifieds/export` (bulk export). Not available on a standard key; contact us to apply. |\n\n`/api/health/live` and `/api/health/ready` need no key and are unaffected by scopes.\n\n> **A scope can reach further than this reference documents** — Some groups — `ops` among them — also cover internal routes that are not part of the contract. Those are unversioned, undocumented and may change or disappear without a changelog entry. Treat the endpoints in this reference as the whole of what your key is meant to call.\n\n> **Check scopes at startup, not per request** — A missing scope is a deployment problem, not a runtime condition. Read `/api/contract` for schema capabilities, then make a small read request to each endpoint your integration needs. The contract does not list your key’s permissions; a 403 on the actual endpoint identifies a missing grant."
    },
    {
      "id": "conventions",
      "label": "Responses & pagination",
      "summary": "The response envelope, caching and conditional requests, compression, pagination aliases, and rate-limit headers.",
      "canonical": "https://thecarapi.com/docs/conventions",
      "blocks": [
        {
          "type": "h2",
          "text": "Response envelope"
        },
        {
          "type": "p",
          "text": "Successful responses use `success: true` and a route-specific payload key. Primary responses also include the fields below."
        },
        {
          "type": "table",
          "head": [
            "Field",
            "Type",
            "Meaning"
          ],
          "rows": [
            [
              "`contract_version`",
              "string",
              "Schema contract, currently `2026-10-05`."
            ],
            [
              "`request_id`",
              "string",
              "Correlation id, also returned in `X-Request-ID`."
            ],
            [
              "`server_time`",
              "timestamp",
              "UTC ISO-8601 response time."
            ],
            [
              "`data_updated_at`",
              "timestamp",
              "Route-provided data timestamp; defaults to server_time when no data timestamp is supplied. It is not a guaranteed source refresh time."
            ]
          ]
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "Not every route carries the metadata",
          "text": "The four contract fields are merged into search, facet, catalog, auction-detail, auction-image, top-offer, archive, classifieds and SEO responses. They are **absent** from `/load-models`, `/api/car-details`, `/api/listVehicles`, price history, VIN history, `/api/search/auction-ids`, the market snapshots, the calculator, `/api/contract`, the health routes and `GET /`. Every one of those still returns the correlation id on the `X-Request-ID` **header**, so read the header rather than the body field if you want one code path."
        },
        {
          "type": "p",
          "text": "`GET /load-models` goes further and returns no `success` flag either: its response *keys* are data (brand names) and nothing else may share that namespace. If you are writing a generic client wrapper that asserts `body.success`, special-case that one route."
        },
        {
          "type": "p",
          "text": "You may send your own `X-Request-ID` (up to 80 characters) and it will be echoed back."
        },
        {
          "type": "code",
          "lang": "json",
          "code": "{\n  \"success\": true,\n  \"results\": [ /* … */ ],\n  \"total\": 18342,\n  \"limit\": 24,\n  \"offset\": 0,\n  \"contract_version\": \"2026-10-05\",\n  \"request_id\": \"0f2c1b9e-4a77-4c31-9a0b-6d2f0a1c8e55\",\n  \"server_time\": \"2026-08-19T09:41:02Z\",\n  \"data_updated_at\": \"2026-08-19T06:00:00Z\"\n}"
        },
        {
          "type": "h2",
          "text": "Freshness and missing values"
        },
        {
          "type": "table",
          "head": [
            "Signal",
            "Meaning",
            "Client behavior"
          ],
          "rows": [
            [
              "`server_time` / `data_updated_at`",
              "Response time and route metadata; data_updated_at can default to server_time.",
              "Do not use these alone to claim that the source was just checked."
            ],
            [
              "`first_seen_at`, `last_seen_at`, `last_available_at`, `last_changed_at`",
              "Stored observations and changes to this listing, where available and permitted.",
              "Use alongside updated_at; inventory refresh, detail enrichment and image processing are separate."
            ],
            [
              "`live_price.fetched_at`",
              "When the overlay bid was read, in Unix seconds.",
              "Display its age. It may be reused within the live-price TTL."
            ],
            [
              "`details_pending` / `live_price_pending`",
              "Detail enrichment or a bid refresh is still running.",
              "Render known card data and use a bounded delayed follow-up; do not create a tight polling loop."
            ],
            [
              "Missing field / null / empty array",
              "A permission, missing source evidence, or incomplete enrichment can affect availability.",
              "Keep unknown distinct from zero, false, accident-free, and no service history."
            ]
          ]
        },
        {
          "type": "h2",
          "text": "Caching"
        },
        {
          "type": "p",
          "text": "Read routes can return `X-Cache: HIT`, `MISS`, or `STALE`. Facet and catalog responses cache for about 600 seconds; shallow search pages cache for about 300 seconds. Deep search pages (`offset > 5000`) are intentionally not cached, and a response carrying a refreshed live price drops its `max-age` to the live-price TTL."
        },
        {
          "type": "h2",
          "text": "Conditional requests: send the ETag back"
        },
        {
          "type": "p",
          "text": "Store the `ETag` from a response and put it in `If-None-Match` on the next request for the same URL. If nothing changed you get `304 Not Modified` with no body — keep using the copy you already hold. A client polling the same search page every few minutes replaces a multi-megabyte download with an empty response for as long as the results hold still."
        },
        {
          "type": "code",
          "lang": "http",
          "code": "GET /api/search?site=openlane&limit=100 HTTP/1.1\nX-API-Key: your_key\nIf-None-Match: W/\"6f1c0a9d8e...\"\n\nHTTP/1.1 304 Not Modified\nETag: W/\"6f1c0a9d8e...\""
        },
        {
          "type": "p",
          "text": "`/api/search` gained an `ETag` in contract `2026-08-19`; the facet and catalog routes already had one. ETags are **weak** (`W/\"…\"`) across the API by design, so one tag stays valid whether or not the body came back compressed. Compare them as opaque strings and echo them back exactly as received — do not strip the `W/` prefix or the quotes."
        },
        {
          "type": "p",
          "text": "Keep cached bodies and ETags scoped to the full request URL and API-key identity. Never reuse one customer’s response for another key: source and field grants can differ. A 304 has no JSON body, so retain the previously cached body and do not call response.json() on it."
        },
        {
          "type": "h2",
          "text": "Ask for compression"
        },
        {
          "type": "p",
          "text": "Send `Accept-Encoding: gzip` and any response over 2 KB comes back gzipped — on the order of an eighth of the bytes on a full 100-row search page. Below that threshold responses are sent uncompressed, where the header overhead would outweigh the saving. Most HTTP clients negotiate this and decompress transparently; a few (notably PHP Guzzle in certain configurations) have to be told to."
        },
        {
          "type": "code",
          "lang": "php",
          "code": "// Guzzle: decode_content is what turns \"Accept-Encoding: gzip\" into a decoded body.\n$client = new GuzzleHttp\\Client([\n    'base_uri' => 'https://api.thecarapi.com',\n    'headers'  => ['X-API-Key' => getenv('THECARAPI_KEY')],\n    'decode_content' => 'gzip',\n]);"
        },
        {
          "type": "h2",
          "text": "Pagination"
        },
        {
          "type": "table",
          "head": [
            "Parameter",
            "Meaning"
          ],
          "rows": [
            [
              "`limit` / `page_size`",
              "Page size. Max **100**; larger values are clamped down, not rejected. `limit=0` or a negative value is a `400`. Do not send both aliases."
            ],
            [
              "`offset`",
              "Zero-based row offset. Do not combine with `page`."
            ],
            [
              "`page`",
              "One-based page number; maps to `offset = (page - 1) * limit`. `page=0` is a `400`."
            ],
            [
              "`page` / `page_size` (response)",
              "The same window in page terms, echoed back so a client that paginated by `page` gets its own spelling. Both are `null` on a `count_only` response, which has no pages to describe. Carried by `/api/search`, the catalog routes and `/api/listVehicles`. `/api/top-offers` and `/api/classifieds/listings` return only `total`, `limit`, `offset` and `total_pages` — page by `offset` there."
            ],
            [
              "`total` / `total_pages`",
              "Null when totals are suppressed."
            ],
            [
              "`max_page`",
              "Deepest reachable page under the current depth policy."
            ],
            [
              "`include_total`",
              "Set `false` to skip counting. Cheaper on large result sets."
            ]
          ]
        },
        {
          "type": "p",
          "text": "Default page size is **100** on `/api/search` and **50** everywhere else (catalog, `/api/listVehicles`, `/api/top-offers`, `/api/classifieds/listings`). `limit` is capped at **100** across the API — larger values are clamped down rather than rejected."
        },
        {
          "type": "h3",
          "text": "Depth policy"
        },
        {
          "type": "ul",
          "items": [
            "**No offset cap.** `/api/search` and `/api/listVehicles` page to any depth, filtered or not, so `max_page` on those responses is simply `total_pages`. A very deep *filtered* search is answered from a compact index where it can be; where it cannot, it is subject to the safety timeout and may come back `503` asking you to narrow it. Pages past offset 5000 are also not cached, so they cost more.",
            "**Capped at offset 5000.** `/api/catalog/*`, `/api/top-offers` and `/api/classifieds/listings`. A deeper page is a `400` naming the limit; narrow the result set instead."
          ]
        },
        {
          "type": "p",
          "text": "The live cap is published as `pagination.max_offset` by `GET /api/contract`, alongside `pagination.max_limit`. Read those rather than hardcoding them."
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "Do not mix the two spellings",
          "text": "`page`/`page_size` and `offset`/`limit` are interchangeable ways of describing the same window, but combining them — `page` with `offset`, or `page_size` with `limit` — is a `400`."
        },
        {
          "type": "h2",
          "text": "Rate limits"
        },
        {
          "type": "p",
          "text": "Managed keys can carry fixed-window quotas on any of four periods: **minute**, **hour**, **day**, **month**. Windows are *fixed*, not sliding — a \"60 per minute\" key may send 60 requests at `12:00:59` and 60 more at `12:01:00`. That boundary burst is expected."
        },
        {
          "type": "p",
          "text": "A key can also be issued with **no** quota, and then none of the rate-limit headers are sent — their absence means \"unlimited\", not \"limit reached\". When a key does carry quotas, every request reports the window with the least headroom left, which is the one that will bite you next. All configured windows are checked before any is charged, so a request rejected by the day limit does not also spend a minute of your minute allowance."
        },
        {
          "type": "p",
          "text": "A quota breach returns `429` with `X-RateLimit-Remaining: 0`, names the window it broke (`{\"success\": false, \"error\": \"Rate limit exceeded (1000 per hour)\"}`), and includes `Retry-After` — honour it rather than backing off on a schedule of your own. `/api/facets` costs **one** request against your quota however many dimensions you ask for, which is the cheapest way to build a filter sidebar."
        },
        {
          "type": "code",
          "lang": "javascript",
          "code": "async function call(url, init = {}) {\n  for (let attempt = 0; attempt < 4; attempt++) {\n    const res = await fetch(url, init);\n\n    if (res.status === 429 || res.status >= 500) {\n      const wait = Number(res.headers.get(\"Retry-After\") ?? 2 ** attempt);\n      await new Promise((r) => setTimeout(r, wait * 1000));\n      continue;\n    }\n\n    // 4xx other than 429 is a bug in the request. Retrying cannot fix it.\n    if (!res.ok && res.status < 500) {\n      throw new Error(`${res.status} ${await res.text()} (request ${res.headers.get(\"X-Request-ID\")})`);\n    }\n    return res;\n  }\n  throw new Error(\"giving up after 4 attempts\");\n}",
          "caption": "Retry `429`, `503` and `5xx`. Never retry `400`, `401`, `403` or `404`."
        }
      ],
      "markdown": "# Responses & pagination\n\nThe response envelope, caching and conditional requests, compression, pagination aliases, and rate-limit headers.\n\n## Response envelope\n\nSuccessful responses use `success: true` and a route-specific payload key. Primary responses also include the fields below.\n\n| Field | Type | Meaning |\n| --- | --- | --- |\n| `contract_version` | string | Schema contract, currently `2026-10-05`. |\n| `request_id` | string | Correlation id, also returned in `X-Request-ID`. |\n| `server_time` | timestamp | UTC ISO-8601 response time. |\n| `data_updated_at` | timestamp | Route-provided data timestamp; defaults to server_time when no data timestamp is supplied. It is not a guaranteed source refresh time. |\n\n> **Not every route carries the metadata** — The four contract fields are merged into search, facet, catalog, auction-detail, auction-image, top-offer, archive, classifieds and SEO responses. They are **absent** from `/load-models`, `/api/car-details`, `/api/listVehicles`, price history, VIN history, `/api/search/auction-ids`, the market snapshots, the calculator, `/api/contract`, the health routes and `GET /`. Every one of those still returns the correlation id on the `X-Request-ID` **header**, so read the header rather than the body field if you want one code path.\n\n`GET /load-models` goes further and returns no `success` flag either: its response *keys* are data (brand names) and nothing else may share that namespace. If you are writing a generic client wrapper that asserts `body.success`, special-case that one route.\n\nYou may send your own `X-Request-ID` (up to 80 characters) and it will be echoed back.\n\n```json\n{\n  \"success\": true,\n  \"results\": [ /* … */ ],\n  \"total\": 18342,\n  \"limit\": 24,\n  \"offset\": 0,\n  \"contract_version\": \"2026-10-05\",\n  \"request_id\": \"0f2c1b9e-4a77-4c31-9a0b-6d2f0a1c8e55\",\n  \"server_time\": \"2026-08-19T09:41:02Z\",\n  \"data_updated_at\": \"2026-08-19T06:00:00Z\"\n}\n```\n\n## Freshness and missing values\n\n| Signal | Meaning | Client behavior |\n| --- | --- | --- |\n| `server_time` / `data_updated_at` | Response time and route metadata; data_updated_at can default to server_time. | Do not use these alone to claim that the source was just checked. |\n| `first_seen_at`, `last_seen_at`, `last_available_at`, `last_changed_at` | Stored observations and changes to this listing, where available and permitted. | Use alongside updated_at; inventory refresh, detail enrichment and image processing are separate. |\n| `live_price.fetched_at` | When the overlay bid was read, in Unix seconds. | Display its age. It may be reused within the live-price TTL. |\n| `details_pending` / `live_price_pending` | Detail enrichment or a bid refresh is still running. | Render known card data and use a bounded delayed follow-up; do not create a tight polling loop. |\n| Missing field / null / empty array | A permission, missing source evidence, or incomplete enrichment can affect availability. | Keep unknown distinct from zero, false, accident-free, and no service history. |\n\n## Caching\n\nRead routes can return `X-Cache: HIT`, `MISS`, or `STALE`. Facet and catalog responses cache for about 600 seconds; shallow search pages cache for about 300 seconds. Deep search pages (`offset > 5000`) are intentionally not cached, and a response carrying a refreshed live price drops its `max-age` to the live-price TTL.\n\n## Conditional requests: send the ETag back\n\nStore the `ETag` from a response and put it in `If-None-Match` on the next request for the same URL. If nothing changed you get `304 Not Modified` with no body — keep using the copy you already hold. A client polling the same search page every few minutes replaces a multi-megabyte download with an empty response for as long as the results hold still.\n\n```http\nGET /api/search?site=openlane&limit=100 HTTP/1.1\nX-API-Key: your_key\nIf-None-Match: W/\"6f1c0a9d8e...\"\n\nHTTP/1.1 304 Not Modified\nETag: W/\"6f1c0a9d8e...\"\n```\n\n`/api/search` gained an `ETag` in contract `2026-08-19`; the facet and catalog routes already had one. ETags are **weak** (`W/\"…\"`) across the API by design, so one tag stays valid whether or not the body came back compressed. Compare them as opaque strings and echo them back exactly as received — do not strip the `W/` prefix or the quotes.\n\nKeep cached bodies and ETags scoped to the full request URL and API-key identity. Never reuse one customer’s response for another key: source and field grants can differ. A 304 has no JSON body, so retain the previously cached body and do not call response.json() on it.\n\n## Ask for compression\n\nSend `Accept-Encoding: gzip` and any response over 2 KB comes back gzipped — on the order of an eighth of the bytes on a full 100-row search page. Below that threshold responses are sent uncompressed, where the header overhead would outweigh the saving. Most HTTP clients negotiate this and decompress transparently; a few (notably PHP Guzzle in certain configurations) have to be told to.\n\n```php\n// Guzzle: decode_content is what turns \"Accept-Encoding: gzip\" into a decoded body.\n$client = new GuzzleHttp\\Client([\n    'base_uri' => 'https://api.thecarapi.com',\n    'headers'  => ['X-API-Key' => getenv('THECARAPI_KEY')],\n    'decode_content' => 'gzip',\n]);\n```\n\n## Pagination\n\n| Parameter | Meaning |\n| --- | --- |\n| `limit` / `page_size` | Page size. Max **100**; larger values are clamped down, not rejected. `limit=0` or a negative value is a `400`. Do not send both aliases. |\n| `offset` | Zero-based row offset. Do not combine with `page`. |\n| `page` | One-based page number; maps to `offset = (page - 1) * limit`. `page=0` is a `400`. |\n| `page` / `page_size` (response) | The same window in page terms, echoed back so a client that paginated by `page` gets its own spelling. Both are `null` on a `count_only` response, which has no pages to describe. Carried by `/api/search`, the catalog routes and `/api/listVehicles`. `/api/top-offers` and `/api/classifieds/listings` return only `total`, `limit`, `offset` and `total_pages` — page by `offset` there. |\n| `total` / `total_pages` | Null when totals are suppressed. |\n| `max_page` | Deepest reachable page under the current depth policy. |\n| `include_total` | Set `false` to skip counting. Cheaper on large result sets. |\n\nDefault page size is **100** on `/api/search` and **50** everywhere else (catalog, `/api/listVehicles`, `/api/top-offers`, `/api/classifieds/listings`). `limit` is capped at **100** across the API — larger values are clamped down rather than rejected.\n\n### Depth policy\n\n- **No offset cap.** `/api/search` and `/api/listVehicles` page to any depth, filtered or not, so `max_page` on those responses is simply `total_pages`. A very deep *filtered* search is answered from a compact index where it can be; where it cannot, it is subject to the safety timeout and may come back `503` asking you to narrow it. Pages past offset 5000 are also not cached, so they cost more.\n- **Capped at offset 5000.** `/api/catalog/*`, `/api/top-offers` and `/api/classifieds/listings`. A deeper page is a `400` naming the limit; narrow the result set instead.\n\nThe live cap is published as `pagination.max_offset` by `GET /api/contract`, alongside `pagination.max_limit`. Read those rather than hardcoding them.\n\n> **Do not mix the two spellings** — `page`/`page_size` and `offset`/`limit` are interchangeable ways of describing the same window, but combining them — `page` with `offset`, or `page_size` with `limit` — is a `400`.\n\n## Rate limits\n\nManaged keys can carry fixed-window quotas on any of four periods: **minute**, **hour**, **day**, **month**. Windows are *fixed*, not sliding — a \"60 per minute\" key may send 60 requests at `12:00:59` and 60 more at `12:01:00`. That boundary burst is expected.\n\nA key can also be issued with **no** quota, and then none of the rate-limit headers are sent — their absence means \"unlimited\", not \"limit reached\". When a key does carry quotas, every request reports the window with the least headroom left, which is the one that will bite you next. All configured windows are checked before any is charged, so a request rejected by the day limit does not also spend a minute of your minute allowance.\n\nA quota breach returns `429` with `X-RateLimit-Remaining: 0`, names the window it broke (`{\"success\": false, \"error\": \"Rate limit exceeded (1000 per hour)\"}`), and includes `Retry-After` — honour it rather than backing off on a schedule of your own. `/api/facets` costs **one** request against your quota however many dimensions you ask for, which is the cheapest way to build a filter sidebar.\n\n```javascript\nasync function call(url, init = {}) {\n  for (let attempt = 0; attempt < 4; attempt++) {\n    const res = await fetch(url, init);\n\n    if (res.status === 429 || res.status >= 500) {\n      const wait = Number(res.headers.get(\"Retry-After\") ?? 2 ** attempt);\n      await new Promise((r) => setTimeout(r, wait * 1000));\n      continue;\n    }\n\n    // 4xx other than 429 is a bug in the request. Retrying cannot fix it.\n    if (!res.ok && res.status < 500) {\n      throw new Error(`${res.status} ${await res.text()} (request ${res.headers.get(\"X-Request-ID\")})`);\n    }\n    return res;\n  }\n  throw new Error(\"giving up after 4 attempts\");\n}\n```\n\n_Retry `429`, `503` and `5xx`. Never retry `400`, `401`, `403` or `404`._"
    },
    {
      "id": "sources",
      "label": "Sources & site slugs",
      "summary": "The `site` slug vocabulary, what each source publishes, and why the classifieds network is not one of them.",
      "canonical": "https://thecarapi.com/docs/sources",
      "blocks": [
        {
          "type": "p",
          "text": "A `site` slug identifies which auction source a listing came from. It is the value you pass to `/api/search?site=`, the first path segment of `/api/auction/{site}/{id}`, the `site` parameter of `/api/car-details`, and the `site_name` fee-model selector on the import calculator."
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "Read the slug list, do not hardcode it",
          "text": "Fetch the live set and counts from `/api/sites`. An unknown slug is a `400` naming the offender, not an empty result — a hardcoded list turns a new source into a broken deploy."
        },
        {
          "type": "table",
          "head": [
            "Slug",
            "Upstream",
            "Origin",
            "Inventory",
            "Detail payloads"
          ],
          "rows": [
            [
              "`auto1`",
              "Auto1 (EU)",
              "Per-listing",
              "Live auctions",
              "Full + normalized `vehicle_details`"
            ],
            [
              "`guazi`",
              "Guazi (China)",
              "CN",
              "Used-vehicle listings",
              "Stored inspections, specifications, videos and image evidence. Granted on request"
            ],
            [
              "`bca`",
              "BCA (EU)",
              "Source inventory",
              "Dealer auctions",
              "VIN, inspection and published price evidence where available"
            ],
            [
              "`autorola`",
              "Autorola (EU)",
              "Source inventory",
              "Dealer auctions",
              "VIN, condition, photos and auction context where available"
            ],
            [
              "`dealercars`",
              "Dealercars (EU)",
              "Source inventory",
              "Dealer auctions",
              "Appraisals, DEKRA evidence and published results where available"
            ],
            [
              "`koreaauctions`",
              "Korea Auctions (KR)",
              "Source inventory",
              "Korean auction houses",
              "Grades, VIN sheets and source starting-price evidence where available"
            ],
            [
              "`openlane`",
              "OpenLane (EU)",
              "Per-listing",
              "Live auctions",
              "Full + normalized `vehicle_details`"
            ],
            [
              "`schadeautos`",
              "Schadeautos (NL)",
              "NL",
              "Live listings",
              "Full"
            ],
            [
              "`copart`",
              "Copart Germany",
              "DE",
              "Live auctions",
              "Full"
            ],
            [
              "`encar`",
              "Encar (South Korea)",
              "KR",
              "Live cash listings",
              "Stored specs, options and available condition data"
            ],
            [
              "`kbchachacha`",
              "KB ChaChaCha (South Korea)",
              "KR",
              "Live cash listings",
              "Stored specs plus optional kb_diagnosis / kb_inspection"
            ],
            [
              "`japan`",
              "Japanese auction and dealer reseller feeds",
              "JP",
              "Current and historical imports",
              "Stored import only; no /api/car-details fetcher"
            ]
          ]
        },
        {
          "type": "h2",
          "text": "KB ChaChaCha and Encar"
        },
        {
          "type": "p",
          "text": "Both Korean sources are published through /api/search with origin KR. KB uses site=kbchachacha and a numeric carSeq for /api/car-details. Translated specifications, equipment and gallery data depend on successful ingestion. Korean lease, excluded and duplicate offers are filtered by the publishing pipeline, so source totals are not the sum of upstream website counts."
        },
        {
          "type": "p",
          "text": "KB detail may contain vehicle_details.kb_diagnosis and vehicle_details.kb_inspection. Each report carries status (available or unavailable), rows, checked_controls, panel_marks, images, documents and vin where collected. An unavailable report is not a promise that another request will produce one. Japan and KB do not support the live bidding overlay."
        },
        {
          "type": "h3",
          "text": "Cross-source duplicate resolution"
        },
        {
          "type": "p",
          "text": "The same Korean car is frequently listed on both marketplaces, at two different asking prices and with two different photo sets. Listings are grouped when they share a valid VIN, or a corroborated licence plate together with make and model. Matching specifications alone never establish identity — forty identically specified Sonatas are forty cars. One listing in a group is published as the representative and the rest are hidden from search, which is why Korean source totals are not the sum of the two upstream websites."
        },
        {
          "type": "p",
          "text": "With the korea_merge field grant, a reconciled detail response carries the evidence. source_comparison.offers lists every listing for that car with its site, auction id, asking price in EUR, its own offer_link and whether it is currently eligible; selected names the one published; spread_eur is the difference between the highest and lowest eligible price; match_method is vin or plate_make_model; and image_source names the listing the gallery was taken from — photo sets from two listings are never interleaved. A hidden duplicate carries canonical_offer pointing at the representative. Source rows and prices remain authoritative; nothing upstream is rewritten."
        },
        {
          "type": "h2",
          "text": "Coverage and refresh timing"
        },
        {
          "type": "p",
          "text": "This source list was reviewed against the implementation on 2026-09-20. /api/sites is the authority for deployed inventory visible to your key. A source listed here is an implemented integration, not a guarantee that a given deployment has completed its first import. Read listing timestamps and data_updated_at; source refreshes and detail enrichment run separately. The classifieds country and portal figures are a 2026-08-18 snapshot, not live counts."
        },
        {
          "type": "callout",
          "tone": "info",
          "title": "Canonical classifieds paths",
          "text": "Use /api/classifieds/listings, /api/classifieds/facets and /api/classifieds/models with the classifieds scope. The /api/theparking/* paths and theparking scope remain compatible legacy aliases. The existing /docs/theparking documentation URL remains available for bookmarked integrations."
        },
        {
          "type": "h2",
          "text": "Japanese auctions"
        },
        {
          "type": "p",
          "text": "The current Japanese feed uses `site=japan`. Search and stored auction detail expose imported auction and dealer offers, with originating reseller evidence under `car_identification.JapanOffers` when present. Use `auction_id_str` verbatim: the numeric identifier can exceed JavaScript precision. There is no Japan detail fetcher, so `/api/car-details` rejects this source. Do not use the retired website example slug `japanauction`; discover deployed source names through `/api/sites`."
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "A Japanese price is an opening bid, not a cost",
          "text": "Japanese `current_price` is an opening price where the imported sources agree, not a purchase quote. Conflicting price evidence can leave it null. A finished listing can carry a reported `final_price`, but its result basis is not verified: do not call it a confirmed hammer price. Delivered reseller totals are kept as evidence rather than substituted for the vehicle price."
        },
        {
          "type": "h3",
          "text": "Auction houses"
        },
        {
          "type": "p",
          "text": "Japanese lots come from 127 auction houses run by 24 operators. Each result names its house in `auction_house`, in the slug form below, and the same slug filters search: `?auction_house=uss_tokyo,honda_kansai`. Matching is by whole words, so `uss` selects every USS branch and `kobe` every Kobe house. Fixed-price dealer stock from Ippatsu, Kyouyuu, GAO and others is still being imported and is not listed yet."
        },
        {
          "type": "table",
          "head": [
            "Operator",
            "Houses",
            "`auction_house` values"
          ],
          "rows": [
            [
              "USS",
              "18",
              "`uss_tokyo`, `uss_nagoya`, `uss_haa_kobe`, `uss_yokohama`, `uss_sapporo`, `uss_kyushu`, `uss_nagoya_hokuriku`, `uss_osaka`, `uss_kobe`, `uss_jaa`, `uss_tohoku`, `uss_r_nagoya`, `uss_niigata`, `uss_shizuoka`, `uss_okayama`, `uss_gunma`, `uss_saitama`, `uss_fukuoka`"
            ],
            [
              "JU",
              "32",
              "`ju_aichi`, `ju_gifu`, `ju_miyagi`, `ju_fukuoka`, `ju_saitama`, `ju_chiba`, `ju_fukushima`, `ju_niigata`, `ju_tokyo`, `ju_hiroshima`, `ju_hokkaido`, `ju_mie`, `ju_gunma`, `ju_kanagawa`, `ju_shizuoka`, `ju_ibaraki`, `ju_okinawa`, `ju_tochigi`, `ju_ishikawa`, `ju_toyama`, `ju_yamaguchi`, `ju_oita`, `ju_nara`, `ju_nagano`, `ju_kumamoto`, `ju_fukui`, `ju_nagasaki`, `ju_shimane`, `ju_yamagata`, `ju_aomori`, `ju_miyazaki`, `ju_yamanashi`"
            ],
            [
              "TAA",
              "11",
              "`taa_yokohama`, `taa_chubu`, `taa_kantou`, `taa_kinki`, `taa_kyushu`, `taa_hiroshima`, `taa_touhoku`, `taa_hyogo`, `taa_shikoku`, `taa_hokkaido`, `taa_minamikyu`"
            ],
            [
              "CAA",
              "4",
              "`caa_chubu`, `caa_tokyo`, `caa_gifu`, `caa_touhoku`"
            ],
            [
              "ARAI",
              "5",
              "`arai_oyama_vt`, `arai_bayside`, `arai_sendai`, `arai_kenki`, `arai_oyama`"
            ],
            [
              "MIRIVE",
              "3",
              "`mirive_saitama`, `mirive_aichi`, `mirive_osaka`"
            ],
            [
              "Honda",
              "6",
              "`honda_tokyo`, `honda_kansai`, `honda_nagoya`, `honda_sendai`, `honda_kyushu`, `honda_hokkaido`"
            ],
            [
              "KCAA",
              "4",
              "`kcaa_fukuoka`, `kcaa_m_kyushu`, `kcaa_yamaguchi`, `kcaa_kyoto`"
            ],
            [
              "LAA",
              "2",
              "`laa_okayama`, `laa_shikoku`"
            ],
            [
              "NAA",
              "4",
              "`naa_nagoya`, `naa_osaka`, `naa_tokyo`, `naa_fukuoka`"
            ],
            [
              "Isuzu",
              "3",
              "`isuzu_kobe`, `isuzu_tokyo`, `isuzu_kyushu`"
            ],
            [
              "IAA",
              "1",
              "`iaa_osaka`"
            ],
            [
              "ORIX",
              "4",
              "`orix_kobe`, `orix_atsugi`, `orix_fukuoka`, `orix_sendai`"
            ],
            [
              "BAYAUC",
              "1",
              "`bayauc`"
            ],
            [
              "LUM",
              "5",
              "`lum_tokyo`, `lum_kobe`, `lum_fukuoka`, `lum_nagoya`, `lum_sapporo`"
            ],
            [
              "HERO",
              "1",
              "`hero`"
            ],
            [
              "ZIP",
              "2",
              "`zip_osaka`, `zip_tokyo`"
            ],
            [
              "SAA",
              "2",
              "`saa_sapporo`, `saa_hamamatsu`"
            ],
            [
              "AUCNET",
              "1",
              "`aucnet`"
            ],
            [
              "NPS",
              "7",
              "`nps_osaka`, `nps_tokyo`, `nps_fukuoka`, `nps_chubu`, `nps_sendai`, `nps_tochigi`, `nps_tomakomai`"
            ],
            [
              "ZERO",
              "6",
              "`zero_syonan`, `zero_chiba`, `zero_osaka`, `zero_hakata`, `zero_sendai`, `zero_hokkaido`"
            ],
            [
              "Nissan",
              "1",
              "`nissan_osaka`"
            ],
            [
              "SOGO",
              "3",
              "`sogo_narita3`, `sogo_internet`, `sogo_narita1`"
            ],
            [
              "AEP",
              "1",
              "`aep_gifu`"
            ]
          ]
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "`country=europe` is an exclusion, not a list",
          "text": "It means \"every origin that is not overseas\", so a source shipping from outside Europe has to be named for it to be excluded. It currently excludes `KR` (Encar and KB ChaChaCha) and `JP` (Japanese auctions); rows with no recorded country are treated as European and are included. If you need strict membership, pass explicit ISO codes instead."
        },
        {
          "type": "h2",
          "text": "Not a source: the European classifieds network"
        },
        {
          "type": "p",
          "text": "The classifieds network is retail market-reference data, not auction inventory: 9,808,689 live listings gathered from 681 origin portals across 39 European countries. It is never merged into the auction feed, and is deliberately a light dataset — no detail fetch, no image vault, no per-listing detail payload. It therefore never appears in `/api/sites` and is **not** a valid `site` value; passing it returns `400`. Query it through its own `/api/classifieds/*` endpoints under the `classifieds` scope — that slug is the scope name, not a source name."
        },
        {
          "type": "table",
          "head": [
            "",
            "Auction sources",
            "Classifieds network"
          ],
          "rows": [
            [
              "Queried through",
              "`/api/search?site=…`",
              "`/api/classifieds/listings`"
            ],
            [
              "Scope",
              "`search`, `auctions`, `details`",
              "`classifieds`"
            ],
            [
              "Price meaning",
              "Bid / buy-now / public EUR",
              "Retail asking price"
            ],
            [
              "Bidding & end date",
              "Source-dependent; Korean marketplaces are cash listings",
              "No"
            ],
            [
              "Detail payload",
              "Full, source-aware",
              "None — the listing row is the whole record"
            ],
            [
              "Images",
              "Vaulted CDN gallery",
              "One remote thumbnail, hotlinked"
            ],
            [
              "Appears in `/api/sites`",
              "Yes",
              "No"
            ]
          ]
        }
      ],
      "markdown": "# Sources & site slugs\n\nThe `site` slug vocabulary, what each source publishes, and why the classifieds network is not one of them.\n\nA `site` slug identifies which auction source a listing came from. It is the value you pass to `/api/search?site=`, the first path segment of `/api/auction/{site}/{id}`, the `site` parameter of `/api/car-details`, and the `site_name` fee-model selector on the import calculator.\n\n> **Read the slug list, do not hardcode it** — Fetch the live set and counts from `/api/sites`. An unknown slug is a `400` naming the offender, not an empty result — a hardcoded list turns a new source into a broken deploy.\n\n| Slug | Upstream | Origin | Inventory | Detail payloads |\n| --- | --- | --- | --- | --- |\n| `auto1` | Auto1 (EU) | Per-listing | Live auctions | Full + normalized `vehicle_details` |\n| `guazi` | Guazi (China) | CN | Used-vehicle listings | Stored inspections, specifications, videos and image evidence. Granted on request |\n| `bca` | BCA (EU) | Source inventory | Dealer auctions | VIN, inspection and published price evidence where available |\n| `autorola` | Autorola (EU) | Source inventory | Dealer auctions | VIN, condition, photos and auction context where available |\n| `dealercars` | Dealercars (EU) | Source inventory | Dealer auctions | Appraisals, DEKRA evidence and published results where available |\n| `koreaauctions` | Korea Auctions (KR) | Source inventory | Korean auction houses | Grades, VIN sheets and source starting-price evidence where available |\n| `openlane` | OpenLane (EU) | Per-listing | Live auctions | Full + normalized `vehicle_details` |\n| `schadeautos` | Schadeautos (NL) | NL | Live listings | Full |\n| `copart` | Copart Germany | DE | Live auctions | Full |\n| `encar` | Encar (South Korea) | KR | Live cash listings | Stored specs, options and available condition data |\n| `kbchachacha` | KB ChaChaCha (South Korea) | KR | Live cash listings | Stored specs plus optional kb_diagnosis / kb_inspection |\n| `japan` | Japanese auction and dealer reseller feeds | JP | Current and historical imports | Stored import only; no /api/car-details fetcher |\n\n## KB ChaChaCha and Encar\n\nBoth Korean sources are published through /api/search with origin KR. KB uses site=kbchachacha and a numeric carSeq for /api/car-details. Translated specifications, equipment and gallery data depend on successful ingestion. Korean lease, excluded and duplicate offers are filtered by the publishing pipeline, so source totals are not the sum of upstream website counts.\n\nKB detail may contain vehicle_details.kb_diagnosis and vehicle_details.kb_inspection. Each report carries status (available or unavailable), rows, checked_controls, panel_marks, images, documents and vin where collected. An unavailable report is not a promise that another request will produce one. Japan and KB do not support the live bidding overlay.\n\n### Cross-source duplicate resolution\n\nThe same Korean car is frequently listed on both marketplaces, at two different asking prices and with two different photo sets. Listings are grouped when they share a valid VIN, or a corroborated licence plate together with make and model. Matching specifications alone never establish identity — forty identically specified Sonatas are forty cars. One listing in a group is published as the representative and the rest are hidden from search, which is why Korean source totals are not the sum of the two upstream websites.\n\nWith the korea_merge field grant, a reconciled detail response carries the evidence. source_comparison.offers lists every listing for that car with its site, auction id, asking price in EUR, its own offer_link and whether it is currently eligible; selected names the one published; spread_eur is the difference between the highest and lowest eligible price; match_method is vin or plate_make_model; and image_source names the listing the gallery was taken from — photo sets from two listings are never interleaved. A hidden duplicate carries canonical_offer pointing at the representative. Source rows and prices remain authoritative; nothing upstream is rewritten.\n\n## Coverage and refresh timing\n\nThis source list was reviewed against the implementation on 2026-09-20. /api/sites is the authority for deployed inventory visible to your key. A source listed here is an implemented integration, not a guarantee that a given deployment has completed its first import. Read listing timestamps and data_updated_at; source refreshes and detail enrichment run separately. The classifieds country and portal figures are a 2026-08-18 snapshot, not live counts.\n\n> **Canonical classifieds paths** — Use /api/classifieds/listings, /api/classifieds/facets and /api/classifieds/models with the classifieds scope. The /api/theparking/* paths and theparking scope remain compatible legacy aliases. The existing /docs/theparking documentation URL remains available for bookmarked integrations.\n\n## Japanese auctions\n\nThe current Japanese feed uses `site=japan`. Search and stored auction detail expose imported auction and dealer offers, with originating reseller evidence under `car_identification.JapanOffers` when present. Use `auction_id_str` verbatim: the numeric identifier can exceed JavaScript precision. There is no Japan detail fetcher, so `/api/car-details` rejects this source. Do not use the retired website example slug `japanauction`; discover deployed source names through `/api/sites`.\n\n> **A Japanese price is an opening bid, not a cost** — Japanese `current_price` is an opening price where the imported sources agree, not a purchase quote. Conflicting price evidence can leave it null. A finished listing can carry a reported `final_price`, but its result basis is not verified: do not call it a confirmed hammer price. Delivered reseller totals are kept as evidence rather than substituted for the vehicle price.\n\n### Auction houses\n\nJapanese lots come from 127 auction houses run by 24 operators. Each result names its house in `auction_house`, in the slug form below, and the same slug filters search: `?auction_house=uss_tokyo,honda_kansai`. Matching is by whole words, so `uss` selects every USS branch and `kobe` every Kobe house. Fixed-price dealer stock from Ippatsu, Kyouyuu, GAO and others is still being imported and is not listed yet.\n\n| Operator | Houses | `auction_house` values |\n| --- | --- | --- |\n| USS | 18 | `uss_tokyo`, `uss_nagoya`, `uss_haa_kobe`, `uss_yokohama`, `uss_sapporo`, `uss_kyushu`, `uss_nagoya_hokuriku`, `uss_osaka`, `uss_kobe`, `uss_jaa`, `uss_tohoku`, `uss_r_nagoya`, `uss_niigata`, `uss_shizuoka`, `uss_okayama`, `uss_gunma`, `uss_saitama`, `uss_fukuoka` |\n| JU | 32 | `ju_aichi`, `ju_gifu`, `ju_miyagi`, `ju_fukuoka`, `ju_saitama`, `ju_chiba`, `ju_fukushima`, `ju_niigata`, `ju_tokyo`, `ju_hiroshima`, `ju_hokkaido`, `ju_mie`, `ju_gunma`, `ju_kanagawa`, `ju_shizuoka`, `ju_ibaraki`, `ju_okinawa`, `ju_tochigi`, `ju_ishikawa`, `ju_toyama`, `ju_yamaguchi`, `ju_oita`, `ju_nara`, `ju_nagano`, `ju_kumamoto`, `ju_fukui`, `ju_nagasaki`, `ju_shimane`, `ju_yamagata`, `ju_aomori`, `ju_miyazaki`, `ju_yamanashi` |\n| TAA | 11 | `taa_yokohama`, `taa_chubu`, `taa_kantou`, `taa_kinki`, `taa_kyushu`, `taa_hiroshima`, `taa_touhoku`, `taa_hyogo`, `taa_shikoku`, `taa_hokkaido`, `taa_minamikyu` |\n| CAA | 4 | `caa_chubu`, `caa_tokyo`, `caa_gifu`, `caa_touhoku` |\n| ARAI | 5 | `arai_oyama_vt`, `arai_bayside`, `arai_sendai`, `arai_kenki`, `arai_oyama` |\n| MIRIVE | 3 | `mirive_saitama`, `mirive_aichi`, `mirive_osaka` |\n| Honda | 6 | `honda_tokyo`, `honda_kansai`, `honda_nagoya`, `honda_sendai`, `honda_kyushu`, `honda_hokkaido` |\n| KCAA | 4 | `kcaa_fukuoka`, `kcaa_m_kyushu`, `kcaa_yamaguchi`, `kcaa_kyoto` |\n| LAA | 2 | `laa_okayama`, `laa_shikoku` |\n| NAA | 4 | `naa_nagoya`, `naa_osaka`, `naa_tokyo`, `naa_fukuoka` |\n| Isuzu | 3 | `isuzu_kobe`, `isuzu_tokyo`, `isuzu_kyushu` |\n| IAA | 1 | `iaa_osaka` |\n| ORIX | 4 | `orix_kobe`, `orix_atsugi`, `orix_fukuoka`, `orix_sendai` |\n| BAYAUC | 1 | `bayauc` |\n| LUM | 5 | `lum_tokyo`, `lum_kobe`, `lum_fukuoka`, `lum_nagoya`, `lum_sapporo` |\n| HERO | 1 | `hero` |\n| ZIP | 2 | `zip_osaka`, `zip_tokyo` |\n| SAA | 2 | `saa_sapporo`, `saa_hamamatsu` |\n| AUCNET | 1 | `aucnet` |\n| NPS | 7 | `nps_osaka`, `nps_tokyo`, `nps_fukuoka`, `nps_chubu`, `nps_sendai`, `nps_tochigi`, `nps_tomakomai` |\n| ZERO | 6 | `zero_syonan`, `zero_chiba`, `zero_osaka`, `zero_hakata`, `zero_sendai`, `zero_hokkaido` |\n| Nissan | 1 | `nissan_osaka` |\n| SOGO | 3 | `sogo_narita3`, `sogo_internet`, `sogo_narita1` |\n| AEP | 1 | `aep_gifu` |\n\n> **`country=europe` is an exclusion, not a list** — It means \"every origin that is not overseas\", so a source shipping from outside Europe has to be named for it to be excluded. It currently excludes `KR` (Encar and KB ChaChaCha) and `JP` (Japanese auctions); rows with no recorded country are treated as European and are included. If you need strict membership, pass explicit ISO codes instead.\n\n## Not a source: the European classifieds network\n\nThe classifieds network is retail market-reference data, not auction inventory: 9,808,689 live listings gathered from 681 origin portals across 39 European countries. It is never merged into the auction feed, and is deliberately a light dataset — no detail fetch, no image vault, no per-listing detail payload. It therefore never appears in `/api/sites` and is **not** a valid `site` value; passing it returns `400`. Query it through its own `/api/classifieds/*` endpoints under the `classifieds` scope — that slug is the scope name, not a source name.\n\n|  | Auction sources | Classifieds network |\n| --- | --- | --- |\n| Queried through | `/api/search?site=…` | `/api/classifieds/listings` |\n| Scope | `search`, `auctions`, `details` | `classifieds` |\n| Price meaning | Bid / buy-now / public EUR | Retail asking price |\n| Bidding & end date | Source-dependent; Korean marketplaces are cash listings | No |\n| Detail payload | Full, source-aware | None — the listing row is the whole record |\n| Images | Vaulted CDN gallery | One remote thumbnail, hotlinked |\n| Appears in `/api/sites` | Yes | No |"
    },
    {
      "id": "fields-by-source",
      "label": "Fields by source",
      "summary": "What each supported inventory source actually publishes — the condition-data coverage matrix, the source-specific traps, and how to write code that survives the differences.",
      "canonical": "https://thecarapi.com/docs/fields-by-source",
      "blocks": [
        {
          "type": "p",
          "text": "Supported inventory sources feed one API, and they do not publish the same things. A German wholesale platform ships a full inspection report and a DAT equipment catalogue; a Japanese export lot ships an auction inspection sheet and a condition grade; Korean listings carry specifications and optional condition evidence. This page is the honest map of who gives you what, so you can decide what your product can promise before you build it."
        },
        {
          "type": "callout",
          "tone": "tip",
          "title": "The one rule that saves you the most work",
          "text": "Prefer `vehicle_details` for normalized fields; render explicitly source-specific blocks such as KB inspection evidence when present. That block is the layer that already reconciled these differences for you. Drop to `car_identification` only for the handful of source-specific values listed below that have no cross-source equivalent."
        },
        {
          "type": "h2",
          "text": "Three layers of data"
        },
        {
          "type": "p",
          "text": "Every vehicle in the API is described at three levels of normalization. Knowing which layer a field lives on tells you immediately how portable it is."
        },
        {
          "type": "table",
          "head": [
            "Layer",
            "Where",
            "Consistency",
            "Use it for"
          ],
          "rows": [
            [
              "**The common core**",
              "Search result cards and the top level of both detail endpoints",
              "Identical across all supported sources",
              "Listings, filters, sorting, price display, countdowns — anything that has to work uniformly."
            ],
            [
              "**The normalized detail block**",
              "`vehicle_details` on `/api/auction/{site}/{id}` and `/api/car-details`",
              "Same key names everywhere; **presence varies** by source",
              "Condition, damages, documents, service history, equipment. Check for the key, do not assume it."
            ],
            [
              "**The raw source map**",
              "`car_identification`",
              "Source-specific. Key names change with each upstream",
              "The last mile — a value the source publishes that has no cross-source equivalent yet."
            ]
          ]
        },
        {
          "type": "p",
          "text": "The common core is guaranteed in the sense that the *field* is always in the contract; an individual value can still be `null` when a particular source does not publish it. `fuel_group`, for example, is empty for most Japanese lots — and a lot with no recorded fuel matches **every** `fuel` filter rather than none, so filtering by fuel never makes those lots unreachable."
        },
        {
          "type": "h2",
          "text": "What each source gives you"
        },
        {
          "type": "p",
          "text": "Read this table before you scope a feature. \"Sparse\" is not a bug to report — it is what the auction house publishes."
        },
        {
          "type": "table",
          "head": [
            "Source",
            "What it is",
            "Depth",
            "What only it gives you"
          ],
          "rows": [
            [
              "`auto1`",
              "Europe's largest digital B2B wholesale marketplace",
              "**Deep** — full `vehicle_details`",
              "The richest equipment data in the API (DAT catalogue), paint-thickness measurements, wheel condition, structured damages with a summary and prior-damage history."
            ],
            [
              "`openlane`",
              "Pan-European ex-lease and fleet auctions",
              "**Deep** — full `vehicle_details`",
              "Per-damage free-text comments, typed report PDFs (internal damage, external damage, maintenance, battery health) and its own report index in `car_reports`."
            ],
            [
              "`schadeautos`",
              "Netherlands and Benelux salvage and repairable stock",
              "Moderate",
              "Salvage-focused fields: engine capacity, transmission, VAT/margin status, net and gross export pricing, narrative damage text."
            ],
            [
              "`copart`",
              "Copart Germany salvage and run-and-drive lots",
              "Moderate",
              "Run-and-drive signal, build-sheet series and special equipment, published lot prices; no at-request live-price overlay."
            ],
            [
              "`encar`",
              "South Korea's dominant vehicle marketplace",
              "Moderate — deep specs, shallow condition",
              "A large specification and options block, photo counts grouped by type, and registration data. Condition and accident detail is thinner than the EU sources."
            ],
            [
              "`kbchachacha`",
              "South Korean cash-sale marketplace",
              "Specs and optional inspection evidence",
              "vehicle_details.kb_diagnosis / kb_inspection: status, report rows, checked controls, panel marks, images and VIN when present."
            ],
            [
              "`japan`",
              "Japanese export inventory from 127 auction houses (USS, JU, TAA, CAA, ARAI, Honda and others)",
              "**Shallow** — expect `vehicle_details` sparse or absent",
              "Imported photos, optional sheets and grades, originating sale information and matched reseller offer evidence. No on-demand detail fetcher."
            ]
          ]
        },
        {
          "type": "h2",
          "text": "Condition data: the coverage matrix"
        },
        {
          "type": "p",
          "text": "This is the table people actually need. It says which keys of `vehicle_details` you can expect from which source. Blank means the current integration does not promise that field. It may be unavailable upstream, not yet enriched, or omitted by permissions; absence is not a clean-condition verdict."
        },
        {
          "type": "table",
          "head": [
            "",
            "`auto1`",
            "`openlane`",
            "Other sources"
          ],
          "rows": [
            [
              "`damages` (per panel)",
              "Structured, with summary, count and prior damages",
              "Structured, with a comment per damage",
              "Sparse or absent"
            ],
            [
              "`inspection_reports`",
              "One URL",
              "Typed PDFs by `type_id`",
              "KB uses kb_diagnosis / kb_inspection. Japan sheets are optional imported evidence."
            ],
            [
              "`documents`",
              "Service book + service-detail images",
              "**None** — `paperwork` booleans only",
              "—"
            ],
            [
              "`service_history`",
              "Summary only",
              "Summary only",
              "—"
            ],
            [
              "`equipment`",
              "DAT catalogue (richest)",
              "Option list + accessories",
              "Varies"
            ],
            [
              "`paperwork`",
              "Partial",
              "`COCAvailable`, `RegistrationDocsAvailable`, `IsV5Present`, `LicensePlate`",
              "—"
            ],
            [
              "`estimated_repair_costs`",
              "Yes",
              "Yes",
              "—"
            ]
          ]
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "Stored report files and redirects",
          "text": "Normalized `documents`, `inspection_reports`, `option_reports` and `car_reports` use `/report-vault/{site}/{auction_id}/{token}` URLs. Resolve relative paths against the API base. Report files are fetched and cached, with `X-Report-Cache: HIT` or `MISS`; `?download=1` requests an attachment. Unsupported hosts and non-file pages can redirect upstream. Reports are optional: absence alone does not establish whether a report exists upstream. Check details_pending separately. KB diagnosis and inspection retain their source-shaped nested evidence; do not assume every nested URL is a vaulted report."
        },
        {
          "type": "h2",
          "text": "Source-specific notes worth knowing"
        },
        {
          "type": "h3",
          "text": "Japanese auctions — the price is an opening bid"
        },
        {
          "type": "p",
          "text": "A live Japanese lot has no hammer price until it closes, so `current_price` on `japan` is the **opening bid**, not a cost. Some lots publish no opener, and conflicting imported prices are withheld. Nominal starters can be far below the eventual price. Read it as a floor. `buy_now_price` is never set on this source, and best-value ranking will flag some of these lots optimistically as a result."
        },
        {
          "type": "p",
          "text": "Japanese rows carry origin JP. Use `country=JP` or `site=japan`, and `/api/auction/japan/{id}` for stored details. `/api/car-details` does not accept Japan. Use the string id from search without converting it to a JavaScript number."
        },
        {
          "type": "h3",
          "text": "OpenLane — report types"
        },
        {
          "type": "p",
          "text": "OpenLane classifies each report it publishes. `type` is the readable name and `type_id` the source's raw code."
        },
        {
          "type": "table",
          "head": [
            "`type_id`",
            "`type`",
            "Appears in"
          ],
          "rows": [
            [
              "1",
              "`internal_damage_report`",
              "`inspection_reports`"
            ],
            [
              "2",
              "`external_damage_report`",
              "`inspection_reports`"
            ],
            [
              "3",
              "`maintenance_records`",
              "`inspection_reports`"
            ],
            [
              "4",
              "`option_report`",
              "`option_reports`"
            ],
            [
              "5",
              "`battery_health_report`",
              "`inspection_reports`"
            ],
            [
              "other",
              "`report`",
              "`inspection_reports`"
            ]
          ]
        },
        {
          "type": "p",
          "text": "Auto1's single inspection report is normalized into the same list with type `inspection_report` and no `type_id`."
        },
        {
          "type": "h3",
          "text": "Japanese auctions — the auction sheet"
        },
        {
          "type": "p",
          "text": "Japanese auction sheets and photos are optional imported evidence. Use the returned gallery and available report entries; the current importer does not promise a normalized `auction_sheet` entry on every lot, an extracted VIN, or live upstream detail fetching. The source specification and matched reseller evidence remain under `car_identification`; imported sheet URLs are under `JapanMerged.AuctionSheetUrls`."
        },
        {
          "type": "h3",
          "text": "Opening and downloading reports"
        },
        {
          "type": "p",
          "text": "Follow the exact returned `/report-vault/{site}/{auction_id}/{token}` URL; the token identifies an already-known report and is not an upstream URL you can construct. This delivery route currently needs no API key. A successful response is a binary file with its Content-Type, not a JSON envelope. `download=1` requests an attachment. A 302 points to an upstream page; a 400 means an invalid token, a 404 means no matching report, and a 503 means the backing database is unavailable. Do not forward your API key to an upstream redirect target."
        },
        {
          "type": "h3",
          "text": "KB ChaChaCha — diagnosis and inspection"
        },
        {
          "type": "p",
          "text": "Read `vehicle_details.kb_diagnosis` and `vehicle_details.kb_inspection` independently: one report can be available while the other is unavailable. They preserve the source evidence rather than turning it into a universal accident score. Use the numeric `carSeq` for `/api/car-details?site=kbchachacha&id=...`, or the exact stored identifier from a search card for `/api/auction/kbchachacha/{id}`."
        },
        {
          "type": "table",
          "head": [
            "Report field",
            "Shape",
            "How to use it"
          ],
          "rows": [
            [
              "`status`",
              "`available` or `unavailable`",
              "Unavailable means there is no usable report; it does not mean no damage. An absent block may also mean details have not been fetched."
            ],
            [
              "`rows`",
              "`{cells, cells_en}[]`",
              "Ordered table rows with original and translated cells. Preserve row order and repeated labels rather than converting to a dictionary."
            ],
            [
              "`checked_controls`",
              "`{name, id, value, label}[]`",
              "Selected controls on the source report. Keep the associated label; a raw checked value alone is not a diagnosis."
            ],
            [
              "`panel_marks`",
              "`{position, panel, code, condition}[]`",
              "X replacement, C corrosion, W panel repair/welding, A scratch, U unevenness, T damage. Source position and diagram panel number differ. Some marks instead carry element_id and condition_original from the source diagram."
            ],
            [
              "`images` / `documents`",
              "arrays",
              "Optional visual evidence and document records. Use returned URLs; these nested links are not uniformly rewritten through the report vault."
            ],
            [
              "`vin`",
              "string or null",
              "Present only when a valid VIN is extracted. Missing VIN is not evidence that two listings are different vehicles."
            ]
          ]
        },
        {
          "type": "h3",
          "text": "Naming collisions to be aware of"
        },
        {
          "type": "p",
          "text": "The same word means different things upstream, which is precisely why the normalized block exists. A downloadable file is `Documents[]` on Auto1 — while `Documents` on OpenLane is a dictionary of paperwork **booleans** and not a file list at all. If you read `car_identification` directly, you own that problem. If you read `vehicle_details.documents`, you do not."
        },
        {
          "type": "h2",
          "text": "Steering side"
        },
        {
          "type": "p",
          "text": "`steering` is normalized to `left` or `right`. Explicit source evidence is used where supported; otherwise it is inferred from registration market. It is an inferred field where no explicit source value exists, not a universal physical inspection guarantee. Japanese imports may include left-hand-drive vehicles, so do not derive steering from country in your client."
        },
        {
          "type": "p",
          "text": "Because every listing resolves to one side or the other, the two values partition the inventory: their counts sum to the unfiltered total. As a filter it takes two values, each with four equivalent spellings — `left`, `lhd`, `left-hand`, `left_hand` and `right`, `rhd`, `right-hand`, `right_hand` — and anything else is a `400`. It is accepted by `/api/search`, `/api/top-offers`, `/api/listVehicles` and every facet endpoint — though on a facet endpoint it is computed live rather than read from the precomputed table, so it is slower than an unfiltered facet call."
        },
        {
          "type": "h2",
          "text": "Writing code that survives the differences"
        },
        {
          "type": "code",
          "lang": "javascript",
          "code": "Everyone else has a summary.\n    serviceVisits: d.service_history ?? null,\n    serviceSummary: d.service_history_summary ?? null,\n\n    // Most listings carry no reports at all. Empty is normal, not an error.\n    reports: d.inspection_reports ?? [],\n\n    repairCost: d.estimated_repair_costs ?? null,\n  };\n}\n\n// Wrong: this breaks the day a source is added, and hides data today.\nif (auction.site_name === \"auto1\") { /* ... */ }",
          "caption": "A missing key can mean unavailable evidence, pending enrichment or a field permission; do not interpret it as a negative finding."
        },
        {
          "type": "ul",
          "items": [
            "**Never hardcode the source list.** Read it from `/api/sites` at startup. An unknown slug is a `400` naming the offender, so a stale hardcoded list turns a new source into a broken deploy.",
            "**Design for the shallowest source you serve.** No source guarantees full service history on every car. Promise it where it exists and degrade gracefully where it does not.",
            "**Treat `car_identification` as unversioned.** Its keys follow each upstream's own naming and change when the upstream changes. Anything you depend on long-term should come from the common core or `vehicle_details`.",
            "**`details_pending: true` is not an error.** It means the detail payload has not been assembled yet. Poll rather than retrying in a tight loop, and render the card data you already have."
          ]
        }
      ],
      "markdown": "# Fields by source\n\nWhat each supported inventory source actually publishes — the condition-data coverage matrix, the source-specific traps, and how to write code that survives the differences.\n\nSupported inventory sources feed one API, and they do not publish the same things. A German wholesale platform ships a full inspection report and a DAT equipment catalogue; a Japanese export lot ships an auction inspection sheet and a condition grade; Korean listings carry specifications and optional condition evidence. This page is the honest map of who gives you what, so you can decide what your product can promise before you build it.\n\n> **The one rule that saves you the most work** — Prefer `vehicle_details` for normalized fields; render explicitly source-specific blocks such as KB inspection evidence when present. That block is the layer that already reconciled these differences for you. Drop to `car_identification` only for the handful of source-specific values listed below that have no cross-source equivalent.\n\n## Three layers of data\n\nEvery vehicle in the API is described at three levels of normalization. Knowing which layer a field lives on tells you immediately how portable it is.\n\n| Layer | Where | Consistency | Use it for |\n| --- | --- | --- | --- |\n| **The common core** | Search result cards and the top level of both detail endpoints | Identical across all supported sources | Listings, filters, sorting, price display, countdowns — anything that has to work uniformly. |\n| **The normalized detail block** | `vehicle_details` on `/api/auction/{site}/{id}` and `/api/car-details` | Same key names everywhere; **presence varies** by source | Condition, damages, documents, service history, equipment. Check for the key, do not assume it. |\n| **The raw source map** | `car_identification` | Source-specific. Key names change with each upstream | The last mile — a value the source publishes that has no cross-source equivalent yet. |\n\nThe common core is guaranteed in the sense that the *field* is always in the contract; an individual value can still be `null` when a particular source does not publish it. `fuel_group`, for example, is empty for most Japanese lots — and a lot with no recorded fuel matches **every** `fuel` filter rather than none, so filtering by fuel never makes those lots unreachable.\n\n## What each source gives you\n\nRead this table before you scope a feature. \"Sparse\" is not a bug to report — it is what the auction house publishes.\n\n| Source | What it is | Depth | What only it gives you |\n| --- | --- | --- | --- |\n| `auto1` | Europe's largest digital B2B wholesale marketplace | **Deep** — full `vehicle_details` | The richest equipment data in the API (DAT catalogue), paint-thickness measurements, wheel condition, structured damages with a summary and prior-damage history. |\n| `openlane` | Pan-European ex-lease and fleet auctions | **Deep** — full `vehicle_details` | Per-damage free-text comments, typed report PDFs (internal damage, external damage, maintenance, battery health) and its own report index in `car_reports`. |\n| `schadeautos` | Netherlands and Benelux salvage and repairable stock | Moderate | Salvage-focused fields: engine capacity, transmission, VAT/margin status, net and gross export pricing, narrative damage text. |\n| `copart` | Copart Germany salvage and run-and-drive lots | Moderate | Run-and-drive signal, build-sheet series and special equipment, published lot prices; no at-request live-price overlay. |\n| `encar` | South Korea's dominant vehicle marketplace | Moderate — deep specs, shallow condition | A large specification and options block, photo counts grouped by type, and registration data. Condition and accident detail is thinner than the EU sources. |\n| `kbchachacha` | South Korean cash-sale marketplace | Specs and optional inspection evidence | vehicle_details.kb_diagnosis / kb_inspection: status, report rows, checked controls, panel marks, images and VIN when present. |\n| `japan` | Japanese export inventory from 127 auction houses (USS, JU, TAA, CAA, ARAI, Honda and others) | **Shallow** — expect `vehicle_details` sparse or absent | Imported photos, optional sheets and grades, originating sale information and matched reseller offer evidence. No on-demand detail fetcher. |\n\n## Condition data: the coverage matrix\n\nThis is the table people actually need. It says which keys of `vehicle_details` you can expect from which source. Blank means the current integration does not promise that field. It may be unavailable upstream, not yet enriched, or omitted by permissions; absence is not a clean-condition verdict.\n\n|  | `auto1` | `openlane` | Other sources |\n| --- | --- | --- | --- |\n| `damages` (per panel) | Structured, with summary, count and prior damages | Structured, with a comment per damage | Sparse or absent |\n| `inspection_reports` | One URL | Typed PDFs by `type_id` | KB uses kb_diagnosis / kb_inspection. Japan sheets are optional imported evidence. |\n| `documents` | Service book + service-detail images | **None** — `paperwork` booleans only | — |\n| `service_history` | Summary only | Summary only | — |\n| `equipment` | DAT catalogue (richest) | Option list + accessories | Varies |\n| `paperwork` | Partial | `COCAvailable`, `RegistrationDocsAvailable`, `IsV5Present`, `LicensePlate` | — |\n| `estimated_repair_costs` | Yes | Yes | — |\n\n> **Stored report files and redirects** — Normalized `documents`, `inspection_reports`, `option_reports` and `car_reports` use `/report-vault/{site}/{auction_id}/{token}` URLs. Resolve relative paths against the API base. Report files are fetched and cached, with `X-Report-Cache: HIT` or `MISS`; `?download=1` requests an attachment. Unsupported hosts and non-file pages can redirect upstream. Reports are optional: absence alone does not establish whether a report exists upstream. Check details_pending separately. KB diagnosis and inspection retain their source-shaped nested evidence; do not assume every nested URL is a vaulted report.\n\n## Source-specific notes worth knowing\n\n### Japanese auctions — the price is an opening bid\n\nA live Japanese lot has no hammer price until it closes, so `current_price` on `japan` is the **opening bid**, not a cost. Some lots publish no opener, and conflicting imported prices are withheld. Nominal starters can be far below the eventual price. Read it as a floor. `buy_now_price` is never set on this source, and best-value ranking will flag some of these lots optimistically as a result.\n\nJapanese rows carry origin JP. Use `country=JP` or `site=japan`, and `/api/auction/japan/{id}` for stored details. `/api/car-details` does not accept Japan. Use the string id from search without converting it to a JavaScript number.\n\n### OpenLane — report types\n\nOpenLane classifies each report it publishes. `type` is the readable name and `type_id` the source's raw code.\n\n| `type_id` | `type` | Appears in |\n| --- | --- | --- |\n| 1 | `internal_damage_report` | `inspection_reports` |\n| 2 | `external_damage_report` | `inspection_reports` |\n| 3 | `maintenance_records` | `inspection_reports` |\n| 4 | `option_report` | `option_reports` |\n| 5 | `battery_health_report` | `inspection_reports` |\n| other | `report` | `inspection_reports` |\n\nAuto1's single inspection report is normalized into the same list with type `inspection_report` and no `type_id`.\n\n### Japanese auctions — the auction sheet\n\nJapanese auction sheets and photos are optional imported evidence. Use the returned gallery and available report entries; the current importer does not promise a normalized `auction_sheet` entry on every lot, an extracted VIN, or live upstream detail fetching. The source specification and matched reseller evidence remain under `car_identification`; imported sheet URLs are under `JapanMerged.AuctionSheetUrls`.\n\n### Opening and downloading reports\n\nFollow the exact returned `/report-vault/{site}/{auction_id}/{token}` URL; the token identifies an already-known report and is not an upstream URL you can construct. This delivery route currently needs no API key. A successful response is a binary file with its Content-Type, not a JSON envelope. `download=1` requests an attachment. A 302 points to an upstream page; a 400 means an invalid token, a 404 means no matching report, and a 503 means the backing database is unavailable. Do not forward your API key to an upstream redirect target.\n\n### KB ChaChaCha — diagnosis and inspection\n\nRead `vehicle_details.kb_diagnosis` and `vehicle_details.kb_inspection` independently: one report can be available while the other is unavailable. They preserve the source evidence rather than turning it into a universal accident score. Use the numeric `carSeq` for `/api/car-details?site=kbchachacha&id=...`, or the exact stored identifier from a search card for `/api/auction/kbchachacha/{id}`.\n\n| Report field | Shape | How to use it |\n| --- | --- | --- |\n| `status` | `available` or `unavailable` | Unavailable means there is no usable report; it does not mean no damage. An absent block may also mean details have not been fetched. |\n| `rows` | `{cells, cells_en}[]` | Ordered table rows with original and translated cells. Preserve row order and repeated labels rather than converting to a dictionary. |\n| `checked_controls` | `{name, id, value, label}[]` | Selected controls on the source report. Keep the associated label; a raw checked value alone is not a diagnosis. |\n| `panel_marks` | `{position, panel, code, condition}[]` | X replacement, C corrosion, W panel repair/welding, A scratch, U unevenness, T damage. Source position and diagram panel number differ. Some marks instead carry element_id and condition_original from the source diagram. |\n| `images` / `documents` | arrays | Optional visual evidence and document records. Use returned URLs; these nested links are not uniformly rewritten through the report vault. |\n| `vin` | string or null | Present only when a valid VIN is extracted. Missing VIN is not evidence that two listings are different vehicles. |\n\n### Naming collisions to be aware of\n\nThe same word means different things upstream, which is precisely why the normalized block exists. A downloadable file is `Documents[]` on Auto1 — while `Documents` on OpenLane is a dictionary of paperwork **booleans** and not a file list at all. If you read `car_identification` directly, you own that problem. If you read `vehicle_details.documents`, you do not.\n\n## Steering side\n\n`steering` is normalized to `left` or `right`. Explicit source evidence is used where supported; otherwise it is inferred from registration market. It is an inferred field where no explicit source value exists, not a universal physical inspection guarantee. Japanese imports may include left-hand-drive vehicles, so do not derive steering from country in your client.\n\nBecause every listing resolves to one side or the other, the two values partition the inventory: their counts sum to the unfiltered total. As a filter it takes two values, each with four equivalent spellings — `left`, `lhd`, `left-hand`, `left_hand` and `right`, `rhd`, `right-hand`, `right_hand` — and anything else is a `400`. It is accepted by `/api/search`, `/api/top-offers`, `/api/listVehicles` and every facet endpoint — though on a facet endpoint it is computed live rather than read from the precomputed table, so it is slower than an unfiltered facet call.\n\n## Writing code that survives the differences\n\n```javascript\nEveryone else has a summary.\n    serviceVisits: d.service_history ?? null,\n    serviceSummary: d.service_history_summary ?? null,\n\n    // Most listings carry no reports at all. Empty is normal, not an error.\n    reports: d.inspection_reports ?? [],\n\n    repairCost: d.estimated_repair_costs ?? null,\n  };\n}\n\n// Wrong: this breaks the day a source is added, and hides data today.\nif (auction.site_name === \"auto1\") { /* ... */ }\n```\n\n_A missing key can mean unavailable evidence, pending enrichment or a field permission; do not interpret it as a negative finding._\n\n- **Never hardcode the source list.** Read it from `/api/sites` at startup. An unknown slug is a `400` naming the offender, so a stale hardcoded list turns a new source into a broken deploy.\n- **Design for the shallowest source you serve.** No source guarantees full service history on every car. Promise it where it exists and degrade gracefully where it does not.\n- **Treat `car_identification` as unversioned.** Its keys follow each upstream's own naming and change when the upstream changes. Anything you depend on long-term should come from the common core or `vehicle_details`.\n- **`details_pending: true` is not an error.** It means the detail payload has not been assembled yet. Poll rather than retrying in a tight loop, and render the card data you already have."
    },
    {
      "id": "co2",
      "label": "CO2 & emissions",
      "summary": "The difference between the measured and estimated CO2 fields, the NEDC/WLTP split that makes a standard tag mandatory, which sources publish a figure, and how to render it without overclaiming.",
      "canonical": "https://thecarapi.com/docs/co2",
      "blocks": [
        {
          "type": "p",
          "text": "Roughly half of EU auction lots publish a CO2 figure and the rest publish nothing at all. The API therefore carries **two** CO2 fields with deliberately different meanings, plus a third that says which test standard the second one is on. Rendering them as one number is the single most common way to get emissions wrong."
        },
        {
          "type": "table",
          "head": [
            "Field",
            "Type",
            "What it is"
          ],
          "rows": [
            [
              "`co2`",
              "`integer | null`",
              "**Measured.** The figure the auction house itself published, in g/km. `null` on the roughly half of lots that publish none. Carries no standard tag."
            ],
            [
              "`co2_estimated`",
              "`integer | null`",
              "**Derived.** Inferred for lots that publish nothing, from other lots of the same specification. Never a measured value. `null` wherever the specification does not decide it."
            ],
            [
              "`co2_estimated_standard`",
              "`\"NEDC\" | \"WLTP\" | null`",
              "Which EU test cycle `co2_estimated` is expressed on. Always read the estimate together with this."
            ]
          ]
        },
        {
          "type": "callout",
          "tone": "info",
          "title": "Why they are two fields and not one",
          "text": "An estimate must never be able to masquerade as a measurement. Estimates are deliberately not written into `co2`, so a consumer can always tell the two apart — and so can a regulator, a buyer, or your own support team when someone asks where a number came from."
        },
        {
          "type": "h2",
          "text": "The two standards, and why it matters"
        },
        {
          "type": "p",
          "text": "EU registration documents declared NEDC-correlated CO2 through 2020 and switched to WLTP from 1 January 2021. **The same physical car reads 12–25% higher under WLTP.** Nothing about the car changed; the test did."
        },
        {
          "type": "table",
          "head": [
            "Specification",
            "Registered ≤2020 (NEDC)",
            "Registered ≥2021 (WLTP)",
            "Step"
          ],
          "rows": [
            [
              "Peugeot 5008 Petrol 96 kW",
              "115–121",
              "145–150",
              "+25%"
            ],
            [
              "Peugeot 3008 Petrol 96 kW",
              "114–120",
              "140–144",
              "+21%"
            ],
            [
              "BMW 1-series Diesel 85 kW",
              "100–104",
              "111–120",
              "+17%"
            ],
            [
              "VW Passat Diesel 110 kW",
              "106–113",
              "127–128",
              "+15%"
            ],
            [
              "VW Tiguan Diesel 110 kW",
              "127–129",
              "140–144",
              "+12%"
            ],
            [
              "BMW X1 Diesel 85 kW",
              "104",
              "117",
              "+12%"
            ]
          ],
          "caption": "Median g/km for fixed specifications across the labelled inventory. The 2021 switch is visible as a step, not a trend."
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "`co2` mixes both eras and does not say which",
          "text": "The measured column has always carried both standards: pre-2021 rows are effectively NEDC, 2021-and-later rows are WLTP, and the field carries no tag to tell them apart. If you rank, average or threshold on `co2` across a mixed-year set, newer cars look dirtier than they are. Group by registration year, or state the caveat where you show the number."
        },
        {
          "type": "h2",
          "text": "Which sources publish a measured figure"
        },
        {
          "type": "table",
          "head": [
            "Source",
            "Measured `co2`",
            "Notes"
          ],
          "rows": [
            [
              "`openlane`",
              "Yes",
              "Published on the listing itself."
            ],
            [
              "`auto1`",
              "Yes",
              "Published with the vehicle specification."
            ],
            [
              "`copart`",
              "Effectively no",
              "Present on a fraction of a percent of lots."
            ],
            [
              "`schadeautos`",
              "No",
              "Publishes none."
            ],
            [
              "`encar`",
              "No",
              "Korean market — homologated on a different cycle entirely."
            ],
            [
              "`japan`",
              "No",
              "Japanese market — homologated on JC08/WLTC, not an EU cycle."
            ]
          ]
        },
        {
          "type": "h2",
          "text": "Which lots can carry an estimate"
        },
        {
          "type": "p",
          "text": "Only EU-market lots: `openlane`, `auto1` and `schadeautos`. Korean and Japanese cars are homologated on JC08/WLTC and American salvage on the EPA cycle — **no EU figure exists for them**, so producing one would not be imprecision, it would be a category error. Those lots carry `null` in both CO2 fields and that is correct."
        },
        {
          "type": "p",
          "text": "Within the eligible set, an estimate is produced only where the specification decides the answer with enough confidence — matched on standard, make, model, fuel, engine power in kW and, where possible, year. Where the specification does not pin it down, the field stays `null` rather than guessing. Coverage is deliberately traded away for accuracy: a broader rule that filled far more lots was measured, found to land within 10 g/km only about a third of the time, and rejected."
        },
        {
          "type": "callout",
          "tone": "tip",
          "title": "What accuracy to expect",
          "text": "Measured against a source the reference table was **not** built from — the realistic case — the tighter match answers around a quarter of eligible lots at a median error of about 5 g/km, and the looser one around 30% at about 7 g/km. Good enough to band a car, filter a search, or show a comparison. Not good enough to print on a document."
        },
        {
          "type": "h2",
          "text": "Rendering it correctly"
        },
        {
          "type": "code",
          "lang": "javascript",
          "code": "function co2Label(auction) {\n  // A measured figure wins. Note it carries no standard tag — infer the era\n  // from registration year if you need to compare across 2020/2021.\n  if (auction.co2 != null) {\n    return { value: auction.co2, measured: true, standard: null };\n  }\n\n  // An estimate is only meaningful with its standard attached.\n  if (auction.co2_estimated != null) {\n    return {\n      value: auction.co2_estimated,\n      measured: false,\n      standard: auction.co2_estimated_standard,   // \"NEDC\" | \"WLTP\"\n    };\n  }\n\n  // Neither. Common, and correct — most non-EU lots will land here.\n  return null;\n}",
          "caption": "Prefer the measurement, fall back to the estimate, and never silently merge the two."
        },
        {
          "type": "ul",
          "items": [
            "**Show the provenance.** \"142 g/km\" and \"≈142 g/km (WLTP, estimated)\" are different claims. Your users can tell, and so can anyone auditing your listing.",
            "**Do not sort or filter on `co2_estimated`.** It is `null` on most lots, so any ordering that uses it drops them without saying so. Sort on `co2` if you must sort at all, and expect the same caveat about mixed eras.",
            "**Do not compare a 2019 figure with a 2022 figure.** They are different tests. A 12–25% gap between two cars of the same specification is usually the standard, not the engine.",
            "**Do not treat `null` as zero.** An electric car and a car that published nothing both render as blank if you do — one of those is very wrong."
          ]
        },
        {
          "type": "h2",
          "text": "Related emissions fields"
        },
        {
          "type": "p",
          "text": "`effluent_standard_group_search` on the auction detail carries the vehicle's emissions class where the source publishes it (Euro 5, Euro 6 and so on). It is a separate concept from CO2 — it describes pollutant limits the car was type-approved against, not the carbon it emits — and the two should not be presented as one figure. `vehicle_details.specs` may also carry source-published emissions and, for electric vehicles, charging figures."
        }
      ],
      "markdown": "# CO2 & emissions\n\nThe difference between the measured and estimated CO2 fields, the NEDC/WLTP split that makes a standard tag mandatory, which sources publish a figure, and how to render it without overclaiming.\n\nRoughly half of EU auction lots publish a CO2 figure and the rest publish nothing at all. The API therefore carries **two** CO2 fields with deliberately different meanings, plus a third that says which test standard the second one is on. Rendering them as one number is the single most common way to get emissions wrong.\n\n| Field | Type | What it is |\n| --- | --- | --- |\n| `co2` | `integer | null` | **Measured.** The figure the auction house itself published, in g/km. `null` on the roughly half of lots that publish none. Carries no standard tag. |\n| `co2_estimated` | `integer | null` | **Derived.** Inferred for lots that publish nothing, from other lots of the same specification. Never a measured value. `null` wherever the specification does not decide it. |\n| `co2_estimated_standard` | `\"NEDC\" | \"WLTP\" | null` | Which EU test cycle `co2_estimated` is expressed on. Always read the estimate together with this. |\n\n> **Why they are two fields and not one** — An estimate must never be able to masquerade as a measurement. Estimates are deliberately not written into `co2`, so a consumer can always tell the two apart — and so can a regulator, a buyer, or your own support team when someone asks where a number came from.\n\n## The two standards, and why it matters\n\nEU registration documents declared NEDC-correlated CO2 through 2020 and switched to WLTP from 1 January 2021. **The same physical car reads 12–25% higher under WLTP.** Nothing about the car changed; the test did.\n\n| Specification | Registered ≤2020 (NEDC) | Registered ≥2021 (WLTP) | Step |\n| --- | --- | --- | --- |\n| Peugeot 5008 Petrol 96 kW | 115–121 | 145–150 | +25% |\n| Peugeot 3008 Petrol 96 kW | 114–120 | 140–144 | +21% |\n| BMW 1-series Diesel 85 kW | 100–104 | 111–120 | +17% |\n| VW Passat Diesel 110 kW | 106–113 | 127–128 | +15% |\n| VW Tiguan Diesel 110 kW | 127–129 | 140–144 | +12% |\n| BMW X1 Diesel 85 kW | 104 | 117 | +12% |\n\n_Median g/km for fixed specifications across the labelled inventory. The 2021 switch is visible as a step, not a trend._\n\n> **`co2` mixes both eras and does not say which** — The measured column has always carried both standards: pre-2021 rows are effectively NEDC, 2021-and-later rows are WLTP, and the field carries no tag to tell them apart. If you rank, average or threshold on `co2` across a mixed-year set, newer cars look dirtier than they are. Group by registration year, or state the caveat where you show the number.\n\n## Which sources publish a measured figure\n\n| Source | Measured `co2` | Notes |\n| --- | --- | --- |\n| `openlane` | Yes | Published on the listing itself. |\n| `auto1` | Yes | Published with the vehicle specification. |\n| `copart` | Effectively no | Present on a fraction of a percent of lots. |\n| `schadeautos` | No | Publishes none. |\n| `encar` | No | Korean market — homologated on a different cycle entirely. |\n| `japan` | No | Japanese market — homologated on JC08/WLTC, not an EU cycle. |\n\n## Which lots can carry an estimate\n\nOnly EU-market lots: `openlane`, `auto1` and `schadeautos`. Korean and Japanese cars are homologated on JC08/WLTC and American salvage on the EPA cycle — **no EU figure exists for them**, so producing one would not be imprecision, it would be a category error. Those lots carry `null` in both CO2 fields and that is correct.\n\nWithin the eligible set, an estimate is produced only where the specification decides the answer with enough confidence — matched on standard, make, model, fuel, engine power in kW and, where possible, year. Where the specification does not pin it down, the field stays `null` rather than guessing. Coverage is deliberately traded away for accuracy: a broader rule that filled far more lots was measured, found to land within 10 g/km only about a third of the time, and rejected.\n\n> **What accuracy to expect** — Measured against a source the reference table was **not** built from — the realistic case — the tighter match answers around a quarter of eligible lots at a median error of about 5 g/km, and the looser one around 30% at about 7 g/km. Good enough to band a car, filter a search, or show a comparison. Not good enough to print on a document.\n\n## Rendering it correctly\n\n```javascript\nfunction co2Label(auction) {\n  // A measured figure wins. Note it carries no standard tag — infer the era\n  // from registration year if you need to compare across 2020/2021.\n  if (auction.co2 != null) {\n    return { value: auction.co2, measured: true, standard: null };\n  }\n\n  // An estimate is only meaningful with its standard attached.\n  if (auction.co2_estimated != null) {\n    return {\n      value: auction.co2_estimated,\n      measured: false,\n      standard: auction.co2_estimated_standard,   // \"NEDC\" | \"WLTP\"\n    };\n  }\n\n  // Neither. Common, and correct — most non-EU lots will land here.\n  return null;\n}\n```\n\n_Prefer the measurement, fall back to the estimate, and never silently merge the two._\n\n- **Show the provenance.** \"142 g/km\" and \"≈142 g/km (WLTP, estimated)\" are different claims. Your users can tell, and so can anyone auditing your listing.\n- **Do not sort or filter on `co2_estimated`.** It is `null` on most lots, so any ordering that uses it drops them without saying so. Sort on `co2` if you must sort at all, and expect the same caveat about mixed eras.\n- **Do not compare a 2019 figure with a 2022 figure.** They are different tests. A 12–25% gap between two cars of the same specification is usually the standard, not the engine.\n- **Do not treat `null` as zero.** An electric car and a car that published nothing both render as blank if you do — one of those is very wrong.\n\n## Related emissions fields\n\n`effluent_standard_group_search` on the auction detail carries the vehicle's emissions class where the source publishes it (Euro 5, Euro 6 and so on). It is a separate concept from CO2 — it describes pollutant limits the car was type-approved against, not the carbon it emits — and the two should not be presented as one figure. `vehicle_details.specs` may also carry source-published emissions and, for electric vehicles, charging figures."
    },
    {
      "id": "live-prices",
      "label": "Live prices",
      "summary": "Cycle prices versus the at-request bid read on running auctions, and how to read `live_price` and `live_price_pending`.",
      "canonical": "https://thecarapi.com/docs/live-prices",
      "blocks": [
        {
          "type": "p",
          "text": "**Two kinds of price, and it is worth knowing which one you are holding.** Stored specifications, condition, availability and prices reflect the latest successful refresh of those fields. A fixed-price listing can also change upstream between refreshes; a cycle price is the latest observation, not a guarantee of current availability or a purchase quote."
        },
        {
          "type": "p",
          "text": "The exception is a **running auction with open bidding**, where the figure can move any time somebody bids. Eligible OpenLane lots receive a **live bid overlay** on detail requests. A recently fetched bid is reused within the live-price TTL. A cold refresh may still be pending or fail, leaving the stored price in place."
        },
        {
          "type": "table",
          "head": [
            "",
            "Cycle price",
            "Live price"
          ],
          "rows": [
            [
              "What it is",
              "The listing as of the last refresh",
              "A recent bid fetched from the auction house, possibly reused from the live-price cache"
            ],
            [
              "Covers",
              "Every listing on every source",
              "Running open-bidding auctions on `openlane`"
            ],
            [
              "Age",
              "Inspect listing timestamps; data_updated_at may default to response time",
              "Inspect fetched_at and ttl_seconds"
            ],
            [
              "Where you see it",
              "Search, facets, top offers, market data, and any detail response that did not qualify",
              "The two detail endpoints only"
            ],
            [
              "How you can tell",
              "No `live_price` block on the response",
              "The response carries a `live_price` block"
            ],
            [
              "Reused for",
              "Until the next refresh",
              "About two minutes (`live_prices.ttl_seconds`)"
            ]
          ]
        },
        {
          "type": "callout",
          "tone": "info",
          "title": "An absent `live_price` block is not an error",
          "text": "Use the stored price when no live overlay is present. The lot may be ineligible, the refresh may have failed, or a request may still be pending. Check live_price_pending and listing timestamps; absence of the block does not prove that a price is current."
        },
        {
          "type": "p",
          "text": "**There is no separate real-time endpoint or query parameter to switch on.** Ask for a car's details the way you already do. If it qualifies, the response carries a `live_price` block; if it does not, the cycle price is what exists."
        },
        {
          "type": "callout",
          "tone": "info",
          "title": "Where the live read applies",
          "text": "`GET /api/auction/{site}/{id}` and `GET|POST /api/car-details`, the latter when the response is served from stored auction data — the normal case. A first-ever fetch of a listing we hold no row for returns the source's own fresh payload anyway. **Search results, facets and top offers are never live-priced**; they serve cycle prices, always, which is what makes them fast enough to page through."
        },
        {
          "type": "h2",
          "text": "Which sources"
        },
        {
          "type": "table",
          "head": [
            "Source",
            "Live prices",
            "Why"
          ],
          "rows": [
            [
              "`openlane`",
              "Yes",
              "Timed online auction with open bidding."
            ],
            [
              "`auto1`, `encar`, `copart`, `schadeautos`, `kbchachacha`, `japan`",
              "No",
              "The current implementation has no live-price overlay for these sources. Prices reflect the latest successful stored observation."
            ]
          ]
        },
        {
          "type": "p",
          "text": "Do not hardcode that list — read `live_prices.sites` from `GET /api/contract`."
        },
        {
          "type": "h2",
          "text": "Reading the response"
        },
        {
          "type": "p",
          "text": "When a price was refreshed, the detail body gains:"
        },
        {
          "type": "code",
          "lang": "json",
          "code": "\"live_price\": {\n  \"price\": 25900,\n  \"currency\": \"EUR\",\n  \"source\": \"openlane\",\n  \"fetched_at\": 1786659750\n}"
        },
        {
          "type": "p",
          "text": "`price` is the raw bid at the auction house. `fetched_at` is a Unix timestamp in seconds — use it, not your own clock, to render an \"as of\" indicator. At the same time the **existing price fields are overwritten** with figures recomputed from that bid, on both detail endpoints, so a client that ignores `live_price` entirely still shows the right number."
        },
        {
          "type": "table",
          "head": [
            "Field",
            "Meaning"
          ],
          "rows": [
            [
              "`current_price`",
              "The raw current bid. Equals `live_price.price`."
            ],
            [
              "`current_final`",
              "Bid plus fees and taxes."
            ],
            [
              "`current_tax`, `current_tax_delivery`",
              "The recomputed tax components."
            ],
            [
              "`public_price_eur`",
              "The headline price, recomputed."
            ],
            [
              "`buy_now_price`, `buynow_final`",
              "Not touched — a buy-now figure does not move with bidding."
            ]
          ]
        },
        {
          "type": "h2",
          "text": "Where the block sits"
        },
        {
          "type": "p",
          "text": "Both detail endpoints publish **the same price fields, under the same names, with the same JSON types**, and a live refresh rewrites them the same way on either one. The only difference is nesting, and it follows the shape each endpoint already had."
        },
        {
          "type": "code",
          "lang": "json",
          "code": "// /api/auction/{site_slug}/{auction_id} — wrapped in \"auction\"\n{ \"success\": true,\n  \"auction\": { \"auction_id\": 11409652,\n               \"current_price\": 25900,\n               \"current_final\": 33566,\n               \"public_price_eur\": 33926,\n               \"live_price\": { \"price\": 25900, \"currency\": \"EUR\" } } }\n\n// /api/car-details — source-shaped, so the same keys sit at the top level\n{ \"success\": true, \"site\": \"openlane\", \"auction_id\": 11409652,\n  \"current_price\": 25900,\n  \"current_final\": 33566,\n  \"public_price_eur\": 33926,\n  \"auction\": { \"extracted_fields\": { \"images\": [] } },\n  \"live_price\": { \"price\": 25900, \"currency\": \"EUR\" } }"
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "`/api/car-details`’s `auction` key is not the vehicle record",
          "text": "For `openlane` it is `{\"extracted_fields\": {\"images\": […]}}` — an image container that shares a name and nothing else. Read prices from the top level on that endpoint, and from inside `auction` on `/api/auction/{site}/{id}`."
        },
        {
          "type": "h2",
          "text": "Price fields are JSON numbers"
        },
        {
          "type": "p",
          "text": "Prices are JSON numbers on both endpoints, whether or not a live refresh happened. Before contract `2026-08-18` they were serialized as **strings** on a normal read but as **numbers** whenever a live price had been refreshed — the same field, two types, switching on something a client could neither control nor predict."
        },
        {
          "type": "code",
          "lang": "diff",
          "code": "- \"current_price\": \"25600.00\"\n+ \"current_price\": 25600.0"
        },
        {
          "type": "p",
          "text": "Nothing to do if you parse with `Number(...)`, `float(...)` or into a dynamic type, or if your client already tolerated the live-price case. A statically typed client that declares these as strings has to retype them as numbers."
        },
        {
          "type": "code",
          "lang": "text",
          "code": "start_price          current_price        current_final\ncurrent_tax          current_tax_delivery buy_now_price\nbuynow_final         buynow_tax           buynow_tax_delivery\nfinal_price          public_price_eur     current_bgn\nbuynow_bgn",
          "caption": "The fields affected by the string → number change."
        },
        {
          "type": "h2",
          "text": "When there is no live price"
        },
        {
          "type": "p",
          "text": "The `live_price` block is **absent** whenever the price was not refreshed. That covers several different situations, and apart from the two flagged below they are indistinguishable in a detail response:"
        },
        {
          "type": "ul",
          "items": [
            "The source is not OpenLane, the supported live-price source.",
            "The auction has already ended.",
            "The car has no bids yet. Common on, where a car with no current bid publishes no price at all.",
            "`current_price` stays `null` and that is the correct, final answer — not a missing value to retry for. This case you *can* identify: the detail response carries `is_blind: true`, and it is the one reliable way to tell \"there will never be a price\" from \"there is no price right now\".",
            "Live prices are unavailable on this deployment.",
            "**The refreshed figure did not arrive in time for this response.** This one is different, and the response tells you so — see `live_price_pending` below."
          ]
        },
        {
          "type": "p",
          "text": "**Its absence never means the stored price is wrong.** It means you are looking at a cycle price, which is the normal state for the overwhelming majority of listings. To tell \"live prices are unavailable\" from \"this car has none right now\", read `live_prices.enabled` from `/api/contract` once at startup; where the feature does not exist at all, the whole `live_prices` block is missing."
        },
        {
          "type": "h2",
          "text": "live_price_pending: the refresh that arrived late"
        },
        {
          "type": "p",
          "text": "Your request is never held waiting on the auction house. When a current figure is not already to hand, the response comes back immediately with the last known price plus `live_price_pending: true`, and the refreshed figure is ready a few seconds later. Nothing is lost; it just missed your response."
        },
        {
          "type": "table",
          "head": [
            "Response carries",
            "What you are holding",
            "What to do"
          ],
          "rows": [
            [
              "`live_price`",
              "The current bid",
              "Nothing. Render it."
            ],
            [
              "`live_price_pending: true`",
              "The last known price; the refresh is still running",
              "Repeat the same request once after ~2s. The repeat is served from cache."
            ],
            [
              "Neither",
              "The cycle price, and that is what exists",
              "Nothing. This is the normal case."
            ]
          ]
        },
        {
          "type": "p",
          "text": "Use the flag to schedule one delayed follow-up read. If a bid is still unavailable, keep the last observed price with its timestamp rather than polling indefinitely; a later user-driven refresh may succeed. A pending response is sent with `Cache-Control: no-store` and `X-Live-Price: pending`, so a browser, CDN or shared proxy cannot answer your follow-up with the very body that missed the price."
        },
        {
          "type": "code",
          "lang": "javascript",
          "code": "async function vehicleWithLivePrice(site, id) {\n  const first = await getJson(`/api/auction/${site}/${id}`);\n  if (first.auction.live_price || !first.auction.live_price_pending) return first;\n\n  // The refresh missed the budget but is still running. Exactly one more read.\n  await new Promise((r) => setTimeout(r, 2000));\n  return getJson(`/api/auction/${site}/${id}`);\n}",
          "caption": "Read once more, then stop. A loop here only burns quota."
        },
        {
          "type": "h2",
          "text": "Polling"
        },
        {
          "type": "p",
          "text": "A refreshed price stays current for `live_prices.ttl_seconds` (default 120), and `Cache-Control: max-age` on the response matches. **Polling faster than that returns the same figure** and gains you nothing — honour `max-age`. Note this is per **read**, not a subscription: asking for the car once gives you the bid at that moment; it does not keep tracking while you hold the page. A bid that moves afterwards appears on your next read once the TTL has passed."
        },
        {
          "type": "h2",
          "text": "What movement to expect"
        },
        {
          "type": "p",
          "text": "A live bid may differ from the last stored observation. The API does not promise a maximum difference; use the returned value and fetched_at timestamp. Treat `live_price` as a **correction, not a stream** — it is the right thing to read when a user opens a vehicle, and the wrong thing to poll in a loop."
        },
        {
          "type": "h2",
          "text": "What stays on cycle prices"
        },
        {
          "type": "p",
          "text": "Only the detail path is live. `/api/search`, `/api/top-offers`, the market intelligence endpoints and every price-based sort or filter read cycle prices."
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "A detail page can show a higher price than the card you clicked",
          "text": "That is not an inconsistency to report; it is a bid that moved after the last cycle. Do not build \"price changed since you clicked\" warnings on this difference, and do not expect a car live-priced above your search's `price_to` to disappear from that search."
        },
        {
          "type": "p",
          "text": "Everything here is read only. This API observes auction prices; it never places, raises or withdraws a bid on your behalf."
        }
      ],
      "markdown": "# Live prices\n\nCycle prices versus the at-request bid read on running auctions, and how to read `live_price` and `live_price_pending`.\n\n**Two kinds of price, and it is worth knowing which one you are holding.** Stored specifications, condition, availability and prices reflect the latest successful refresh of those fields. A fixed-price listing can also change upstream between refreshes; a cycle price is the latest observation, not a guarantee of current availability or a purchase quote.\n\nThe exception is a **running auction with open bidding**, where the figure can move any time somebody bids. Eligible OpenLane lots receive a **live bid overlay** on detail requests. A recently fetched bid is reused within the live-price TTL. A cold refresh may still be pending or fail, leaving the stored price in place.\n\n|  | Cycle price | Live price |\n| --- | --- | --- |\n| What it is | The listing as of the last refresh | A recent bid fetched from the auction house, possibly reused from the live-price cache |\n| Covers | Every listing on every source | Running open-bidding auctions on `openlane` |\n| Age | Inspect listing timestamps; data_updated_at may default to response time | Inspect fetched_at and ttl_seconds |\n| Where you see it | Search, facets, top offers, market data, and any detail response that did not qualify | The two detail endpoints only |\n| How you can tell | No `live_price` block on the response | The response carries a `live_price` block |\n| Reused for | Until the next refresh | About two minutes (`live_prices.ttl_seconds`) |\n\n> **An absent `live_price` block is not an error** — Use the stored price when no live overlay is present. The lot may be ineligible, the refresh may have failed, or a request may still be pending. Check live_price_pending and listing timestamps; absence of the block does not prove that a price is current.\n\n**There is no separate real-time endpoint or query parameter to switch on.** Ask for a car's details the way you already do. If it qualifies, the response carries a `live_price` block; if it does not, the cycle price is what exists.\n\n> **Where the live read applies** — `GET /api/auction/{site}/{id}` and `GET|POST /api/car-details`, the latter when the response is served from stored auction data — the normal case. A first-ever fetch of a listing we hold no row for returns the source's own fresh payload anyway. **Search results, facets and top offers are never live-priced**; they serve cycle prices, always, which is what makes them fast enough to page through.\n\n## Which sources\n\n| Source | Live prices | Why |\n| --- | --- | --- |\n| `openlane` | Yes | Timed online auction with open bidding. |\n| `auto1`, `encar`, `copart`, `schadeautos`, `kbchachacha`, `japan` | No | The current implementation has no live-price overlay for these sources. Prices reflect the latest successful stored observation. |\n\nDo not hardcode that list — read `live_prices.sites` from `GET /api/contract`.\n\n## Reading the response\n\nWhen a price was refreshed, the detail body gains:\n\n```json\n\"live_price\": {\n  \"price\": 25900,\n  \"currency\": \"EUR\",\n  \"source\": \"openlane\",\n  \"fetched_at\": 1786659750\n}\n```\n\n`price` is the raw bid at the auction house. `fetched_at` is a Unix timestamp in seconds — use it, not your own clock, to render an \"as of\" indicator. At the same time the **existing price fields are overwritten** with figures recomputed from that bid, on both detail endpoints, so a client that ignores `live_price` entirely still shows the right number.\n\n| Field | Meaning |\n| --- | --- |\n| `current_price` | The raw current bid. Equals `live_price.price`. |\n| `current_final` | Bid plus fees and taxes. |\n| `current_tax`, `current_tax_delivery` | The recomputed tax components. |\n| `public_price_eur` | The headline price, recomputed. |\n| `buy_now_price`, `buynow_final` | Not touched — a buy-now figure does not move with bidding. |\n\n## Where the block sits\n\nBoth detail endpoints publish **the same price fields, under the same names, with the same JSON types**, and a live refresh rewrites them the same way on either one. The only difference is nesting, and it follows the shape each endpoint already had.\n\n```json\n// /api/auction/{site_slug}/{auction_id} — wrapped in \"auction\"\n{ \"success\": true,\n  \"auction\": { \"auction_id\": 11409652,\n               \"current_price\": 25900,\n               \"current_final\": 33566,\n               \"public_price_eur\": 33926,\n               \"live_price\": { \"price\": 25900, \"currency\": \"EUR\" } } }\n\n// /api/car-details — source-shaped, so the same keys sit at the top level\n{ \"success\": true, \"site\": \"openlane\", \"auction_id\": 11409652,\n  \"current_price\": 25900,\n  \"current_final\": 33566,\n  \"public_price_eur\": 33926,\n  \"auction\": { \"extracted_fields\": { \"images\": [] } },\n  \"live_price\": { \"price\": 25900, \"currency\": \"EUR\" } }\n```\n\n> **`/api/car-details`’s `auction` key is not the vehicle record** — For `openlane` it is `{\"extracted_fields\": {\"images\": […]}}` — an image container that shares a name and nothing else. Read prices from the top level on that endpoint, and from inside `auction` on `/api/auction/{site}/{id}`.\n\n## Price fields are JSON numbers\n\nPrices are JSON numbers on both endpoints, whether or not a live refresh happened. Before contract `2026-08-18` they were serialized as **strings** on a normal read but as **numbers** whenever a live price had been refreshed — the same field, two types, switching on something a client could neither control nor predict.\n\n```diff\n- \"current_price\": \"25600.00\"\n+ \"current_price\": 25600.0\n```\n\nNothing to do if you parse with `Number(...)`, `float(...)` or into a dynamic type, or if your client already tolerated the live-price case. A statically typed client that declares these as strings has to retype them as numbers.\n\n```text\nstart_price          current_price        current_final\ncurrent_tax          current_tax_delivery buy_now_price\nbuynow_final         buynow_tax           buynow_tax_delivery\nfinal_price          public_price_eur     current_bgn\nbuynow_bgn\n```\n\n_The fields affected by the string → number change._\n\n## When there is no live price\n\nThe `live_price` block is **absent** whenever the price was not refreshed. That covers several different situations, and apart from the two flagged below they are indistinguishable in a detail response:\n\n- The source is not OpenLane, the supported live-price source.\n- The auction has already ended.\n- The car has no bids yet. Common on, where a car with no current bid publishes no price at all.\n- `current_price` stays `null` and that is the correct, final answer — not a missing value to retry for. This case you *can* identify: the detail response carries `is_blind: true`, and it is the one reliable way to tell \"there will never be a price\" from \"there is no price right now\".\n- Live prices are unavailable on this deployment.\n- **The refreshed figure did not arrive in time for this response.** This one is different, and the response tells you so — see `live_price_pending` below.\n\n**Its absence never means the stored price is wrong.** It means you are looking at a cycle price, which is the normal state for the overwhelming majority of listings. To tell \"live prices are unavailable\" from \"this car has none right now\", read `live_prices.enabled` from `/api/contract` once at startup; where the feature does not exist at all, the whole `live_prices` block is missing.\n\n## live_price_pending: the refresh that arrived late\n\nYour request is never held waiting on the auction house. When a current figure is not already to hand, the response comes back immediately with the last known price plus `live_price_pending: true`, and the refreshed figure is ready a few seconds later. Nothing is lost; it just missed your response.\n\n| Response carries | What you are holding | What to do |\n| --- | --- | --- |\n| `live_price` | The current bid | Nothing. Render it. |\n| `live_price_pending: true` | The last known price; the refresh is still running | Repeat the same request once after ~2s. The repeat is served from cache. |\n| Neither | The cycle price, and that is what exists | Nothing. This is the normal case. |\n\nUse the flag to schedule one delayed follow-up read. If a bid is still unavailable, keep the last observed price with its timestamp rather than polling indefinitely; a later user-driven refresh may succeed. A pending response is sent with `Cache-Control: no-store` and `X-Live-Price: pending`, so a browser, CDN or shared proxy cannot answer your follow-up with the very body that missed the price.\n\n```javascript\nasync function vehicleWithLivePrice(site, id) {\n  const first = await getJson(`/api/auction/${site}/${id}`);\n  if (first.auction.live_price || !first.auction.live_price_pending) return first;\n\n  // The refresh missed the budget but is still running. Exactly one more read.\n  await new Promise((r) => setTimeout(r, 2000));\n  return getJson(`/api/auction/${site}/${id}`);\n}\n```\n\n_Read once more, then stop. A loop here only burns quota._\n\n## Polling\n\nA refreshed price stays current for `live_prices.ttl_seconds` (default 120), and `Cache-Control: max-age` on the response matches. **Polling faster than that returns the same figure** and gains you nothing — honour `max-age`. Note this is per **read**, not a subscription: asking for the car once gives you the bid at that moment; it does not keep tracking while you hold the page. A bid that moves afterwards appears on your next read once the TTL has passed.\n\n## What movement to expect\n\nA live bid may differ from the last stored observation. The API does not promise a maximum difference; use the returned value and fetched_at timestamp. Treat `live_price` as a **correction, not a stream** — it is the right thing to read when a user opens a vehicle, and the wrong thing to poll in a loop.\n\n## What stays on cycle prices\n\nOnly the detail path is live. `/api/search`, `/api/top-offers`, the market intelligence endpoints and every price-based sort or filter read cycle prices.\n\n> **A detail page can show a higher price than the card you clicked** — That is not an inconsistency to report; it is a bid that moved after the last cycle. Do not build \"price changed since you clicked\" warnings on this difference, and do not expect a car live-priced above your search's `price_to` to disappear from that search.\n\nEverything here is read only. This API observes auction prices; it never places, raises or withdraws a bid on your behalf."
    },
    {
      "id": "schema",
      "label": "Data dictionary",
      "summary": "Search card and auction detail field lists, the normalized `vehicle_details` block, per-source coverage, and what is never exposed.",
      "canonical": "https://thecarapi.com/docs/schema",
      "blocks": [
        {
          "type": "h2",
          "text": "Search result card fields"
        },
        {
          "type": "p",
          "text": "Fields are returned when the selected source provides them."
        },
        {
          "type": "code",
          "lang": "text",
          "code": "auction_id, site_name, auction_batch_id, batch_start_date, batch_end_date,\nauction_sec_left, buy_now_price, car_country_extended, country_code,\ncountry_marker_code, country_marker_name, country_marker_color, car_name_en,\ncurrent_price, buynow_final, current_final, buynow_bgn, current_bgn,\ndate_first_registration, has_technical_damage, is_broken, is_buy_now, kw,\nmileage, start_price, final_price, thumbnail_url, hp, pk, size, picture_count,\nclean_make, clean_model, model_display, model_class_series, fuel_group,\ngearbox_group, clean_type, registration_year, production_year, production_month, is_margin, public_damage_state, public_price_eur,\noffer_link, is_active, is_top_offer, top_offer_savings_pct, top_offer_savings"
        },
        {
          "type": "p",
          "text": "Four more fields are derived rather than stored. The first three are on every row; the fourth appears once the photo is on our CDN."
        },
        {
          "type": "table",
          "head": [
            "Field",
            "Meaning"
          ],
          "rows": [
            [
              "`auction_id_str`",
              "The auction id as a **string**, on every row. Address a car with this one — see below."
            ],
            [
              "`auction_end_at`",
              "Absolute UTC close instant, or `null` for stock with no deadline. On every row."
            ],
            [
              "`steering`",
              "`left` / `right`, never `null`. On every row."
            ],
            [
              "`served_thumbnail_url`",
              "The card's thumbnail once the photo is stored on our CDN — the same value `thumbnail_url` then carries. **Absent until then**, so fall back to `thumbnail_url` rather than treating it as required."
            ]
          ]
        },
        {
          "type": "callout",
          "tone": "warn",
          "title": "Use `auction_id_str` to address a car, not `auction_id`",
          "text": "They carry the same id, but `japan` ids exceed 2^53 — almost every lot from that source — so a JSON number cannot hold one exactly. JavaScript, and any other client parsing JSON numbers as IEEE-754 doubles, silently rounds `auction_id` and the result no longer addresses the row it came from. `auction_id` is unchanged and stays a number for existing consumers; `auction_id_str` is the exact value, and it is what the `site_name` + id pair you persist should use."
        },
        {
          "type": "p",
          "text": "Rows also carry image annotations alongside the thumbnail: `thumbnailUrl` and `servedThumbnailUrl` (camelCase twins of the fields above), `image_status` and `image_source` as on gallery entries, `original_thumbnail_url` (the source's own thumbnail, when the vault's replaced it), and `images` — at most the first **8** display-ordered gallery entries, primary first. **`images` on a search row is a card preview, never the full gallery**: for everything, read `vault_gallery` on the detail response or `/api/auction-images/{site}/{id}`."
        },
        {
          "type": "p",
          "text": "`fuel_group` can be empty — `japan` publishes no fuel for most of the catalogue — and a lot with no known fuel matches **every** `fuel` filter rather than none, so filtering by fuel does not make those lots unreachable."
        },
        {
          "type": "h2",
          "text": "Additional auction detail fields"
        },
        {
          "type": "code",
          "lang": "text",
          "code": "car_id, chassis_number, co2, co2_estimated, co2_estimated_standard,\nbuynow_tax, current_tax, buynow_tax_delivery, current_tax_delivery,\ncylinder_capacity, effluent_standard_group_search, is_margin,\nrequested_sales_price, requested_sales_price_can_be_shown, special_price, places,\nseller_score, country_code_dealer, community_name, community_seller_name,\ncurrency_code_id, vehicle_grade_id, mechanical_indicator_id, is_commercial_car,\ncar_identification, car_report_types, images, gallery_images, created_at,\nupdated_at, first_seen_at, last_seen_at, last_available_at, last_changed_at,\nvehicle_details, vault_gallery, estimated_value_eur, is_blind, details_pending"
        },
        {
          "type": "p",
          "text": "It also carries `auction_id_str`, `auction_end_at` and `steering`, exactly as the search cards do, plus the same four catalog-shaped keys `/api/listVehicles` adds: `vehicle_id` (a duplicate of `auction_id`, and subject to the same 2^53 caveat — prefer `auction_id_str`), `manufacturer_slug`, `model_group_slug` and, where the row has one, `fuel_type`. The two slugs are the values the catalog routes are addressed by, so a detail response is enough to build a link to its manufacturer and model-group pages without a second lookup."
        },
        {
          "type": "h3",
          "text": "`is_blind` and `details_pending`"
        },
        {
          "type": "p",
          "text": "Two booleans answer questions the rest of the payload cannot."
        },
        {
          "type": "p",
          "text": "**`is_blind`** is `true` when the auction house publishes no bid at all, by design. This is the one case where `current_price: null` is a **final answer rather than missing data** — there is no figure to fetch, now or later, and no live price will ever arrive. A client that shows \"price unavailable, retrying…\" on a blind lot will retry forever; read this flag and show `estimated_value_eur` instead, labelled as an estimate."
        },
        {
          "type": "p",
          "text": "**`details_pending`** is `true` when the listing's full detail payload has not been fetched from the source yet. While it is `true`, `vehicle_details` is omitted and the response is served with a short `max-age` so a polling client sees the details land. It is not an error and needs no handling beyond rendering the specification section once it turns `false`."
        },
        {
          "type": "callout",
          "tone": "tip",
          "title": "The fields worth knowing first",
          "text": "`public_price_eur` is the canonical public EUR price. `car_identification` is a source-specific specification map. `auction_end_at` is the absolute UTC close instant — drive countdowns from it, not from `batch_end_date` (per-house timezone) or `auction_sec_left` (a snapshot taken at the last refresh, not seconds from now). `estimated_value_eur` is the auction house's own valuation and **not a price you can pay** — see [Fields by source](/docs/fields-by-source). `co2` and `co2_estimated` are measured and derived respectively and must never be merged — see [CO2 & emissions](/docs/co2)."
        },
        {
          "type": "h2",
          "text": "The vehicle_details block"
        },
        {
          "type": "p",
          "text": "Present on `/api/auction/{site}/{id}` and `/api/car-details`. The detail-rich sources describe the same concepts under different names — a downloadable file is `Documents[]` on Auto1, while `Documents` on OpenLane is a dict of paperwork booleans and not a file list at all — so this block normalizes them. Read it instead of branching on `site_name`. Every key is omitted when the source has nothing for it, and the whole block is omitted while `details_pending` is true."
        },
        {
          "type": "table",
          "head": [
            "Key",
            "Type",
            "Meaning"
          ],
          "rows": [
            [
              "`documents`",
              "array",
              "Downloadable files: `{type, url, label}`. Links only."
            ],
            [
              "`inspection_reports`",
              "array",
              "Inspection/damage/appraisal reports: `{type, type_id, url}`."
            ],
            [
              "`option_reports`",
              "array",
              "Equipment reports (`type_id` 4), kept separate from damages."
            ],
            [
              "`car_reports`",
              "array",
              "OpenLane's own report index, as the source sends it."
            ],
            [
              "`service_history`",
              "array",
              "Per-visit rows: `{date, mileage_km, company, description, price_eur}`."
            ],
            [
              "`service_history_summary`",
              "object",
              "Whether serviced, last date, last mileage."
            ],
            [
              "`technical_inspection`",
              "object / string",
              "Roadworthiness inspection validity."
            ],
            [
              "`paperwork`",
              "object",
              "`COCAvailable`, `RegistrationDocsAvailable`, `IsV5Present`, `LicensePlate`."
            ],
            [
              "`condition`",
              "object",
              "Driveability, keys, tyre depths, grade, inspection status, accident flag."
            ],
            [
              "`damages`",
              "array",
              "Structured damage records per panel/area, where the source provides them."
            ],
            [
              "`damage_comment`",
              "string",
              "Free-text damage notes (also `damage_service_comment`)."
            ],
            [
              "`technical_issues` / `remarks`",
              "string",
              ""
            ],
            [
              "`is_damaged` / `has_technical_issues`",
              "boolean",
              "Optional source-specific evidence, where available."
            ],
            [
              "`estimated_repair_costs`",
              "number",
              "Estimated cost to repair the vehicle's damage. Not a fee."
            ],
            [
              "`paint_measurements` / `wheels` / `highlights`",
              "array / object",
              "Auto1 condition extras."
            ],
            [
              "`technical_inspection_date`",
              "string",
              "Roadworthiness inspection expiry, where the source publishes a bare date."
            ],
            [
              "`equipment`",
              "array",
              "Equipment/options list."
            ],
            [
              "`warranty`",
              "object",
              "Warranty type, expiry date and mileage."
            ],
            [
              "`specs`",
              "object",
              "Engine, body, dimensions, emissions, EV charging figures."
            ],
            [
              "`location`",
              "object",
              "Physical and origin country of the vehicle."
            ],
            [
              "`seller`",
              "object",
              "Seller/dealer record as the source provides it."
            ],
            [
              "`profile`",
              "object",
              "Optional source-specific evidence, where available."
            ],
            [
              "`videos`",
              "array",
              "Optional source video entries; use returned URLs where present."
            ],
            [
              "`kb_diagnosis` / `kb_inspection`",
              "object",
              "KB report status, ordered rows, checked controls, panel marks, images, documents and optional VIN. See Fields by source for the shapes."
            ],
            [
              "`other_notes`",
              "array / object",
              "Further Auto1 condition notes."
            ]
          ]
        },
        {
          "type": "h3",
          "text": "Coverage is not uniform across sources"
        },
        {
          "type": "p",
          "text": "Each auction house publishes a different subset of these keys, and the differences are large: only Auto1 has paint measurements, OpenLane distinguishes report files from paperwork flags, KB exposes optional diagnosis and inspection records, and Japan exposes imported photos and optional sheet evidence. This is an upstream limitation, not a gap in our data — do not build a UI that assumes a key is always there. The full coverage matrix, the OpenLane report-type codes and the per-source traps live on [Fields by source](/docs/fields-by-source)."
        },
        {
          "type": "h2",
          "text": "Production year, registration and trim"
        },
        {
          "type": "p",
          "text": "registration_year describes first registration; production_year uses production/model-year evidence and falls back to registration_year where absent. production_month is nullable and is not synthesized from year-only data. Use production_year_from / production_year_to and production_year_new / production_year_old for the production-year dimension. /api/production-years returns production_years and production_year_counts; /api/facets includes it as production_years. /api/types requires brand and model and returns counted trim values plus grouped model entries. Use type for trims, distinct from vehicle_type and body_style."
        },
        {
          "type": "p",
          "text": "is_margin is a nullable VAT-scheme flag on cards and detail. True means margin, false means deductible, null means unknown. Do not interpret missing permission-gated price fields as zero. Optional source report blocks include KB kb_diagnosis and kb_inspection; Japanese imported offer evidence is under car_identification.JapanOffers."
        },
        {
          "type": "h2",
          "text": "Never exposed"
        },
        {
          "type": "p",
          "text": "Bidder and private fields — bidder identities, private bids, personal maximums, and order data — are never returned. Neither is account-scoped commercial data: the auction houses' fee, cost and delivery figures are quoted to *our* buyer account and *our* delivery address, so they describe our commercial terms rather than the vehicle."
        },
        {
          "type": "table",
          "head": [
            "Removed",
            "Examples"
          ],
          "rows": [
            [
              "Fees and auction terms",
              "`Fees`, `bidprice`, `MerchantVat`, `Finance`, `Bidding`, `PackageDeal`, `commission_tiers`, `price_breakdown`"
            ],
            [
              "Delivery quoted to our address",
              "Every key beginning `delivery` or `selfpickup`, at any depth, plus `transport`, `TransportOptions`, `StandingFees`, `PickupAddress`, `CompoundAddress`"
            ],
            [
              "Third-party contact details",
              "Email addresses redacted from free-text values; surrounding text kept"
            ],
            [
              "Our identity on the platforms",
              "The buyer-account identity each source sees, our logistics account references, and `BidHistory`"
            ],
            [
              "Internal processing metadata",
              "`_metrics`, `details_fetch_meta`, `enrich_errors`"
            ]
          ]
        },
        {
          "type": "p",
          "text": "Still served: the auction house's own published asking prices (`price_buy_now`, `price_min_bid`, `price_estimation`), the VAT scheme (`is_margin`, `vat_scheme`), the estimated repair cost, `Specs.TransportCategoryId` (a vehicle class, not a shipment), the seller/warehouse location, and `buynow_tax_delivery` / `current_tax_delivery`. For landed-cost figures use the [import calculator](/docs/calculator), which is built for your own route."
        },
        {
          "type": "h2",
          "text": "Sources and enums"
        },
        {
          "type": "table",
          "head": [
            "Type",
            "Values"
          ],
          "rows": [
            [
              "Sources",
              "`schadeautos`, `auto1`, `openlane`, `copart`, `encar`, `kbchachacha`, `japan`"
            ],
            [
              "Sort",
              "`default` (photo-first, then newest), `newest`, `top_offers`, `time_left_least`, `time_left_desc`, `time_left_most`, `price_low`, `price_high`, `year_new`, `year_old`, `production_year_new`, `production_year_old`, `brand_az`, `brand_za`. The retired `random` value is still accepted and is served as `default`."
            ],
            [
              "`vehicle_type`",
              "`cars`, `suvs & trucks` (alias: `suvs and trucks`)"
            ],
            [
              "`smart_filter`",
              "`top_offers`, `low_mileage`, `best_value`"
            ],
            [
              "Booleans",
              "`true`, `false`, `1`, `0`, `yes`, `no`"
            ]
          ]
        },
        {
          "type": "p",
          "text": "The European classifieds network is not in the source list. It is retail market-reference data, not auction inventory, so it is not a valid `site` value — see the [classifieds endpoint group](/docs/theparking) instead."
        }
      ],
      "markdown": "# Data dictionary\n\nSearch card and auction detail field lists, the normalized `vehicle_details` block, per-source coverage, and what is never exposed.\n\n## Search result card fields\n\nFields are returned when the selected source provides them.\n\n```text\nauction_id, site_name, auction_batch_id, batch_start_date, batch_end_date,\nauction_sec_left, buy_now_price, car_country_extended, country_code,\ncountry_marker_code, country_marker_name, country_marker_color, car_name_en,\ncurrent_price, buynow_final, current_final, buynow_bgn, current_bgn,\ndate_first_registration, has_technical_damage, is_broken, is_buy_now, kw,\nmileage, start_price, final_price, thumbnail_url, hp, pk, size, picture_count,\nclean_make, clean_model, model_display, model_class_series, fuel_group,\ngearbox_group, clean_type, registration_year, production_year, production_month, is_margin, public_damage_state, public_price_eur,\noffer_link, is_active, is_top_offer, top_offer_savings_pct, top_offer_savings\n```\n\nFour more fields are derived rather than stored. The first three are on every row; the fourth appears once the photo is on our CDN.\n\n| Field | Meaning |\n| --- | --- |\n| `auction_id_str` | The auction id as a **string**, on every row. Address a car with this one — see below. |\n| `auction_end_at` | Absolute UTC close instant, or `null` for stock with no deadline. On every row. |\n| `steering` | `left` / `right`, never `null`. On every row. |\n| `served_thumbnail_url` | The card's thumbnail once the photo is stored on our CDN — the same value `thumbnail_url` then carries. **Absent until then**, so fall back to `thumbnail_url` rather than treating it as required. |\n\n> **Use `auction_id_str` to address a car, not `auction_id`** — They carry the same id, but `japan` ids exceed 2^53 — almost every lot from that source — so a JSON number cannot hold one exactly. JavaScript, and any other client parsing JSON numbers as IEEE-754 doubles, silently rounds `auction_id` and the result no longer addresses the row it came from. `auction_id` is unchanged and stays a number for existing consumers; `auction_id_str` is the exact value, and it is what the `site_name` + id pair you persist should use.\n\nRows also carry image annotations alongside the thumbnail: `thumbnailUrl` and `servedThumbnailUrl` (camelCase twins of the fields above), `image_status` and `image_source` as on gallery entries, `original_thumbnail_url` (the source's own thumbnail, when the vault's replaced it), and `images` — at most the first **8** display-ordered gallery entries, primary first. **`images` on a search row is a card preview, never the full gallery**: for everything, read `vault_gallery` on the detail response or `/api/auction-images/{site}/{id}`.\n\n`fuel_group` can be empty — `japan` publishes no fuel for most of the catalogue — and a lot with no known fuel matches **every** `fuel` filter rather than none, so filtering by fuel does not make those lots unreachable.\n\n## Additional auction detail fields\n\n```text\ncar_id, chassis_number, co2, co2_estimated, co2_estimated_standard,\nbuynow_tax, current_tax, buynow_tax_delivery, current_tax_delivery,\ncylinder_capacity, effluent_standard_group_search, is_margin,\nrequested_sales_price, requested_sales_price_can_be_shown, special_price, places,\nseller_score, country_code_dealer, community_name, community_seller_name,\ncurrency_code_id, vehicle_grade_id, mechanical_indicator_id, is_commercial_car,\ncar_identification, car_report_types, images, gallery_images, created_at,\nupdated_at, first_seen_at, last_seen_at, last_available_at, last_changed_at,\nvehicle_details, vault_gallery, estimated_value_eur, is_blind, details_pending\n```\n\nIt also carries `auction_id_str`, `auction_end_at` and `steering`, exactly as the search cards do, plus the same four catalog-shaped keys `/api/listVehicles` adds: `vehicle_id` (a duplicate of `auction_id`, and subject to the same 2^53 caveat — prefer `auction_id_str`), `manufacturer_slug`, `model_group_slug` and, where the row has one, `fuel_type`. The two slugs are the values the catalog routes are addressed by, so a detail response is enough to build a link to its manufacturer and model-group pages without a second lookup.\n\n### `is_blind` and `details_pending`\n\nTwo booleans answer questions the rest of the payload cannot.\n\n**`is_blind`** is `true` when the auction house publishes no bid at all, by design. This is the one case where `current_price: null` is a **final answer rather than missing data** — there is no figure to fetch, now or later, and no live price will ever arrive. A client that shows \"price unavailable, retrying…\" on a blind lot will retry forever; read this flag and show `estimated_value_eur` instead, labelled as an estimate.\n\n**`details_pending`** is `true` when the listing's full detail payload has not been fetched from the source yet. While it is `true`, `vehicle_details` is omitted and the response is served with a short `max-age` so a polling client sees the details land. It is not an error and needs no handling beyond rendering the specification section once it turns `false`.\n\n> **The fields worth knowing first** — `public_price_eur` is the canonical public EUR price. `car_identification` is a source-specific specification map. `auction_end_at` is the absolute UTC close instant — drive countdowns from it, not from `batch_end_date` (per-house timezone) or `auction_sec_left` (a snapshot taken at the last refresh, not seconds from now). `estimated_value_eur` is the auction house's own valuation and **not a price you can pay** — see [Fields by source](https://thecarapi.com/docs/fields-by-source). `co2` and `co2_estimated` are measured and derived respectively and must never be merged — see [CO2 & emissions](https://thecarapi.com/docs/co2).\n\n## The vehicle_details block\n\nPresent on `/api/auction/{site}/{id}` and `/api/car-details`. The detail-rich sources describe the same concepts under different names — a downloadable file is `Documents[]` on Auto1, while `Documents` on OpenLane is a dict of paperwork booleans and not a file list at all — so this block normalizes them. Read it instead of branching on `site_name`. Every key is omitted when the source has nothing for it, and the whole block is omitted while `details_pending` is true.\n\n| Key | Type | Meaning |\n| --- | --- | --- |\n| `documents` | array | Downloadable files: `{type, url, label}`. Links only. |\n| `inspection_reports` | array | Inspection/damage/appraisal reports: `{type, type_id, url}`. |\n| `option_reports` | array | Equipment reports (`type_id` 4), kept separate from damages. |\n| `car_reports` | array | OpenLane's own report index, as the source sends it. |\n| `service_history` | array | Per-visit rows: `{date, mileage_km, company, description, price_eur}`. |\n| `service_history_summary` | object | Whether serviced, last date, last mileage. |\n| `technical_inspection` | object / string | Roadworthiness inspection validity. |\n| `paperwork` | object | `COCAvailable`, `RegistrationDocsAvailable`, `IsV5Present`, `LicensePlate`. |\n| `condition` | object | Driveability, keys, tyre depths, grade, inspection status, accident flag. |\n| `damages` | array | Structured damage records per panel/area, where the source provides them. |\n| `damage_comment` | string | Free-text damage notes (also `damage_service_comment`). |\n| `technical_issues` / `remarks` | string |  |\n| `is_damaged` / `has_technical_issues` | boolean | Optional source-specific evidence, where available. |\n| `estimated_repair_costs` | number | Estimated cost to repair the vehicle's damage. Not a fee. |\n| `paint_measurements` / `wheels` / `highlights` | array / object | Auto1 condition extras. |\n| `technical_inspection_date` | string | Roadworthiness inspection expiry, where the source publishes a bare date. |\n| `equipment` | array | Equipment/options list. |\n| `warranty` | object | Warranty type, expiry date and mileage. |\n| `specs` | object | Engine, body, dimensions, emissions, EV charging figures. |\n| `location` | object | Physical and origin country of the vehicle. |\n| `seller` | object | Seller/dealer record as the source provides it. |\n| `profile` | object | Optional source-specific evidence, where available. |\n| `videos` | array | Optional source video entries; use returned URLs where present. |\n| `kb_diagnosis` / `kb_inspection` | object | KB report status, ordered rows, checked controls, panel marks, images, documents and optional VIN. See Fields by source for the shapes. |\n| `other_notes` | array / object | Further Auto1 condition notes. |\n\n### Coverage is not uniform across sources\n\nEach auction house publishes a different subset of these keys, and the differences are large: only Auto1 has paint measurements, OpenLane distinguishes report files from paperwork flags, KB exposes optional diagnosis and inspection records, and Japan exposes imported photos and optional sheet evidence. This is an upstream limitation, not a gap in our data — do not build a UI that assumes a key is always there. The full coverage matrix, the OpenLane report-type codes and the per-source traps live on [Fields by source](https://thecarapi.com/docs/fields-by-source).\n\n## Production year, registration and trim\n\nregistration_year describes first registration; production_year uses production/model-year evidence and falls back to registration_year where absent. production_month is nullable and is not synthesized from year-only data. Use production_year_from / production_year_to and production_year_new / production_year_old for the production-year dimension. /api/production-years returns production_years and production_year_counts; /api/facets includes it as production_years. /api/types requires brand and model and returns counted trim values plus grouped model entries. Use type for trims, distinct from vehicle_type and body_style.\n\nis_margin is a nullable VAT-scheme flag on cards and detail. True means margin, false means deductible, null means unknown. Do not interpret missing permission-gated price fields as zero. Optional source report blocks include KB kb_diagnosis and kb_inspection; Japanese imported offer evidence is under car_identification.JapanOffers.\n\n## Never exposed\n\nBidder and private fields — bidder identities, private bids, personal maximums, and order data — are never returned. Neither is account-scoped commercial data: the auction houses' fee, cost and delivery figures are quoted to *our* buyer account and *our* delivery address, so they describe our commercial terms rather than the vehicle.\n\n| Removed | Examples |\n| --- | --- |\n| Fees and auction terms | `Fees`, `bidprice`, `MerchantVat`, `Finance`, `Bidding`, `PackageDeal`, `commission_tiers`, `price_breakdown` |\n| Delivery quoted to our address | Every key beginning `delivery` or `selfpickup`, at any depth, plus `transport`, `TransportOptions`, `StandingFees`, `PickupAddress`, `CompoundAddress` |\n| Third-party contact details | Email addresses redacted from free-text values; surrounding text kept |\n| Our identity on the platforms | The buyer-account identity each source sees, our logistics account references, and `BidHistory` |\n| Internal processing metadata | `_metrics`, `details_fetch_meta`, `enrich_errors` |\n\nStill served: the auction house's own published asking prices (`price_buy_now`, `price_min_bid`, `price_estimation`), the VAT scheme (`is_margin`, `vat_scheme`), the estimated repair cost, `Specs.TransportCategoryId` (a vehicle class, not a shipment), the seller/warehouse location, and `buynow_tax_delivery` / `current_tax_delivery`. For landed-cost figures use the [import calculator](https://thecarapi.com/docs/calculator), which is built for your own route.\n\n## Sources and enums\n\n| Type | Values |\n| --- | --- |\n| Sources | `schadeautos`, `auto1`, `openlane`, `copart`, `encar`, `kbchachacha`, `japan` |\n| Sort | `default` (photo-first, then newest), `newest`, `top_offers`, `time_left_least`, `time_left_desc`, `time_left_most`, `price_low`, `price_high`, `year_new`, `year_old`, `production_year_new`, `production_year_old`, `brand_az`, `brand_za`. The retired `random` value is still accepted and is served as `default`. |\n| `vehicle_type` | `cars`, `suvs & trucks` (alias: `suvs and trucks`) |\n| `smart_filter` | `top_offers`, `low_mileage`, `best_value` |\n| Booleans | `true`, `false`, `1`, `0`, `yes`, `no` |\n\nThe European classifieds network is not in the source list. It is retail market-reference data, not auction inventory, so it is not a valid `site` value — see the [classifieds endpoint group](https://thecarapi.com/docs/theparking) instead."
    },
    {
      "id": "errors",
      "label": "Errors & limits",
      "summary": "Status codes, the error body shape, and which failures are worth retrying.",
      "canonical": "https://thecarapi.com/docs/errors",
      "blocks": [
        {
          "type": "p",
          "text": "Errors use `success: false` and a sanitized `error` message. Log the `X-Request-ID` response header when reporting a failure — it is the only handle support has on a specific call."
        },
        {
          "type": "table",
          "head": [
            "Status",
            "Meaning",
            "Retry?"
          ],
          "rows": [
            [
              "`400`",
              "Invalid filter, pagination, source, unknown query parameter, or malformed body.",
              "No — fix the request."
            ],
            [
              "`401`",
              "Missing, invalid, or expired API key.",
              "No — repeated failures trigger a lockout."
            ],
            [
              "`403`",
              "Revoked key, or scope, IP, or origin denial.",
              "No."
            ],
            [
              "`404`",
              "Source-aware resource not found.",
              "No."
            ],
            [
              "`409`",
              "Ambiguous legacy identifier.",
              "No — disambiguate with `site`."
            ],
            [
              "`413`",
              "Request body over **50 MB**. Only reachable on the POST routes.",
              "No."
            ],
            [
              "`429`",
              "Brute-force lockout (five failed authentications from your IP) or a rate/quota limit.",
              "Yes — after `Retry-After`."
            ],
            [
              "`500`",
              "Unexpected server error (message is sanitized).",
              "Yes — with backoff."
            ],
            [
              "`503`",
              "A dependency is unavailable, a search or facet query exceeded its safety timeout, a dataset has not been built yet, or authentication itself could not be verified (`Authentication is temporarily unavailable` — retry it; it is not a verdict on your key).",
              "Yes — with backoff."
            ]
          ]
        },
        {
          "type": "code",
          "lang": "json",
          "code": "{\n  \"success\": false,\n  \"error\": \"Unknown site 'ebay'. Valid sites: auto1, japan, copart, encar, kbchachacha, openlane, schadeautos\"\n}",
          "caption": "A `400` names the offending value and lists what would have been accepted."
        },
        {
          "type": "p",
          "text": "**Unknown query parameters are currently ignored, not rejected — do not rely on that.** Each endpoint understands a fixed set of parameters, and one outside that set is on its way to becoming a `400` naming the offender. Send only what an endpoint documents, and treat a filter that seems to have no effect as a misspelling rather than an unsupported feature."
        },
        {
          "type": "h2",
          "text": "Failures that are not errors"
        },
        {
          "type": "p",
          "text": "Three responses look like something went wrong and have not. Handling them as errors is the most common integration bug:"
        },
        {
          "type": "ul",
          "items": [
            "**A missing `live_price` block.** It means the car is served at its cycle price, which for most listings is the only price there is. See [Live prices](/docs/live-prices).",
            "**`details_pending: true`.** The upstream fetch is still running. Poll, do not retry in a tight loop.",
            "**An empty `inspection_reports` or `images` array.** Most listings publish no reports, and a gallery that has not been vaulted yet reports its shortfall as `pending`."
          ]
        },
        {
          "type": "h2",
          "text": "Safety timeouts on deep filtered searches"
        },
        {
          "type": "p",
          "text": "`/api/search` and `/api/listVehicles` have no offset cap. A very deep *filtered* search is answered from a compact index where it can be; where it cannot, it is subject to the safety timeout and may come back `503` asking you to narrow it. Catalog, top-offers and classifieds listings are capped at offset 5000 — a deeper page is a `400` naming the limit. Narrow the filter rather than retrying the same URL."
        }
      ],
      "markdown": "# Errors & limits\n\nStatus codes, the error body shape, and which failures are worth retrying.\n\nErrors use `success: false` and a sanitized `error` message. Log the `X-Request-ID` response header when reporting a failure — it is the only handle support has on a specific call.\n\n| Status | Meaning | Retry? |\n| --- | --- | --- |\n| `400` | Invalid filter, pagination, source, unknown query parameter, or malformed body. | No — fix the request. |\n| `401` | Missing, invalid, or expired API key. | No — repeated failures trigger a lockout. |\n| `403` | Revoked key, or scope, IP, or origin denial. | No. |\n| `404` | Source-aware resource not found. | No. |\n| `409` | Ambiguous legacy identifier. | No — disambiguate with `site`. |\n| `413` | Request body over **50 MB**. Only reachable on the POST routes. | No. |\n| `429` | Brute-force lockout (five failed authentications from your IP) or a rate/quota limit. | Yes — after `Retry-After`. |\n| `500` | Unexpected server error (message is sanitized). | Yes — with backoff. |\n| `503` | A dependency is unavailable, a search or facet query exceeded its safety timeout, a dataset has not been built yet, or authentication itself could not be verified (`Authentication is temporarily unavailable` — retry it; it is not a verdict on your key). | Yes — with backoff. |\n\n```json\n{\n  \"success\": false,\n  \"error\": \"Unknown site 'ebay'. Valid sites: auto1, japan, copart, encar, kbchachacha, openlane, schadeautos\"\n}\n```\n\n_A `400` names the offending value and lists what would have been accepted._\n\n**Unknown query parameters are currently ignored, not rejected — do not rely on that.** Each endpoint understands a fixed set of parameters, and one outside that set is on its way to becoming a `400` naming the offender. Send only what an endpoint documents, and treat a filter that seems to have no effect as a misspelling rather than an unsupported feature.\n\n## Failures that are not errors\n\nThree responses look like something went wrong and have not. Handling them as errors is the most common integration bug:\n\n- **A missing `live_price` block.** It means the car is served at its cycle price, which for most listings is the only price there is. See [Live prices](https://thecarapi.com/docs/live-prices).\n- **`details_pending: true`.** The upstream fetch is still running. Poll, do not retry in a tight loop.\n- **An empty `inspection_reports` or `images` array.** Most listings publish no reports, and a gallery that has not been vaulted yet reports its shortfall as `pending`.\n\n## Safety timeouts on deep filtered searches\n\n`/api/search` and `/api/listVehicles` have no offset cap. A very deep *filtered* search is answered from a compact index where it can be; where it cannot, it is subject to the safety timeout and may come back `503` asking you to narrow it. Catalog, top-offers and classifieds listings are capped at offset 5000 — a deeper page is a `400` naming the limit. Narrow the filter rather than retrying the same URL."
    },
    {
      "id": "sdks",
      "label": "Code examples",
      "summary": "Minimal clients in cURL, TypeScript, Python and PHP, plus recipes for pagination, conditional requests and detail pages.",
      "canonical": "https://thecarapi.com/docs/code-examples",
      "blocks": [
        {
          "type": "p",
          "text": "There is no required SDK. Any HTTP client that can send headers and parse JSON will work. The search examples use cURL with jq, a server-side TypeScript runtime, Python with requests, or PHP with Guzzle. Set `THECARAPI_KEY` in that runtime. The recipes illustrate integration patterns and use your application’s existing state and helpers."
        },
        {
          "type": "h2",
          "text": "cURL"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "export THECARAPI_KEY=\"your_key\"\n\ncurl -sS --compressed \\\n  -H \"X-API-Key: $THECARAPI_KEY\" \\\n  \"https://api.thecarapi.com/api/search?brand=bmw&fuel=Diesel&year_from=2018&sort=price_low&limit=24\" \\\n   | jq '.results[] | {auction_id_str, site_name, clean_make, clean_model, public_price_eur}'"
        },
        {
          "type": "h2",
          "text": "TypeScript"
        },
        {
          "type": "code",
          "lang": "typescript",
          "code": "const BASE = \"https://api.thecarapi.com\";\n\ninterface SearchResponse {\n  success: boolean;\n  results: SearchCard[];\n  total: number | null;\n  limit: number;\n  offset: number;\n  max_page: number | null;\n  total_pages: number | null;\n  contract_version: string;\n  request_id: string;\n}\n\ninterface SearchCard {\n  auction_id: number;\n  auction_id_str: string;\n  site_name: string;\n  clean_make: string;\n  clean_model: string;\n  registration_year: number | null;\n  mileage: number | null;\n  /** Prices are JSON numbers, never strings. */\n  public_price_eur?: number | null; // May be omitted by field permissions.\n  thumbnail_url: string | null;\n}\n\nasync function search(params: Record<string, string | number>): Promise<SearchResponse> {\n  const url = `${BASE}/api/search?${new URLSearchParams(\n    Object.entries(params).map(([k, v]) => [k, String(v)]),\n  )}`;\n\n  const res = await fetch(url, {\n    headers: {\n      \"X-API-Key\": process.env.THECARAPI_KEY!,\n      \"Accept-Encoding\": \"gzip\",\n    },\n  });\n\n  if (!res.ok) {\n    throw new Error(`${res.status} — request ${res.headers.get(\"X-Request-ID\")}`);\n  }\n  return res.json();\n}\n\nconst page = await search({ brand: \"bmw\", fuel: \"Diesel\", limit: 24 });\nconsole.log(page.results.length, \"of\", page.total);"
        },
        {
          "type": "h2",
          "text": "Python"
        },
        {
          "type": "code",
          "lang": "python",
          "code": "import os\nimport requests\n\nBASE = \"https://api.thecarapi.com\"\n\nsession = requests.Session()\nsession.headers.update({\"X-API-Key\": os.environ[\"THECARAPI_KEY\"]})\n# requests negotiates gzip and decompresses transparently.\n\nresponse = session.get(\n    f\"{BASE}/api/search\",\n    params={\"brand\": \"bmw\", \"fuel\": \"Diesel\", \"year_from\": 2018, \"limit\": 24},\n    timeout=30,\n)\nresponse.raise_for_status()\npayload = response.json()\n\nfor car in payload[\"results\"]:\n    print(car[\"site_name\"], car[\"auction_id_str\"], car.get(\"public_price_eur\"))"
        },
        {
          "type": "h2",
          "text": "PHP"
        },
        {
          "type": "code",
          "lang": "php",
          "code": "<?php\n$client = new GuzzleHttp\\Client([\n    'base_uri'       => 'https://api.thecarapi.com',\n    'headers'        => ['X-API-Key' => getenv('THECARAPI_KEY')],\n    'decode_content' => 'gzip',   // Guzzle needs this told to it explicitly\n    'timeout'        => 30,\n]);\n\n$response = $client->get('/api/search', [\n    'query' => ['brand' => 'bmw', 'fuel' => 'Diesel', 'limit' => 24],\n]);\n\n$payload = json_decode((string) $response->getBody(), true);\nforeach ($payload['results'] as $car) {\n    printf(\"%s/%s — €%s\\n\", $car['site_name'], $car['auction_id_str'], $car['public_price_eur'] ?? 'unavailable');\n}"
        },
        {
          "type": "h2",
          "text": "Recipe: page through a result set"
        },
        {
          "type": "code",
          "lang": "python",
          "code": "def iter_results(session, **filters):\n    \"\"\"Yield every card, respecting the depth policy rather than guessing at it.\"\"\"\n    offset, limit = 0, 100\n    while True:\n        r = session.get(f\"{BASE}/api/search\",\n                        params={**filters, \"limit\": limit, \"offset\": offset,\n                                \"include_total\": \"false\"},\n                        timeout=30)\n        r.raise_for_status()\n        rows = r.json()[\"results\"]\n        if not rows:\n            return\n        yield from rows\n        offset += limit\n        # /api/search and /api/listVehicles have no offset cap. Catalog,\n        # top-offers and classifieds listings stop at 5000 and a page too deep\n        # answers 400. Read pagination from /api/contract.",
          "caption": "`include_total=false` skips the count, which is the expensive half of a deep search."
        },
        {
          "type": "h2",
          "text": "Recipe: poll a search page without re-downloading it"
        },
        {
          "type": "code",
          "lang": "javascript",
          "code": "let etag = null;\nlet cached = null;\n\nasync function poll(url) {\n  const res = await fetch(url, {\n    headers: {\n      \"X-API-Key\": process.env.THECARAPI_KEY,\n      ...(etag ? { \"If-None-Match\": etag } : {}),\n    },\n  });\n\n  if (res.status === 304) return cached;   // nothing changed, no body transferred\n\n  if (!res.ok) throw new Error(`HTTP ${res.status}`);\n\n  etag = res.headers.get(\"ETag\");          // opaque — echo it back exactly\n  cached = await res.json();\n  return cached;\n}"
        },
        {
          "type": "h2",
          "text": "Recipe: a vehicle detail page in one request"
        },
        {
          "type": "code",
          "lang": "javascript",
          "code": "const { auction } = await getJson(`/api/auction/${site}/${id}`);\n\nconst photos = auction.vault_gallery?.images ?? [];\nconst stillComing = auction.vault_gallery?.pending ?? 0;   // render placeholders\n\nconst price = auction.public_price_eur;                     // already live if it could be\nconst asOf = auction.live_price\n  ? new Date(auction.live_price.fetched_at * 1000)          // seconds → ms\n  : null;\n\n// Only if the refresh missed the request budget, read exactly once more.\nif (!auction.live_price && auction.live_price_pending) {\n  setTimeout(() => refetch(site, id), 2000);\n}",
          "caption": "`vault_gallery` is the same body `/api/auction-images/{site}/{id}` returns, so the second request is unnecessary."
        }
      ],
      "markdown": "# Code examples\n\nMinimal clients in cURL, TypeScript, Python and PHP, plus recipes for pagination, conditional requests and detail pages.\n\nThere is no required SDK. Any HTTP client that can send headers and parse JSON will work. The search examples use cURL with jq, a server-side TypeScript runtime, Python with requests, or PHP with Guzzle. Set `THECARAPI_KEY` in that runtime. The recipes illustrate integration patterns and use your application’s existing state and helpers.\n\n## cURL\n\n```bash\nexport THECARAPI_KEY=\"your_key\"\n\ncurl -sS --compressed \\\n  -H \"X-API-Key: $THECARAPI_KEY\" \\\n  \"https://api.thecarapi.com/api/search?brand=bmw&fuel=Diesel&year_from=2018&sort=price_low&limit=24\" \\\n   | jq '.results[] | {auction_id_str, site_name, clean_make, clean_model, public_price_eur}'\n```\n\n## TypeScript\n\n```typescript\nconst BASE = \"https://api.thecarapi.com\";\n\ninterface SearchResponse {\n  success: boolean;\n  results: SearchCard[];\n  total: number | null;\n  limit: number;\n  offset: number;\n  max_page: number | null;\n  total_pages: number | null;\n  contract_version: string;\n  request_id: string;\n}\n\ninterface SearchCard {\n  auction_id: number;\n  auction_id_str: string;\n  site_name: string;\n  clean_make: string;\n  clean_model: string;\n  registration_year: number | null;\n  mileage: number | null;\n  /** Prices are JSON numbers, never strings. */\n  public_price_eur?: number | null; // May be omitted by field permissions.\n  thumbnail_url: string | null;\n}\n\nasync function search(params: Record<string, string | number>): Promise<SearchResponse> {\n  const url = `${BASE}/api/search?${new URLSearchParams(\n    Object.entries(params).map(([k, v]) => [k, String(v)]),\n  )}`;\n\n  const res = await fetch(url, {\n    headers: {\n      \"X-API-Key\": process.env.THECARAPI_KEY!,\n      \"Accept-Encoding\": \"gzip\",\n    },\n  });\n\n  if (!res.ok) {\n    throw new Error(`${res.status} — request ${res.headers.get(\"X-Request-ID\")}`);\n  }\n  return res.json();\n}\n\nconst page = await search({ brand: \"bmw\", fuel: \"Diesel\", limit: 24 });\nconsole.log(page.results.length, \"of\", page.total);\n```\n\n## Python\n\n```python\nimport os\nimport requests\n\nBASE = \"https://api.thecarapi.com\"\n\nsession = requests.Session()\nsession.headers.update({\"X-API-Key\": os.environ[\"THECARAPI_KEY\"]})\n# requests negotiates gzip and decompresses transparently.\n\nresponse = session.get(\n    f\"{BASE}/api/search\",\n    params={\"brand\": \"bmw\", \"fuel\": \"Diesel\", \"year_from\": 2018, \"limit\": 24},\n    timeout=30,\n)\nresponse.raise_for_status()\npayload = response.json()\n\nfor car in payload[\"results\"]:\n    print(car[\"site_name\"], car[\"auction_id_str\"], car.get(\"public_price_eur\"))\n```\n\n## PHP\n\n```php\n<?php\n$client = new GuzzleHttp\\Client([\n    'base_uri'       => 'https://api.thecarapi.com',\n    'headers'        => ['X-API-Key' => getenv('THECARAPI_KEY')],\n    'decode_content' => 'gzip',   // Guzzle needs this told to it explicitly\n    'timeout'        => 30,\n]);\n\n$response = $client->get('/api/search', [\n    'query' => ['brand' => 'bmw', 'fuel' => 'Diesel', 'limit' => 24],\n]);\n\n$payload = json_decode((string) $response->getBody(), true);\nforeach ($payload['results'] as $car) {\n    printf(\"%s/%s — €%s\\n\", $car['site_name'], $car['auction_id_str'], $car['public_price_eur'] ?? 'unavailable');\n}\n```\n\n## Recipe: page through a result set\n\n```python\ndef iter_results(session, **filters):\n    \"\"\"Yield every card, respecting the depth policy rather than guessing at it.\"\"\"\n    offset, limit = 0, 100\n    while True:\n        r = session.get(f\"{BASE}/api/search\",\n                        params={**filters, \"limit\": limit, \"offset\": offset,\n                                \"include_total\": \"false\"},\n                        timeout=30)\n        r.raise_for_status()\n        rows = r.json()[\"results\"]\n        if not rows:\n            return\n        yield from rows\n        offset += limit\n        # /api/search and /api/listVehicles have no offset cap. Catalog,\n        # top-offers and classifieds listings stop at 5000 and a page too deep\n        # answers 400. Read pagination from /api/contract.\n```\n\n_`include_total=false` skips the count, which is the expensive half of a deep search._\n\n## Recipe: poll a search page without re-downloading it\n\n```javascript\nlet etag = null;\nlet cached = null;\n\nasync function poll(url) {\n  const res = await fetch(url, {\n    headers: {\n      \"X-API-Key\": process.env.THECARAPI_KEY,\n      ...(etag ? { \"If-None-Match\": etag } : {}),\n    },\n  });\n\n  if (res.status === 304) return cached;   // nothing changed, no body transferred\n\n  if (!res.ok) throw new Error(`HTTP ${res.status}`);\n\n  etag = res.headers.get(\"ETag\");          // opaque — echo it back exactly\n  cached = await res.json();\n  return cached;\n}\n```\n\n## Recipe: a vehicle detail page in one request\n\n```javascript\nconst { auction } = await getJson(`/api/auction/${site}/${id}`);\n\nconst photos = auction.vault_gallery?.images ?? [];\nconst stillComing = auction.vault_gallery?.pending ?? 0;   // render placeholders\n\nconst price = auction.public_price_eur;                     // already live if it could be\nconst asOf = auction.live_price\n  ? new Date(auction.live_price.fetched_at * 1000)          // seconds → ms\n  : null;\n\n// Only if the refresh missed the request budget, read exactly once more.\nif (!auction.live_price && auction.live_price_pending) {\n  setTimeout(() => refetch(site, id), 2000);\n}\n```\n\n_`vault_gallery` is the same body `/api/auction-images/{site}/{id}` returns, so the second request is unnecessary._"
    },
    {
      "id": "recipes",
      "label": "Recipes",
      "summary": "Six end-to-end flows that cover most of what the API is for. Each is a complete sequence, not a fragment.",
      "canonical": "https://thecarapi.com/docs/recipes",
      "blocks": [
        {
          "type": "p",
          "text": "Set `API=https://api.thecarapi.com` and `KEY=your_key` first."
        },
        {
          "type": "h2",
          "text": "1. Build a filter sidebar in one request"
        },
        {
          "type": "p",
          "text": "Six dimensions, one round trip, one unit of quota:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS -H \"X-API-Key: $KEY\" --compressed \\\n  \"$API/api/facets?fields=brands,years,fuels,gearboxes,countries,sites\""
        },
        {
          "type": "p",
          "text": "Then narrow it as the user picks. Facets **cross-filter**: pass the filters already chosen and every remaining dimension re-counts against them, while each dimension still ignores its own filter so the user can change their mind."
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "# After \"BMW, under EUR 15,000\": what fuels and years are left?\ncurl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/facets?fields=fuels,years&brand=bmw&price_to=15000\""
        },
        {
          "type": "p",
          "text": "Models are per-brand and stay on their own route:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS -H \"X-API-Key: $KEY\" \"$API/api/models?brand=bmw&ordering=-count&limit=25\""
        },
        {
          "type": "p",
          "text": "Cache the sidebar. Facets are served `max-age=600` with an `ETag`; store it and send `If-None-Match` and the refresh costs an empty `304`."
        },
        {
          "type": "h2",
          "text": "2. Search, page it, and keep the totals honest"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS -H \"X-API-Key: $KEY\" --compressed \\\n  \"$API/api/search?brand=bmw&fuel=Diesel&year_from=2018&sort=price_low&limit=50&page=1\""
        },
        {
          "type": "ul",
          "items": [
            "**Paging.** Pick one spelling and stay with it — `page`+`page_size`, or `offset`+`limit`. Mixing them is a `400`. Loop to `total_pages`; a filtered search's `total` and its rows now agree, so you will not run into short pages.",
            "**Counting is the expensive half.** If you are streaming rather than showing a page count, send `include_total=false` and skip it entirely. If you want only the number, send `count_only=true` and get no rows.",
            "**Persist `site_name` + `auction_id_str`**, never `auction_id` on its own — japan ids exceed 2^53."
          ]
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "# \"How many diesel BMWs are there?\" — one cheap call, no rows.\ncurl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/search?brand=bmw&fuel=Diesel&count_only=true\" | jq .total"
        },
        {
          "type": "h2",
          "text": "3. Open one car: detail, live bid, photos"
        },
        {
          "type": "p",
          "text": "One request gets specification, condition and gallery together:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS -D headers.txt -H \"X-API-Key: $KEY\" --compressed \\\n  \"$API/api/auction/openlane/11409652\" -o car.json"
        },
        {
          "type": "p",
          "text": "Then branch on three things, in this order:"
        },
        {
          "type": "code",
          "lang": "text",
          "code": "live_price          present  -> that IS the current bid; render it\nlive_price_pending  present  -> read once more after ~2s, then stop\nneither                      -> the cycle price is the price; do not retry\n\nis_blind: true      -> there will never be a price. Show estimated_value_eur,\n                       labelled as the auction house's estimate.\ndetails_pending: t  -> vehicle_details is not there yet. Re-read on the short\n                       max-age the response carries; render the rest meanwhile.\nvault_gallery.pending > 0 -> more photos are coming. Poll on max-age=10."
        },
        {
          "type": "p",
          "text": "Photo URLs are paths, so join them to the base:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "jq -r '.auction.vault_gallery.images[] | .served_url // .remote_url' car.json \\\n  | sed \"s|^/|$API/|\""
        },
        {
          "type": "p",
          "text": "You do not need `/api/auction-images` as well — `vault_gallery` is that same body embedded. Use the separate route only when you want the gallery alone."
        },
        {
          "type": "h2",
          "text": "4. Shop the discounts, then price the import"
        },
        {
          "type": "p",
          "text": "With the market_reference field grant, top offers carry the reference used for their scoring:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/top-offers?site=openlane&min_savings_pct=20&sort=savings&limit=10\""
        },
        {
          "type": "p",
          "text": "Take one and cost it landed. Read `is_margin` off the detail response, and send the source so the fee model matches the one that priced the listing:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS -X POST -H \"X-API-Key: $KEY\" -H \"Content-Type: application/json\" \\\n  -d '{\"price\":9000,\"site_name\":\"openlane\",\"origin\":\"DE\",\"destination\":\"BG\",\"is_margin\":false}' \\\n  \"$API/api/calculator/calculate\" | jq .breakdown.estimated_total"
        },
        {
          "type": "p",
          "text": "Compare that landed total with the retail market, not with the lot price:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/cars-bg-market?brand=BMW&model=320d&year=2019&flex=1\" \\\n  | jq '{n: .snapshot.listing_count, median: .snapshot.median_price_eur}'"
        },
        {
          "type": "p",
          "text": "Check `listing_count` before you trust the median. Market snapshots are not enabled on a new key by default — ask for them."
        },
        {
          "type": "h2",
          "text": "5. Track one car over time"
        },
        {
          "type": "p",
          "text": "Price movements for a lot you are watching:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/auction/openlane/11409652/price-history\" \\\n  | jq '.history[] | {observed_at, current_price, changed_fields}'"
        },
        {
          "type": "p",
          "text": "And its life before this listing, by VIN — this is what shows a car that has been through auction more than once:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "curl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/vin/WBA8E9G50GNU12345/history\" \\\n  | jq '.auctions[] | {site_name, last_seen_at, mileage, public_price_eur, archived}'"
        },
        {
          "type": "p",
          "text": "VIN history is not enabled on a new key by default — ask for it. `event_type` on price history is `initial`, `baseline` or `change` — there is no `price_change` value."
        },
        {
          "type": "h2",
          "text": "6. Mirror the whole feed, cheaply"
        },
        {
          "type": "p",
          "text": "To keep a local copy in sync rather than to answer a query:"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "# Unfiltered = no depth limit. Walk it at the maximum page size.\ncurl -sS -H \"X-API-Key: $KEY\" -H \"Accept-Encoding: gzip\" --compressed \\\n  \"$API/api/search?limit=100&offset=0&include_total=false\""
        },
        {
          "type": "p",
          "text": "Four things make this an order of magnitude cheaper:"
        },
        {
          "type": "ol",
          "items": [
            "`--compressed` — roughly an eighth of the bytes.",
            "`include_total=false` — the count is the expensive half of a search.",
            "Store each page's `ETag` and send `If-None-Match` on the re-walk; unchanged pages answer `304` with no body.",
            "Do **not** expand every card. Hydrate detail only for rows whose `last_changed_at` moved, or that you actually display."
          ]
        },
        {
          "type": "p",
          "text": "Search results are always cycle-priced, never live-priced. That is what makes them fast enough to page — see [what stays on cycle prices](/docs/live-prices)."
        }
      ],
      "markdown": "# Recipes\n\nSix end-to-end flows that cover most of what the API is for. Each is a complete sequence, not a fragment.\n\nSet `API=https://api.thecarapi.com` and `KEY=your_key` first.\n\n## 1. Build a filter sidebar in one request\n\nSix dimensions, one round trip, one unit of quota:\n\n```bash\ncurl -sS -H \"X-API-Key: $KEY\" --compressed \\\n  \"$API/api/facets?fields=brands,years,fuels,gearboxes,countries,sites\"\n```\n\nThen narrow it as the user picks. Facets **cross-filter**: pass the filters already chosen and every remaining dimension re-counts against them, while each dimension still ignores its own filter so the user can change their mind.\n\n```bash\n# After \"BMW, under EUR 15,000\": what fuels and years are left?\ncurl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/facets?fields=fuels,years&brand=bmw&price_to=15000\"\n```\n\nModels are per-brand and stay on their own route:\n\n```bash\ncurl -sS -H \"X-API-Key: $KEY\" \"$API/api/models?brand=bmw&ordering=-count&limit=25\"\n```\n\nCache the sidebar. Facets are served `max-age=600` with an `ETag`; store it and send `If-None-Match` and the refresh costs an empty `304`.\n\n## 2. Search, page it, and keep the totals honest\n\n```bash\ncurl -sS -H \"X-API-Key: $KEY\" --compressed \\\n  \"$API/api/search?brand=bmw&fuel=Diesel&year_from=2018&sort=price_low&limit=50&page=1\"\n```\n\n- **Paging.** Pick one spelling and stay with it — `page`+`page_size`, or `offset`+`limit`. Mixing them is a `400`. Loop to `total_pages`; a filtered search's `total` and its rows now agree, so you will not run into short pages.\n- **Counting is the expensive half.** If you are streaming rather than showing a page count, send `include_total=false` and skip it entirely. If you want only the number, send `count_only=true` and get no rows.\n- **Persist `site_name` + `auction_id_str`**, never `auction_id` on its own — japan ids exceed 2^53.\n\n```bash\n# \"How many diesel BMWs are there?\" — one cheap call, no rows.\ncurl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/search?brand=bmw&fuel=Diesel&count_only=true\" | jq .total\n```\n\n## 3. Open one car: detail, live bid, photos\n\nOne request gets specification, condition and gallery together:\n\n```bash\ncurl -sS -D headers.txt -H \"X-API-Key: $KEY\" --compressed \\\n  \"$API/api/auction/openlane/11409652\" -o car.json\n```\n\nThen branch on three things, in this order:\n\n```text\nlive_price          present  -> that IS the current bid; render it\nlive_price_pending  present  -> read once more after ~2s, then stop\nneither                      -> the cycle price is the price; do not retry\n\nis_blind: true      -> there will never be a price. Show estimated_value_eur,\n                       labelled as the auction house's estimate.\ndetails_pending: t  -> vehicle_details is not there yet. Re-read on the short\n                       max-age the response carries; render the rest meanwhile.\nvault_gallery.pending > 0 -> more photos are coming. Poll on max-age=10.\n```\n\nPhoto URLs are paths, so join them to the base:\n\n```bash\njq -r '.auction.vault_gallery.images[] | .served_url // .remote_url' car.json \\\n  | sed \"s|^/|$API/|\"\n```\n\nYou do not need `/api/auction-images` as well — `vault_gallery` is that same body embedded. Use the separate route only when you want the gallery alone.\n\n## 4. Shop the discounts, then price the import\n\nWith the market_reference field grant, top offers carry the reference used for their scoring:\n\n```bash\ncurl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/top-offers?site=openlane&min_savings_pct=20&sort=savings&limit=10\"\n```\n\nTake one and cost it landed. Read `is_margin` off the detail response, and send the source so the fee model matches the one that priced the listing:\n\n```bash\ncurl -sS -X POST -H \"X-API-Key: $KEY\" -H \"Content-Type: application/json\" \\\n  -d '{\"price\":9000,\"site_name\":\"openlane\",\"origin\":\"DE\",\"destination\":\"BG\",\"is_margin\":false}' \\\n  \"$API/api/calculator/calculate\" | jq .breakdown.estimated_total\n```\n\nCompare that landed total with the retail market, not with the lot price:\n\n```bash\ncurl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/cars-bg-market?brand=BMW&model=320d&year=2019&flex=1\" \\\n  | jq '{n: .snapshot.listing_count, median: .snapshot.median_price_eur}'\n```\n\nCheck `listing_count` before you trust the median. Market snapshots are not enabled on a new key by default — ask for them.\n\n## 5. Track one car over time\n\nPrice movements for a lot you are watching:\n\n```bash\ncurl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/auction/openlane/11409652/price-history\" \\\n  | jq '.history[] | {observed_at, current_price, changed_fields}'\n```\n\nAnd its life before this listing, by VIN — this is what shows a car that has been through auction more than once:\n\n```bash\ncurl -sS -H \"X-API-Key: $KEY\" \\\n  \"$API/api/vin/WBA8E9G50GNU12345/history\" \\\n  | jq '.auctions[] | {site_name, last_seen_at, mileage, public_price_eur, archived}'\n```\n\nVIN history is not enabled on a new key by default — ask for it. `event_type` on price history is `initial`, `baseline` or `change` — there is no `price_change` value.\n\n## 6. Mirror the whole feed, cheaply\n\nTo keep a local copy in sync rather than to answer a query:\n\n```bash\n# Unfiltered = no depth limit. Walk it at the maximum page size.\ncurl -sS -H \"X-API-Key: $KEY\" -H \"Accept-Encoding: gzip\" --compressed \\\n  \"$API/api/search?limit=100&offset=0&include_total=false\"\n```\n\nFour things make this an order of magnitude cheaper:\n\n1. `--compressed` — roughly an eighth of the bytes.\n2. `include_total=false` — the count is the expensive half of a search.\n3. Store each page's `ETag` and send `If-None-Match` on the re-walk; unchanged pages answer `304` with no body.\n4. Do **not** expand every card. Hydrate detail only for rows whose `last_changed_at` moved, or that you actually display.\n\nSearch results are always cycle-priced, never live-priced. That is what makes them fast enough to page — see [what stays on cycle prices](https://thecarapi.com/docs/live-prices)."
    },
    {
      "id": "agents",
      "label": "Agents & machine formats",
      "summary": "Every page of this reference as Markdown, plain text, JSON and OpenAPI — and how to point a coding agent at it.",
      "canonical": "https://thecarapi.com/docs/agents",
      "blocks": [
        {
          "type": "p",
          "text": "This documentation is generated from structured data, so it can be served in whatever shape reads it. If you are integrating with a coding agent or an LLM, take one of the files below rather than parsing these HTML pages."
        },
        {
          "type": "h2",
          "text": "Formats"
        },
        {
          "type": "table",
          "head": [
            "Format",
            "URL",
            "Use it for"
          ],
          "rows": [
            [
              "Markdown, one page",
              "`https://thecarapi.com/docs/<section>.md`",
              "Pasting a single section into a prompt or a repo."
            ],
            [
              "Markdown, everything",
              "`https://thecarapi.com/docs/thecarapi-api-reference.md`",
              "The entire reference as one file, front matter included."
            ],
            [
              "Plain text, everything",
              "`https://thecarapi.com/docs/thecarapi-api-reference.txt`",
              "Context windows and tools that choke on Markdown tables."
            ],
            [
              "JSON",
              "`https://thecarapi.com/docs/thecarapi-api-reference.json`",
              "Programmatic access to sections, endpoints, parameters and response fields."
            ],
            [
              "OpenAPI",
              "`https://thecarapi.com/openapi.json`",
              "Client generation, Postman/Insomnia import, agent tool definitions. OpenAPI 3.0.3 with full response schemas, parameter definitions and security requirements."
            ],
            [
              "Postman collection",
              "`https://thecarapi.com/thecarapi.postman_collection.json`",
              "Import straight into Postman — every endpoint, grouped and described."
            ],
            [
              "`llms.txt`",
              "`https://thecarapi.com/llms.txt`",
              "The site index in the llms.txt convention."
            ],
            [
              "Docs index for agents",
              "`https://thecarapi.com/docs/llms.txt`",
              "Just the documentation tree, with a one-line summary per section."
            ]
          ]
        },
        {
          "type": "p",
          "text": "Every docs page also advertises its Markdown twin in the head as `<link rel=\"alternate\" type=\"text/markdown\">`, and the **Copy** and **Download** controls at the top of each page produce exactly these files client-side."
        },
        {
          "type": "h2",
          "text": "Fetching them"
        },
        {
          "type": "code",
          "lang": "bash",
          "code": "# One section\ncurl -sS https://thecarapi.com/docs/live-prices.md\n\n# The whole reference, as Markdown\ncurl -sS https://thecarapi.com/docs/thecarapi-api-reference.md\n\n# The endpoint surface, as OpenAPI\ncurl -sS https://thecarapi.com/openapi.json | jq '.paths | keys'",
          "caption": "These are static files. No API key is needed to read the documentation."
        },
        {
          "type": "h2",
          "text": "Giving an agent the right context"
        },
        {
          "type": "p",
          "text": "The full reference is large. For most tasks an agent needs three things: the conventions, the data dictionary, and the one endpoint group it is calling. Fetching those three sections is cheaper and more accurate than dropping the whole document into a context window."
        },
        {
          "type": "code",
          "lang": "text",
          "code": "You are integrating with TheCarApi.\n\nRead these first:\n  https://thecarapi.com/docs/conventions.md     — envelope, ETags, pagination, rate limits\n  https://thecarapi.com/docs/errors.md          — status codes and what is worth retrying\n  https://thecarapi.com/docs/recipes.md         — six complete flows, end to end\n  https://thecarapi.com/docs/<group>.md         — the endpoint group you are calling\n  https://thecarapi.com/openapi.json            — machine-readable parameter and response shapes\n\nRules that are not obvious from the schema:\n  - A vehicle is addressed by site_name + auction_id_str, e.g. encar/38112900.\n  - Prices are JSON numbers, never strings.\n  - An absent live_price block is normal, not an error.\n  - Never hardcode the source list; read /api/sites and /api/contract at startup.\n  - Retry 429/503 honouring Retry-After. Never retry 400/401/403/404.",
          "caption": "A starting prompt. Swap `<group>` for `search`, `auctions`, `car-details` and so on."
        },
        {
          "type": "h2",
          "text": "Crawling policy"
        },
        {
          "type": "p",
          "text": "`robots.txt` allows search and AI citation crawlers and disallows bulk training harvesters. The documentation is public and unauthenticated; the API behind it is not. Nothing in these files is a credential. Data endpoints require a key; liveness, readiness and returned report/image delivery URLs are exceptions."
        },
        {
          "type": "callout",
          "tone": "info",
          "title": "Generated, not hand-maintained",
          "text": "HTML, Markdown, text and JSON are generated from the structured documentation. OpenAPI and Postman share a reviewed contract snapshot. The documentation checks verify endpoint coverage and downloadable requests whenever the contract is updated."
        }
      ],
      "markdown": "# Agents & machine formats\n\nEvery page of this reference as Markdown, plain text, JSON and OpenAPI — and how to point a coding agent at it.\n\nThis documentation is generated from structured data, so it can be served in whatever shape reads it. If you are integrating with a coding agent or an LLM, take one of the files below rather than parsing these HTML pages.\n\n## Formats\n\n| Format | URL | Use it for |\n| --- | --- | --- |\n| Markdown, one page | `https://thecarapi.com/docs/<section>.md` | Pasting a single section into a prompt or a repo. |\n| Markdown, everything | `https://thecarapi.com/docs/thecarapi-api-reference.md` | The entire reference as one file, front matter included. |\n| Plain text, everything | `https://thecarapi.com/docs/thecarapi-api-reference.txt` | Context windows and tools that choke on Markdown tables. |\n| JSON | `https://thecarapi.com/docs/thecarapi-api-reference.json` | Programmatic access to sections, endpoints, parameters and response fields. |\n| OpenAPI | `https://thecarapi.com/openapi.json` | Client generation, Postman/Insomnia import, agent tool definitions. OpenAPI 3.0.3 with full response schemas, parameter definitions and security requirements. |\n| Postman collection | `https://thecarapi.com/thecarapi.postman_collection.json` | Import straight into Postman — every endpoint, grouped and described. |\n| `llms.txt` | `https://thecarapi.com/llms.txt` | The site index in the llms.txt convention. |\n| Docs index for agents | `https://thecarapi.com/docs/llms.txt` | Just the documentation tree, with a one-line summary per section. |\n\nEvery docs page also advertises its Markdown twin in the head as `<link rel=\"alternate\" type=\"text/markdown\">`, and the **Copy** and **Download** controls at the top of each page produce exactly these files client-side.\n\n## Fetching them\n\n```bash\n# One section\ncurl -sS https://thecarapi.com/docs/live-prices.md\n\n# The whole reference, as Markdown\ncurl -sS https://thecarapi.com/docs/thecarapi-api-reference.md\n\n# The endpoint surface, as OpenAPI\ncurl -sS https://thecarapi.com/openapi.json | jq '.paths | keys'\n```\n\n_These are static files. No API key is needed to read the documentation._\n\n## Giving an agent the right context\n\nThe full reference is large. For most tasks an agent needs three things: the conventions, the data dictionary, and the one endpoint group it is calling. Fetching those three sections is cheaper and more accurate than dropping the whole document into a context window.\n\n```text\nYou are integrating with TheCarApi.\n\nRead these first:\n  https://thecarapi.com/docs/conventions.md     — envelope, ETags, pagination, rate limits\n  https://thecarapi.com/docs/errors.md          — status codes and what is worth retrying\n  https://thecarapi.com/docs/recipes.md         — six complete flows, end to end\n  https://thecarapi.com/docs/<group>.md         — the endpoint group you are calling\n  https://thecarapi.com/openapi.json            — machine-readable parameter and response shapes\n\nRules that are not obvious from the schema:\n  - A vehicle is addressed by site_name + auction_id_str, e.g. encar/38112900.\n  - Prices are JSON numbers, never strings.\n  - An absent live_price block is normal, not an error.\n  - Never hardcode the source list; read /api/sites and /api/contract at startup.\n  - Retry 429/503 honouring Retry-After. Never retry 400/401/403/404.\n```\n\n_A starting prompt. Swap `<group>` for `search`, `auctions`, `car-details` and so on._\n\n## Crawling policy\n\n`robots.txt` allows search and AI citation crawlers and disallows bulk training harvesters. The documentation is public and unauthenticated; the API behind it is not. Nothing in these files is a credential. Data endpoints require a key; liveness, readiness and returned report/image delivery URLs are exceptions.\n\n> **Generated, not hand-maintained** — HTML, Markdown, text and JSON are generated from the structured documentation. OpenAPI and Postman share a reviewed contract snapshot. The documentation checks verify endpoint coverage and downloadable requests whenever the contract is updated."
    }
  ],
  "endpoint_groups": [
    {
      "id": "search",
      "label": "Search & discovery",
      "description": "Search live auction inventory, then resolve lightweight full-text matches. Only live lots are returned. include_ended=true also shows lots whose clock has run out but that have not been archived yet; finished auctions are searched with /api/archive/search.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/search",
          "summary": "Primary filtered search over live auction inventory.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "search",
              "type": "string",
              "example": "BMW 320d",
              "source": "Free text entered by your user.",
              "description": "Free-text brand, model, or keyword query."
            },
            {
              "name": "site",
              "type": "string",
              "example": "encar",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs, OR within the parameter. Case-insensitive. An unknown slug is a 400 naming the offender — one bad member rejects the whole list."
            },
            {
              "name": "site_exclude",
              "type": "string",
              "example": "copart",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to leave out. Sent alongside site, it narrows the include list; excluding everything you included is a 400, not an empty page."
            },
            {
              "name": "brand",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Brand display name, slug, or brand_id — all three select the same inventory. Matched on the brand identity rather than the published spelling, so sources that write it differently (openlane publishes \"Volkswagen\" where others write \"VW\") are all included."
            },
            {
              "name": "model",
              "type": "string",
              "example": "320d",
              "source": "/api/models?brand=bmw -> models[].slug",
              "description": "Model name or slug."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range."
            },
            {
              "name": "fuel",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[]",
              "description": "Canonical fuel group, matched whole. Hybrid, Mild Hybrid and Plug-in Hybrid are three separate groups — fuel=Hybrid returns full hybrids only. A lot with no recorded fuel matches every fuel filter rather than none, so it stays reachable."
            },
            {
              "name": "gearbox",
              "type": "string",
              "example": "Automatic",
              "source": "/api/gearboxes -> gearboxes[]",
              "description": "Canonical gearbox group, matched whole. Same unknown-matches-all rule as fuel: a car with no recorded gearbox answers every gearbox filter."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe."
            },
            {
              "name": "vehicle_type",
              "type": "string",
              "example": "cars",
              "source": "Choose from the documented vehicle_type enum.",
              "description": "cars or suvs & trucks."
            },
            {
              "name": "body_style",
              "type": "string",
              "example": "sedan",
              "source": "Your application's normalized body-style value.",
              "description": "Body-style filter."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Left- or right-hand drive. The parameter takes two values, each with four equivalent spellings. Every listing resolves to one side or the other, so the two values partition the inventory and their counts sum to the unfiltered total."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 80000",
              "source": "Mileage range chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Horsepower range chosen by your user.",
              "description": "Horsepower range."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "10000 / 30000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Return damaged or broken vehicles only."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Return Buy-Now listings only."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Return auctions ending within about two hours."
            },
            {
              "name": "smart_filter",
              "type": "enum",
              "example": "best_value",
              "source": "Choose from the documented smart_filter enum.",
              "description": "top_offers, low_mileage, or best_value."
            },
            {
              "name": "is_active",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Only true, the default, is accepted. false is a 400: this endpoint returns live lots only. For finished auctions use /api/archive/search."
            },
            {
              "name": "include_ended",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Default false. true also shows lots whose clock has run out but that have not been archived yet. Also accepts 1/0/yes/no."
            },
            {
              "name": "sort",
              "type": "enum",
              "example": "price_low",
              "source": "Choose from the documented Sort enum.",
              "description": "Sort value from the data dictionary. Default default — photo-first, then newest. The retired random value is still accepted and is served as default."
            },
            {
              "name": "limit / page_size",
              "type": "integer",
              "example": "24",
              "source": "Page size chosen by your application.",
              "description": "Page size. Default 100, maximum pagination.max_limit from /api/contract — 100, or up to 500 on a key granted bulk pages. Larger values are clamped down."
            },
            {
              "name": "offset / page",
              "type": "integer",
              "example": "24 / 2",
              "source": "Current page state in your application.",
              "description": "Offset-based or page-based pagination."
            },
            {
              "name": "include_total",
              "type": "boolean",
              "example": "false",
              "source": "Your performance/display choice.",
              "description": "Set false to skip the count. Default true."
            },
            {
              "name": "count_only",
              "type": "boolean",
              "example": "true",
              "source": "Your response-mode choice.",
              "description": "Return counts without result rows."
            }
          ],
          "examples": [
            "/api/search?brand=bmw&fuel=Diesel&year_from=2018&sort=price_low&limit=24",
            "/api/search?site=encar&country=KR&buy_now=true&page=2&page_size=20"
          ],
          "response": "{\n  \"success\": true,\n  \"results\": [\n    {\n      \"auction_id\": 38112900,\n      \"site_name\": \"encar\",\n      \"clean_make\": \"BMW\",\n      \"clean_model\": \"320d\",\n      \"registration_year\": 2020,\n      \"mileage\": 45000,\n      \"public_price_eur\": 21500,\n      \"thumbnail_url\": \"https://cdn.example/photo.webp\"\n    }\n  ],\n  \"total\": 18342,\n  \"limit\": 24,\n  \"offset\": 0,\n  \"count_only\": false,\n  \"page\": 1,\n  \"page_size\": 24,\n  \"total_pages\": 764,\n  \"max_page\": 764,\n  \"secret_mode\": false,\n  \"contract_version\": \"2026-10-05\"\n}",
          "responseFields": [
            {
              "name": "results[]",
              "type": "object[]",
              "description": "Search result cards. The full allowlisted field set is in the data dictionary; fields are present when the source provides them. Address a car by auction_id_str, not auction_id — japan ids exceed 2^53, so a client parsing JSON numbers as doubles silently rounds the numeric one."
            },
            {
              "name": "results[].images",
              "type": "object[]",
              "description": "At most the first 8 display-ordered gallery entries, primary first. This is a card preview, never the full gallery — read vault_gallery on the detail response or /api/auction-images/{site}/{id} for everything."
            },
            {
              "name": "total",
              "type": "integer | null",
              "description": "Total matching rows, counting the same lots the pages return — so paging to total_pages does not run into short or empty pages. Null when include_total=false or totals are suppressed."
            },
            {
              "name": "limit / offset",
              "type": "integer",
              "description": "The window that was actually applied, echoed back. On a count_only=true response both are 0."
            },
            {
              "name": "page / page_size / total_pages / max_page",
              "type": "integer | null",
              "description": "The same window in page terms, plus total pages and the deepest reachable page. All four are null on a count_only response, which has no pages to describe. max_page equals total_pages here — /api/search has no offset cap."
            },
            {
              "name": "count_only",
              "type": "boolean",
              "description": "Echoes the parameter. On true the rows are omitted and only total remains."
            },
            {
              "name": "secret_mode",
              "type": "boolean",
              "description": "Legacy field, always false. Ignore it."
            },
            {
              "name": "contract_version",
              "type": "string",
              "description": "Schema contract the response was built against."
            },
            {
              "name": "request_id / server_time / data_updated_at",
              "type": "string",
              "description": "Correlation id (also X-Request-ID), response time, and when the underlying data was last refreshed."
            }
          ],
          "note": "site accepts the slugs /api/sites returns for your key — guazi, openlane, auto1, copart, schadeautos, bca, autorola, dealercars, encar, kbchachacha, koreaauctions and japan at the time of writing — and the European classifieds network is not a site value. Both site and site_exclude take a comma-separated, case-insensitive list; an unknown slug on either is a 400 naming the offender, and passing both narrows the include list by the exclusion. There is no offset cap: filtered or not, max_page equals total_pages. A very deep filtered search is answered from a compact index where it can be; where it cannot, it is subject to the safety timeout and may come back 503 asking you to narrow it. Pages past offset 5000 are not cached."
        },
        {
          "method": "GET",
          "path": "/api/search/auction-ids",
          "summary": "Resolve a free-text query to matching auction ids only.",
          "scope": "search",
          "params": [
            {
              "name": "q",
              "type": "string",
              "example": "bmw 320d touring",
              "source": "Free text entered by your user.",
              "description": "Free-text query. An empty or missing query returns an empty list, not a 400."
            }
          ],
          "examples": [
            "/api/search/auction-ids?q=bmw%20320d",
            "/api/search/auction-ids?q=kia%20ev6"
          ],
          "response": "{\n  \"success\": true,\n  \"auction_ids\": [\n    38112900,\n    11409652\n  ],\n  \"found\": 2\n}",
          "responseFields": [
            {
              "name": "auction_ids",
              "type": "integer[]",
              "description": "Bare auction_id values with no source attached. An id is only unambiguous if it happens to be unique across sources — which is exactly what /api/auction/{site}/{id} needs a site for. Use this as a filter, not as an addressing scheme; hydrate through /api/search?search= instead, which returns whole cards carrying site_name and auction_id_str."
            },
            {
              "name": "found",
              "type": "integer",
              "description": "Length of auction_ids. The whole match set is returned, capped at 10,000 ids — there is no paging."
            }
          ],
          "note": "Not enabled on a new key by default — ask for it. Three answers are possible: 200 with an empty list when full-text search is switched off or q is empty; 503 with success: false and an empty list when full-text search is enabled but unreachable; 500 with success: false and an empty list for anything else. An empty list on a 200 genuinely means \"no matches\" only when you already know the feature is on. Treat success: false as \"unknown\", never as \"zero\". This response carries no envelope metadata — read X-Request-ID from the header."
        }
      ]
    },
    {
      "id": "facets",
      "label": "Filter facets",
      "description": "Discover live filter values and counts. Every facet endpoint accepts the search filters, so a facet describes whatever slice of inventory you are looking at rather than the whole of it — each dimension ignoring only its own filter, so a user can switch brands without the option disappearing. Counts and the totals /api/search reports for the same filters cannot disagree. Facet responses cache for about 600 seconds, and a filter sidebar can read every flat dimension in one call with /api/facets.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/types",
          "summary": "Trim filter values for one brand and model",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "damaged",
              "type": "boolean",
              "description": "`true` restricts to damaged/broken vehicles, `false` to undamaged ones. Also accepts `1`/`0`/`yes`/`no`.",
              "example": "true",
              "source": "Current API reference"
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "description": "`true` restricts to Buy-Now listings. Also accepts `1`/`0`/`yes`/`no`.",
              "example": "true",
              "source": "Current API reference"
            },
            {
              "name": "country",
              "type": "string",
              "description": "ISO country code from `/api/countries`, or the pseudo-country `europe`.\nAccepted and ignored by `/api/sites` and `/load-models`.\n",
              "example": "DE",
              "source": "Current API reference"
            },
            {
              "name": "brand",
              "type": "string",
              "description": "Brand display name, slug (from `/api/brands`), or numeric `brand_id`.",
              "example": "bmw",
              "source": "Current API reference",
              "required": true
            },
            {
              "name": "model",
              "type": "string",
              "description": "Model name from `/api/models`. Comma-separated values select several.",
              "example": "5-Series",
              "source": "Current API reference",
              "required": true
            },
            {
              "name": "year_from",
              "type": "integer",
              "description": "Lower bound on registration year, inclusive.",
              "example": "2018",
              "source": "Current API reference"
            },
            {
              "name": "year_to",
              "type": "integer",
              "description": "Upper bound on registration year, inclusive.",
              "example": "2024",
              "source": "Current API reference"
            },
            {
              "name": "production_year_from",
              "type": "integer",
              "description": "Lower bound on production year, inclusive. `production_year` falls back to the registration year where the source publishes no build year, so missing build-year evidence falls back to the registration-year dimension.",
              "example": "2018",
              "source": "Current API reference"
            },
            {
              "name": "production_year_to",
              "type": "integer",
              "description": "Upper bound on production year, inclusive.",
              "example": "2024",
              "source": "Current API reference"
            },
            {
              "name": "fuel",
              "type": "string",
              "description": "Canonical fuel group from `/api/fuels`.",
              "example": "Diesel",
              "source": "Current API reference"
            },
            {
              "name": "gearbox",
              "type": "string",
              "description": "Canonical gearbox group from `/api/gearboxes`.",
              "example": "Automatic",
              "source": "Current API reference"
            },
            {
              "name": "kilometers_from",
              "type": "integer",
              "description": "Lower bound on mileage in kilometres.",
              "example": "0",
              "source": "Current API reference"
            },
            {
              "name": "kilometers_to",
              "type": "integer",
              "description": "Upper bound on mileage in kilometres.",
              "example": "150000",
              "source": "Current API reference"
            },
            {
              "name": "horsepower_from",
              "type": "integer",
              "description": "",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "horsepower_to",
              "type": "integer",
              "description": "",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "price_from",
              "type": "integer",
              "description": "Lower bound on the public EUR price.",
              "example": "1000",
              "source": "Current API reference"
            },
            {
              "name": "price_to",
              "type": "integer",
              "description": "Upper bound on the public EUR price.",
              "example": "30000",
              "source": "Current API reference"
            },
            {
              "name": "type",
              "type": "string",
              "description": "Trim / version, as listed by [`/api/types`](#operation/listTypes).\nComma-separated values select several. Cars with no trim recorded are\nkept, so a trim filter narrows the list without hiding unclassified\nstock.\n",
              "example": "520d",
              "source": "Current API reference"
            },
            {
              "name": "vehicle_type",
              "type": "string",
              "description": "",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "body_style",
              "type": "string",
              "description": "",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "description": "`true` restricts to auctions ending within roughly two hours.",
              "example": "true",
              "source": "Current API reference"
            },
            {
              "name": "site",
              "type": "string",
              "description": "Comma-separated source slugs from `/api/sites`, OR within the parameter.\nCase-insensitive; padding and empty members are ignored. An unknown slug\nis a `400` naming the offender.\n",
              "example": "openlane,auto1",
              "source": "Current API reference"
            },
            {
              "name": "site_exclude",
              "type": "string",
              "description": "Comma-separated source slugs to leave out. Same rules as `site`.",
              "example": "copart",
              "source": "Current API reference"
            },
            {
              "name": "search",
              "type": "string",
              "description": "Substring filter on the facet value name.",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "ordering",
              "type": "string",
              "description": "",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "limit",
              "type": "integer",
              "description": "Maximum facet values to return.",
              "example": "20",
              "source": "Current API reference"
            },
            {
              "name": "steering",
              "type": "string",
              "description": "Which side the steering wheel is on. The parameter takes two values,\n`left` and `right`; `lhd`/`left-hand`/`left_hand` and\n`rhd`/`right-hand`/`right_hand` are equivalent spellings of them. Any\nother value is a 400.\n\nDerived rather than stored. The side is taken from the car's\nregistration market, so a car from an ISO country that drives on the left\n(GB, IE, JP, CY, MT, AU, NZ, ZA, IN, TH, HK, SG) is `right` and everything\nelse is `left`. Every listing resolves to one side or the other, so the\ntwo values partition the inventory and their totals sum to the unfiltered\ntotal. The same value is published on each result as `steering`.\n\nAccepted by `/api/search`, `/api/listVehicles`, `/api/top-offers` and\nevery facet endpoint, which narrow their option lists by it. Sending it\nto a facet endpoint takes that request off the precomputed facet table\nand onto a live aggregate, so it is slower than an unfiltered facet call.\n",
              "example": "left",
              "source": "Current API reference"
            }
          ],
          "examples": [
            "/api/types?brand=bmw&model=320d"
          ],
          "response": "{\n  \"success\": true,\n  \"types\": [\n    {\n      \"name\": \"Touring\",\n      \"count\": 12\n    }\n  ],\n  \"grouped\": {\"320d\": [{\"name\": \"Touring\", \"count\": 12}]}\n}",
          "note": "`brand` and `model` are both required — omitting either is a `400`.\n\nReturns the trims flat in `types`, each carrying the model it belongs\nto, and again in `grouped` keyed by that model, so a sidebar can nest\nthem under the model the user picked without regrouping client-side.\n\nA model is stored under each source's own spelling of its name\n(`5-Series`, `5 Series`, `5-серия`). All of them are accepted here and\ntheir trims are returned once, under the one canonical name\n[`/api/models`](#operation/listModels) offers, with the counts summed.\n"
        },
        {
          "method": "GET",
          "path": "/api/production-years",
          "summary": "Available production years",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "damaged",
              "type": "boolean",
              "description": "`true` restricts to damaged/broken vehicles, `false` to undamaged ones. Also accepts `1`/`0`/`yes`/`no`.",
              "example": "true",
              "source": "Current API reference"
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "description": "`true` restricts to Buy-Now listings. Also accepts `1`/`0`/`yes`/`no`.",
              "example": "true",
              "source": "Current API reference"
            },
            {
              "name": "country",
              "type": "string",
              "description": "ISO country code from `/api/countries`, or the pseudo-country `europe`.\nAccepted and ignored by `/api/sites` and `/load-models`.\n",
              "example": "DE",
              "source": "Current API reference"
            },
            {
              "name": "brand",
              "type": "string",
              "description": "Brand display name, slug (from `/api/brands`), or numeric `brand_id`.",
              "example": "bmw",
              "source": "Current API reference"
            },
            {
              "name": "model",
              "type": "string",
              "description": "Model name or slug.",
              "example": "320d",
              "source": "Current API reference"
            },
            {
              "name": "year_from",
              "type": "integer",
              "description": "Lower bound on registration year, inclusive.",
              "example": "2018",
              "source": "Current API reference"
            },
            {
              "name": "year_to",
              "type": "integer",
              "description": "Upper bound on registration year, inclusive.",
              "example": "2024",
              "source": "Current API reference"
            },
            {
              "name": "production_year_from",
              "type": "integer",
              "description": "Lower bound on production year, inclusive. `production_year` falls back to the registration year where the source publishes no build year, so missing build-year evidence falls back to the registration-year dimension.",
              "example": "2018",
              "source": "Current API reference"
            },
            {
              "name": "production_year_to",
              "type": "integer",
              "description": "Upper bound on production year, inclusive.",
              "example": "2024",
              "source": "Current API reference"
            },
            {
              "name": "fuel",
              "type": "string",
              "description": "Canonical fuel group from `/api/fuels`.",
              "example": "Diesel",
              "source": "Current API reference"
            },
            {
              "name": "gearbox",
              "type": "string",
              "description": "Canonical gearbox group from `/api/gearboxes`.",
              "example": "Automatic",
              "source": "Current API reference"
            },
            {
              "name": "kilometers_from",
              "type": "integer",
              "description": "Lower bound on mileage in kilometres.",
              "example": "0",
              "source": "Current API reference"
            },
            {
              "name": "kilometers_to",
              "type": "integer",
              "description": "Upper bound on mileage in kilometres.",
              "example": "150000",
              "source": "Current API reference"
            },
            {
              "name": "horsepower_from",
              "type": "integer",
              "description": "",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "horsepower_to",
              "type": "integer",
              "description": "",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "price_from",
              "type": "integer",
              "description": "Lower bound on the public EUR price.",
              "example": "1000",
              "source": "Current API reference"
            },
            {
              "name": "price_to",
              "type": "integer",
              "description": "Upper bound on the public EUR price.",
              "example": "30000",
              "source": "Current API reference"
            },
            {
              "name": "type",
              "type": "string",
              "description": "Trim / version, as listed by [`/api/types`](#operation/listTypes).\nComma-separated values select several. Cars with no trim recorded are\nkept, so a trim filter narrows the list without hiding unclassified\nstock.\n",
              "example": "520d",
              "source": "Current API reference"
            },
            {
              "name": "vehicle_type",
              "type": "string",
              "description": "",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "body_style",
              "type": "string",
              "description": "",
              "example": "",
              "source": "Current API reference"
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "description": "`true` restricts to auctions ending within roughly two hours.",
              "example": "true",
              "source": "Current API reference"
            },
            {
              "name": "site",
              "type": "string",
              "description": "Comma-separated source slugs from `/api/sites`, OR within the parameter.\nCase-insensitive; padding and empty members are ignored. An unknown slug\nis a `400` naming the offender.\n",
              "example": "openlane,auto1",
              "source": "Current API reference"
            },
            {
              "name": "site_exclude",
              "type": "string",
              "description": "Comma-separated source slugs to leave out. Same rules as `site`.",
              "example": "copart",
              "source": "Current API reference"
            },
            {
              "name": "steering",
              "type": "string",
              "description": "Which side the steering wheel is on. The parameter takes two values,\n`left` and `right`; `lhd`/`left-hand`/`left_hand` and\n`rhd`/`right-hand`/`right_hand` are equivalent spellings of them. Any\nother value is a 400.\n\nDerived rather than stored. The side is taken from the car's\nregistration market, so a car from an ISO country that drives on the left\n(GB, IE, JP, CY, MT, AU, NZ, ZA, IN, TH, HK, SG) is `right` and everything\nelse is `left`. Every listing resolves to one side or the other, so the\ntwo values partition the inventory and their totals sum to the unfiltered\ntotal. The same value is published on each result as `steering`.\n\nAccepted by `/api/search`, `/api/listVehicles`, `/api/top-offers` and\nevery facet endpoint, which narrow their option lists by it. Sending it\nto a facet endpoint takes that request off the precomputed facet table\nand onto a live aggregate, so it is slower than an unfiltered facet call.\n",
              "example": "left",
              "source": "Current API reference"
            }
          ],
          "examples": [
            "/api/production-years"
          ],
          "response": "{\n  \"success\": true,\n  \"production_years\": [\n    2023,\n    2022\n  ],\n  \"production_year_counts\": [\n    {\n      \"value\": 2023,\n      \"count\": 12\n    }\n  ]\n}",
          "note": "Distinct production years in the filtered inventory, newest first. `production_year` falls back to the registration year where the source publishes no build year, so this vocabulary is as wide as `/api/years` rather than a sparse subset of it. `production_year_counts` carries the per-year offer count under the same filters."
        },
        {
          "method": "GET",
          "path": "/api/facets",
          "summary": "Every facet dimension of a filter sidebar in one request.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "fields",
              "type": "string",
              "example": "brands,fuels,gearboxes",
              "source": "The dimensions your sidebar actually renders.",
              "description": "Comma-separated subset of brands, years, production_years, fuels, countries, gearboxes, sites, plus the opt-in models and types. Defaults to all but the opt-in two. models needs brand, types needs brand and model, exactly as /api/models and /api/types do; without them the field is reported under errors. That makes a full sidebar one call: ?brand=BMW&model=3-Series&fields=brands,models,types,years,fuels."
            },
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "brand / model",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Name, slug, or brand_id, exactly as on /api/search."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range. Accepted and ignored by /api/years itself, which is the dimension they would narrow."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical group, matched whole."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe. Accepted and ignored by /api/sites and /load-models."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "0 / 5000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range. price_to=5000 on /api/brands returns each brand count under EUR 5,000."
            },
            {
              "name": "vehicle_type / body_style",
              "type": "string",
              "example": "cars",
              "source": "As on /api/search.",
              "description": "Vehicle-type and body-style filters."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Restrict to left- or right-hand-drive vehicles. Slower than an unfiltered facet call, because it is computed live rather than read from the precomputed facet table."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to damaged or broken vehicles."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to Buy-Now inventory."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to lots ending within about two hours."
            }
          ],
          "examples": [
            "/api/facets?fields=brands,fuels,gearboxes&country=DE",
            "/api/facets?fields=brands,years,sites&damaged=true",
            "/api/facets?brand=bmw&model=320d&fields=brands,models,types,years,fuels"
          ],
          "response": "{\n  \"success\": true,\n  \"brands\": [\n    {\n      \"id\": 12,\n      \"name\": \"BMW\",\n      \"slug\": \"bmw\",\n      \"count\": 3266\n    }\n  ],\n  \"fuels\": [\n    \"Diesel\",\n    \"Petrol\"\n  ],\n  \"gearboxes\": [\n    \"Automatic\",\n    \"Manual\"\n  ]\n}",
          "responseFields": [
            {
              "name": "brands / years / production_years / fuels / countries / gearboxes / sites / models / types",
              "type": "array",
              "description": "Each field is its own endpoint’s payload merged verbatim (models and types only when named in fields) — countries still ships country_details alongside it, and no field gains or loses counts by being requested here."
            },
            {
              "name": "errors",
              "type": "object",
              "description": "Present only when a dimension could not be served: { field: reason }. Every other field is still returned, so one slow dimension cannot blank a filter sidebar."
            }
          ],
          "note": "Every filter accepted by the individual facet endpoints applies here, per field, exactly as its own endpoint would apply it — including the cross-filtering, so ?price_to=5000&fields=brands returns each brand count under EUR 5,000 and value lists omit options with no matches left. is_active and include_ended are not accepted: facets always describe lots whose auction is still open. The per-dimension endpoints below are unchanged and stay fully supported — this is an additional way to read the same data. Scope search, the same scope that already grants them, and the fan-out bills one quota unit for the combined call."
        },
        {
          "method": "GET",
          "path": "/api/brands",
          "summary": "List brands with round-trippable slugs and live inventory counts.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "brand / model",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Name, slug, or brand_id, exactly as on /api/search."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range. Accepted and ignored by /api/years itself, which is the dimension they would narrow."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical group, matched whole."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe. Accepted and ignored by /api/sites and /load-models."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "0 / 5000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range. price_to=5000 on /api/brands returns each brand count under EUR 5,000."
            },
            {
              "name": "vehicle_type / body_style",
              "type": "string",
              "example": "cars",
              "source": "As on /api/search.",
              "description": "Vehicle-type and body-style filters."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Restrict to left- or right-hand-drive vehicles. Slower than an unfiltered facet call, because it is computed live rather than read from the precomputed facet table."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to damaged or broken vehicles."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to Buy-Now inventory."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to lots ending within about two hours."
            },
            {
              "name": "search",
              "type": "string",
              "example": "bm",
              "source": "Free text entered by your user.",
              "description": "Substring filter on brand name."
            },
            {
              "name": "ordering",
              "type": "enum",
              "example": "-count",
              "source": "Choose name, -name, count, or -count.",
              "description": "name, -name, count, or -count. An unrecognised value falls back to name rather than erroring."
            },
            {
              "name": "limit",
              "type": "integer",
              "example": "20",
              "source": "Result count chosen by your application.",
              "description": "Maximum number of values. No default — the full brand list is returned unless you cap it, which is what a filter sidebar wants."
            }
          ],
          "examples": [
            "/api/brands?search=bm&ordering=-count&limit=20",
            "/api/brands?country=DE&damaged=true"
          ],
          "response": "{\n  \"success\": true,\n  \"brands\": [\n    {\n      \"id\": 12,\n      \"name\": \"BMW\",\n      \"slug\": \"bmw\",\n      \"count\": 1543\n    }\n  ]\n}",
          "note": "The slug round-trips: send it back as ?brand=<slug> to /api/search or /api/models. Each brand appears once, keyed on its identity rather than on the spelling a source happened to use, and its count is the whole count for that brand — never a per-spelling fraction of it. The list is safe to render straight into a dropdown without de-duplicating."
        },
        {
          "method": "GET",
          "path": "/api/models",
          "summary": "List models for one brand with live inventory counts.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "brand",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "required": true,
              "description": "Brand name, slug, or brand_id."
            },
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "brand / model",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Name, slug, or brand_id, exactly as on /api/search."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range. Accepted and ignored by /api/years itself, which is the dimension they would narrow."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical group, matched whole."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe. Accepted and ignored by /api/sites and /load-models."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "0 / 5000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range. price_to=5000 on /api/brands returns each brand count under EUR 5,000."
            },
            {
              "name": "vehicle_type / body_style",
              "type": "string",
              "example": "cars",
              "source": "As on /api/search.",
              "description": "Vehicle-type and body-style filters."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Restrict to left- or right-hand-drive vehicles. Slower than an unfiltered facet call, because it is computed live rather than read from the precomputed facet table."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to damaged or broken vehicles."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to Buy-Now inventory."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to lots ending within about two hours."
            },
            {
              "name": "search",
              "type": "string",
              "example": "x",
              "source": "Free text entered by your user.",
              "description": "Substring filter on model name."
            },
            {
              "name": "ordering",
              "type": "enum",
              "example": "-count",
              "source": "Choose name, -name, count, or -count.",
              "description": "name, -name, count, or -count. An unrecognised value falls back to name rather than erroring."
            },
            {
              "name": "limit",
              "type": "integer",
              "example": "20",
              "source": "Result count chosen by your application.",
              "description": "Maximum number of values. No default — the full model list for the brand is returned unless you cap it."
            }
          ],
          "examples": [
            "/api/models?brand=bmw&ordering=-count",
            "/api/models?brand=12&search=x&country=DE"
          ],
          "response": "{\n  \"success\": true,\n  \"models\": [\n    {\n      \"name\": \"320d\",\n      \"slug\": \"320d\",\n      \"count\": 210\n    }\n  ]\n}",
          "note": "brand is required — calling without it returns 400. Each model appears once, merged on the model identity rather than its printed label, so a model spelled \"GOLF\" by one source and \"Golf\" by another is one row carrying the whole count."
        },
        {
          "method": "GET",
          "path": "/api/years",
          "summary": "List registration years available in current inventory.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "brand / model",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Name, slug, or brand_id, exactly as on /api/search."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range. Accepted and ignored by /api/years itself, which is the dimension they would narrow."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical group, matched whole."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe. Accepted and ignored by /api/sites and /load-models."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "0 / 5000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range. price_to=5000 on /api/brands returns each brand count under EUR 5,000."
            },
            {
              "name": "vehicle_type / body_style",
              "type": "string",
              "example": "cars",
              "source": "As on /api/search.",
              "description": "Vehicle-type and body-style filters."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Restrict to left- or right-hand-drive vehicles. Slower than an unfiltered facet call, because it is computed live rather than read from the precomputed facet table."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to damaged or broken vehicles."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to Buy-Now inventory."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to lots ending within about two hours."
            }
          ],
          "examples": [
            "/api/years?country=DE",
            "/api/years?damaged=true&buy_now=true"
          ],
          "response": "{\n  \"success\": true,\n  \"years\": [\n    2024,\n    2023,\n    2022,\n    2021\n  ],\n  \"year_counts\": [\n    {\n      \"value\": 2024,\n      \"count\": 512\n    },\n    {\n      \"value\": 2023,\n      \"count\": 4127\n    },\n    {\n      \"value\": 2022,\n      \"count\": 3890\n    },\n    {\n      \"value\": 2021,\n      \"count\": 3544\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "years",
              "type": "integer[]",
              "description": "Registration years present in the filtered inventory, newest first."
            },
            {
              "name": "year_counts",
              "type": "{value, count}[]",
              "description": "Mirrors years in the same order. Each count is the offers for that registration year under the request other filters."
            }
          ]
        },
        {
          "method": "GET",
          "path": "/api/fuels",
          "summary": "List canonical fuel groups available in current inventory.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "brand / model",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Name, slug, or brand_id, exactly as on /api/search."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range. Accepted and ignored by /api/years itself, which is the dimension they would narrow."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical group, matched whole."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe. Accepted and ignored by /api/sites and /load-models."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "0 / 5000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range. price_to=5000 on /api/brands returns each brand count under EUR 5,000."
            },
            {
              "name": "vehicle_type / body_style",
              "type": "string",
              "example": "cars",
              "source": "As on /api/search.",
              "description": "Vehicle-type and body-style filters."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Restrict to left- or right-hand-drive vehicles. Slower than an unfiltered facet call, because it is computed live rather than read from the precomputed facet table."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to damaged or broken vehicles."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to Buy-Now inventory."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to lots ending within about two hours."
            }
          ],
          "examples": [
            "/api/fuels?country=DE",
            "/api/fuels?buy_now=true"
          ],
          "response": "{\n  \"success\": true,\n  \"fuels\": [\n    \"Diesel\",\n    \"Electric\",\n    \"Hybrid\",\n    \"Mild Hybrid\",\n    \"Petrol\",\n    \"Plug-in Hybrid\"\n  ],\n  \"fuel_counts\": [\n    {\n      \"value\": \"Diesel\",\n      \"count\": 18342\n    },\n    {\n      \"value\": \"Electric\",\n      \"count\": 903\n    },\n    {\n      \"value\": \"Hybrid\",\n      \"count\": 2211\n    },\n    {\n      \"value\": \"Mild Hybrid\",\n      \"count\": 5107\n    },\n    {\n      \"value\": \"Petrol\",\n      \"count\": 24980\n    },\n    {\n      \"value\": \"Plug-in Hybrid\",\n      \"count\": 1489\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "fuels",
              "type": "string[]",
              "description": "Canonical fuel groups present in the filtered inventory, alphabetical."
            },
            {
              "name": "fuel_counts",
              "type": "{value, count}[]",
              "description": "Mirrors fuels in the same order."
            }
          ],
          "note": "Six groups, and the three hybrid kinds are distinct: Hybrid (full hybrid), Mild Hybrid (48V assist, cannot drive on the motor alone) and Plug-in Hybrid. The fuel filter matches a group whole, so fuel=Hybrid returns full hybrids only. To catch every electrified car, send all three."
        },
        {
          "method": "GET",
          "path": "/api/gearboxes",
          "summary": "List canonical gearbox groups available in current inventory.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "brand / model",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Name, slug, or brand_id, exactly as on /api/search."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range. Accepted and ignored by /api/years itself, which is the dimension they would narrow."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical group, matched whole."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe. Accepted and ignored by /api/sites and /load-models."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "0 / 5000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range. price_to=5000 on /api/brands returns each brand count under EUR 5,000."
            },
            {
              "name": "vehicle_type / body_style",
              "type": "string",
              "example": "cars",
              "source": "As on /api/search.",
              "description": "Vehicle-type and body-style filters."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Restrict to left- or right-hand-drive vehicles. Slower than an unfiltered facet call, because it is computed live rather than read from the precomputed facet table."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to damaged or broken vehicles."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to Buy-Now inventory."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to lots ending within about two hours."
            }
          ],
          "examples": [
            "/api/gearboxes?country=KR",
            "/api/gearboxes?damaged=true"
          ],
          "response": "{\n  \"success\": true,\n  \"gearboxes\": [\n    \"Automatic\",\n    \"Manual\"\n  ],\n  \"gearbox_counts\": [\n    {\n      \"value\": \"Automatic\",\n      \"count\": 22190\n    },\n    {\n      \"value\": \"Manual\",\n      \"count\": 15432\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "gearboxes",
              "type": "string[]",
              "description": "Canonical gearbox groups present in the filtered inventory."
            },
            {
              "name": "gearbox_counts",
              "type": "{value, count}[]",
              "description": "Mirrors gearboxes in the same order."
            }
          ]
        },
        {
          "method": "GET",
          "path": "/api/countries",
          "summary": "List vehicle-location countries and display names.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "brand / model",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Name, slug, or brand_id, exactly as on /api/search."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range. Accepted and ignored by /api/years itself, which is the dimension they would narrow."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical group, matched whole."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe. Accepted and ignored by /api/sites and /load-models."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "0 / 5000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range. price_to=5000 on /api/brands returns each brand count under EUR 5,000."
            },
            {
              "name": "vehicle_type / body_style",
              "type": "string",
              "example": "cars",
              "source": "As on /api/search.",
              "description": "Vehicle-type and body-style filters."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Restrict to left- or right-hand-drive vehicles. Slower than an unfiltered facet call, because it is computed live rather than read from the precomputed facet table."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to damaged or broken vehicles."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to Buy-Now inventory."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to lots ending within about two hours."
            }
          ],
          "examples": [
            "/api/countries?buy_now=true",
            "/api/countries?damaged=true"
          ],
          "response": "{\n  \"success\": true,\n  \"countries\": [\n    \"DE\",\n    \"JP\",\n    \"KR\",\n    \"NL\"\n  ],\n  \"country_details\": [\n    {\n      \"code\": \"DE\",\n      \"name\": \"Germany\"\n    },\n    {\n      \"code\": \"JP\",\n      \"name\": \"Japan\"\n    },\n    {\n      \"code\": \"KR\",\n      \"name\": \"South Korea\"\n    },\n    {\n      \"code\": \"NL\",\n      \"name\": \"Netherlands\"\n    }\n  ],\n  \"country_counts\": [\n    {\n      \"value\": \"DE\",\n      \"count\": 9871\n    },\n    {\n      \"value\": \"JP\",\n      \"count\": 14203\n    },\n    {\n      \"value\": \"KR\",\n      \"count\": 8455\n    },\n    {\n      \"value\": \"NL\",\n      \"count\": 3120\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "countries",
              "type": "string[]",
              "description": "ISO country codes present in the filtered inventory."
            },
            {
              "name": "country_details",
              "type": "{code, name}[]",
              "description": "The same codes with display names, for rendering a picker."
            },
            {
              "name": "country_counts",
              "type": "{value, count}[]",
              "description": "Keyed by canonical ISO code (UK folds into GB), in the same order as countries."
            }
          ],
          "note": "country=europe means \"every origin that is not overseas\" and is implemented as an exclusion, not a list: it currently excludes KR (Encar) and JP (the Japanese auction houses), and rows with no recorded country are treated as European and included. If you need strict membership, pass explicit ISO codes instead."
        },
        {
          "method": "GET",
          "path": "/api/sites",
          "summary": "List auction source slugs with live inventory counts.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "brand / model",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Name, slug, or brand_id, exactly as on /api/search."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range. Accepted and ignored by /api/years itself, which is the dimension they would narrow."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical group, matched whole."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe. Accepted and ignored by /api/sites and /load-models."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "0 / 5000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range. price_to=5000 on /api/brands returns each brand count under EUR 5,000."
            },
            {
              "name": "vehicle_type / body_style",
              "type": "string",
              "example": "cars",
              "source": "As on /api/search.",
              "description": "Vehicle-type and body-style filters."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Restrict to left- or right-hand-drive vehicles. Slower than an unfiltered facet call, because it is computed live rather than read from the precomputed facet table."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to damaged or broken vehicles."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to Buy-Now inventory."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to lots ending within about two hours."
            }
          ],
          "examples": [
            "/api/sites?buy_now=true",
            "/api/sites?damaged=true"
          ],
          "response": "{\n  \"success\": true,\n  \"sites\": [\n    {\n      \"id\": 1,\n      \"name\": \"encar\",\n      \"count\": 9021\n    },\n    {\n      \"id\": 2,\n      \"name\": \"openlane\",\n      \"count\": 4110\n    },\n    {\n      \"id\": 4,\n      \"name\": \"japan\",\n      \"count\": 1642\n    }\n  ]\n}",
          "note": "This is the discovery endpoint for both /api/search?site= and /api/auction/{site}/{id}. Counts here respect every other filter on the request but ignore site / site_exclude themselves — a facet that filtered by its own dimension could only ever return the sources you already named. The European classifieds network is retail data, not auction inventory, so it never appears here; it has its own endpoint group."
        },
        {
          "method": "GET",
          "path": "/load-models",
          "summary": "Return the complete model catalog grouped by brand.",
          "scope": "search",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2019 / 2024",
              "source": "/api/production-years",
              "description": "Production-year bounds. Falls back to registration year when the source provides no production year."
            },
            {
              "name": "type",
              "type": "string",
              "example": "Touring",
              "source": "/api/types?brand=bmw&model=320d",
              "description": "Trim/type filter, distinct from vehicle_type and body_style. Accepts multiple comma-separated values; unclassified trims are retained."
            },
            {
              "name": "brand / model",
              "type": "string | integer",
              "example": "bmw",
              "source": "/api/brands -> brands[].slug (or id/name)",
              "description": "Name, slug, or brand_id, exactly as on /api/search."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "/api/years -> years[]",
              "description": "Registration-year range. Accepted and ignored by /api/years itself, which is the dimension they would narrow."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical group, matched whole."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe. Accepted and ignored by /api/sites and /load-models."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "0 / 5000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range. price_to=5000 on /api/brands returns each brand count under EUR 5,000."
            },
            {
              "name": "vehicle_type / body_style",
              "type": "string",
              "example": "cars",
              "source": "As on /api/search.",
              "description": "Vehicle-type and body-style filters."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Restrict to left- or right-hand-drive vehicles. Slower than an unfiltered facet call, because it is computed live rather than read from the precomputed facet table."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to damaged or broken vehicles."
            },
            {
              "name": "buy_now",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to Buy-Now inventory."
            },
            {
              "name": "ending_soon",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Restrict to lots ending within about two hours."
            }
          ],
          "examples": [
            "/load-models?country=DE",
            "/load-models?buy_now=true&damaged=false"
          ],
          "response": "{\n  \"BMW\": [\n    {\n      \"text\": \"320d\"\n    },\n    {\n      \"text\": \"X5\"\n    }\n  ],\n  \"Volvo\": [\n    {\n      \"text\": \"XC60\"\n    }\n  ]\n}",
          "note": "This response is shaped differently from every other endpoint in the API: the body IS the map. Every key is a brand name and every value is that brand list of models — there is no wrapper object, no success flag and no contract metadata, because the keys are data and nothing else may share that namespace. Read the correlation id from the X-Request-ID header instead. An error response IS wrapped and does carry success: false, so test for the error shape rather than for a success flag that never appears. If you write a generic client wrapper that asserts body.success, special-case this one route. Model entries are objects, not bare strings, and each list is sorted by name. Heavy — cache it, and prefer /api/models for interactive brand-specific dropdowns."
        }
      ]
    },
    {
      "id": "catalog",
      "label": "Catalog",
      "description": "Build slug-addressable manufacturer and model-group navigation. All catalog list routes paginate with a default limit of 50 and are capped at offset 5000 — a deeper page is a 400 naming the limit. They carry page, page_size and max_page.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/catalog/manufacturers",
          "summary": "Paginated manufacturer catalog with inventory counts.",
          "scope": "catalog",
          "params": [
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "Restrict to a country or europe."
            },
            {
              "name": "limit / offset / page / page_size",
              "type": "integer",
              "example": "20 / 0",
              "source": "Pagination state in your application.",
              "description": "Standard pagination. Default limit 50."
            }
          ],
          "examples": [
            "/api/catalog/manufacturers?country=DE&limit=20",
            "/api/catalog/manufacturers?page=2&page_size=25"
          ],
          "response": "{\n  \"success\": true,\n  \"results\": [\n    {\n      \"slug\": \"bmw\",\n      \"name\": \"BMW\",\n      \"inventory_count\": 1543,\n      \"brand_id\": 12\n    }\n  ],\n  \"total\": 96,\n  \"limit\": 50,\n  \"offset\": 0,\n  \"total_pages\": 2,\n  \"max_page\": 2\n}"
        },
        {
          "method": "GET",
          "path": "/api/catalog/manufacturers/{slug}",
          "summary": "Resolve one manufacturer by slug.",
          "scope": "catalog",
          "params": [
            {
              "name": "slug",
              "type": "string",
              "location": "path",
              "example": "bmw",
              "source": "/api/catalog/manufacturers -> results[].slug",
              "required": true,
              "description": "Manufacturer slug."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "Optional country restriction."
            }
          ],
          "examples": [
            "/api/catalog/manufacturers/bmw",
            "/api/catalog/manufacturers/bmw?country=DE"
          ],
          "response": "{\n  \"success\": true,\n  \"manufacturer\": {\n    \"slug\": \"bmw\",\n    \"name\": \"BMW\",\n    \"inventory_count\": 1543,\n    \"brand_id\": 12\n  }\n}",
          "note": "Returns 404 when the slug is unknown."
        },
        {
          "method": "GET",
          "path": "/api/catalog/manufacturers/stats",
          "summary": "Return aggregate manufacturer statistics.",
          "scope": "catalog",
          "params": [
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "Optional country restriction."
            }
          ],
          "examples": [
            "/api/catalog/manufacturers/stats",
            "/api/catalog/manufacturers/stats?country=DE"
          ],
          "response": "{\n  \"success\": true\n}",
          "note": "Aggregate manufacturer statistics. Optional country filter. The reference does not pin a field list for this body — treat extra keys as operational, not contract."
        },
        {
          "method": "GET",
          "path": "/api/catalog/model-groups",
          "summary": "Paginated model groups for one manufacturer.",
          "scope": "catalog",
          "params": [
            {
              "name": "manufacturer__slug",
              "type": "string",
              "example": "bmw",
              "source": "/api/catalog/manufacturers -> results[].slug",
              "required": true,
              "description": "Parent manufacturer slug."
            },
            {
              "name": "search",
              "type": "string",
              "example": "320",
              "source": "Free text entered by your user.",
              "description": "Substring filter on model name."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "Optional country restriction."
            },
            {
              "name": "limit / offset / page / page_size",
              "type": "integer",
              "example": "20 / 0",
              "source": "Pagination state in your application.",
              "description": "Standard pagination. Default limit 50."
            }
          ],
          "examples": [
            "/api/catalog/model-groups?manufacturer__slug=bmw&search=320",
            "/api/catalog/model-groups?manufacturer__slug=bmw&country=DE&page=2&page_size=20"
          ],
          "response": "{\n  \"success\": true,\n  \"results\": [\n    {\n      \"slug\": \"320d\",\n      \"name\": \"320d\",\n      \"inventory_count\": 210,\n      \"manufacturer_slug\": \"bmw\"\n    }\n  ],\n  \"total\": 34,\n  \"limit\": 50,\n  \"offset\": 0,\n  \"total_pages\": 1,\n  \"max_page\": 1\n}"
        },
        {
          "method": "GET",
          "path": "/api/catalog/model-groups/{slug}",
          "summary": "Resolve one model group by slug.",
          "scope": "catalog",
          "params": [
            {
              "name": "slug",
              "type": "string",
              "location": "path",
              "example": "320d",
              "source": "/api/catalog/model-groups -> results[].slug",
              "required": true,
              "description": "Model-group slug."
            },
            {
              "name": "manufacturer__slug",
              "type": "string",
              "example": "bmw",
              "source": "/api/catalog/manufacturers -> results[].slug",
              "description": "Manufacturer slug for disambiguation."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "Optional country restriction."
            }
          ],
          "examples": [
            "/api/catalog/model-groups/320d?manufacturer__slug=bmw",
            "/api/catalog/model-groups/320d?manufacturer__slug=bmw&country=DE"
          ],
          "response": "{\n  \"success\": true,\n  \"model_group\": {\n    \"slug\": \"320d\",\n    \"name\": \"320d\",\n    \"inventory_count\": 210,\n    \"manufacturer_slug\": \"bmw\"\n  }\n}"
        }
      ]
    },
    {
      "id": "seo",
      "label": "SEO helpers",
      "description": "Build popular landing pages and resolve brand/model URL slugs.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/seo/popular-searches",
          "summary": "List the most common live brand/model searches.",
          "scope": "seo",
          "params": [
            {
              "name": "limit",
              "type": "integer",
              "example": "50",
              "source": "Result count chosen by your application.",
              "description": "Number of results from 1 to 200. Default 20."
            }
          ],
          "examples": [
            "/api/seo/popular-searches",
            "/api/seo/popular-searches?limit=50"
          ],
          "response": "{\n  \"success\": true,\n  \"results\": [\n    {\n      \"brand\": \"BMW\",\n      \"model\": \"320d\",\n      \"brand_slug\": \"bmw\",\n      \"model_slug\": \"320d\",\n      \"count\": 210\n    }\n  ]\n}"
        },
        {
          "method": "GET",
          "path": "/api/seo/brand-model-from-slug",
          "summary": "Resolve brand and model slugs to their canonical display names.",
          "scope": "seo",
          "params": [
            {
              "name": "brand_slug",
              "type": "string",
              "example": "bmw",
              "source": "/api/seo/popular-searches -> results[].brand_slug",
              "required": true,
              "description": "Brand slug, such as bmw."
            },
            {
              "name": "model_slug",
              "type": "string",
              "example": "320d",
              "source": "/api/seo/popular-searches -> results[].model_slug",
              "required": true,
              "description": "Model slug, such as 320d."
            }
          ],
          "examples": [
            "/api/seo/brand-model-from-slug?brand_slug=bmw&model_slug=320d",
            "/api/seo/brand-model-from-slug?brand_slug=volvo&model_slug=xc60"
          ],
          "response": "{\n  \"success\": true,\n  \"brand\": \"BMW\",\n  \"model\": \"320d\"\n}",
          "note": "Returns 404 unless the slug pair identifies exactly one live brand/model combination."
        }
      ]
    },
    {
      "id": "auctions",
      "label": "Auctions & history",
      "description": "Retrieve source-aware listing detail, galleries, price changes, and VIN history. A running openlane auction has its bid read from the auction house at the moment you request the detail — nothing needs to be passed to enable it. Every other listing is served at its cycle price, refreshed by the scheduled ingestion pipeline.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/auction/{site_slug}/{auction_id}",
          "summary": "Canonical auction detail with private and bidder fields removed.",
          "scope": "auctions",
          "params": [
            {
              "name": "include",
              "type": "string",
              "example": "price_history",
              "source": "Listing values or your application settings.",
              "description": "Comma-separated extras folded into this response, saving a request\neach. `price_history` adds a top-level `price_history` array, exactly\nthe `history` of `/api/auction/{site_slug}/{auction_id}/price-history`\n(the key must be allowed that endpoint, else `403`). With it,\n`max-age` is at most 60. Unknown values are a `400`. Omit it and the\nresponse is unchanged.\n"
            },
            {
              "name": "site_slug",
              "type": "string",
              "location": "path",
              "example": "encar",
              "source": "/api/search -> results[].site_name",
              "required": true,
              "description": "Source name returned by /api/sites."
            },
            {
              "name": "auction_id",
              "type": "integer",
              "location": "path",
              "example": "38112900",
              "source": "/api/search -> results[].auction_id",
              "required": true,
              "description": "Integer auction database id."
            }
          ],
          "examples": [
            "/api/auction/encar/38112900",
            "/api/auction/openlane/11125938"
          ],
          "response": "{\n  \"success\": true,\n  \"auction\": {\n    \"auction_id\": 11409652,\n    \"site_name\": \"openlane\",\n    \"clean_make\": \"BMW\",\n    \"clean_model\": \"320d\",\n    \"model_display\": \"320d M Sport\",\n    \"registration_year\": 2020,\n    \"mileage\": 45000,\n    \"current_price\": 25900,\n    \"current_final\": 33566,\n    \"public_price_eur\": 33926,\n    \"auction_end_at\": \"2026-08-20T10:00:00Z\",\n    \"images\": [],\n    \"vault_gallery\": {\n      \"images\": [\n        {\n          \"served_url\": \"/image-vault/ab/cd/openlane_11409652_00_deadbeef.avif\",\n          \"remote_url\": \"https://cdn.example/photo_1.jpg\",\n          \"image_status\": \"ready\"\n        }\n      ],\n      \"count\": 28,\n      \"pending\": 0\n    },\n    \"vehicle_details\": {},\n    \"car_identification\": {},\n    \"live_price\": {\n      \"price\": 25900,\n      \"currency\": \"EUR\",\n      \"source\": \"openlane\",\n      \"fetched_at\": 1786659750\n    }\n  }\n}",
          "responseFields": [
            {
              "name": "vault_gallery",
              "type": "object",
              "description": "The image vault’s authoritative photo set, embedded verbatim: the exact body of /api/auction-images/{site}/{id} ({ images, count, pending }). Read it and a detail page is one request instead of two. Both routes build it from one cache entry, so they can never disagree; while pending is non-zero this response is served with max-age=10 so a polling client sees photos as they land. A client that ignores it behaves exactly as before."
            },
            {
              "name": "vehicle_details",
              "type": "object",
              "description": "Cross-source normalized condition and paperwork: documents, inspection_reports, option_reports, car_reports, service_history, technical_inspection, paperwork, condition, damages, equipment, warranty, specs, location, seller, profile. Every key is omitted when the source has nothing for it, and the whole block is omitted for a listing whose details have not been fetched yet (details_pending: true)."
            },
            {
              "name": "car_identification",
              "type": "object",
              "description": "Raw source-specific spec map. Prefer vehicle_details where it covers what you need — its keys are stable across sources, these are not."
            },
            {
              "name": "images / gallery_images",
              "type": "object[]",
              "description": "What the source record itself carried — untouched by this release. For the vault’s photo set with served URLs and dimensions, read vault_gallery above."
            },
            {
              "name": "live_price",
              "type": "object",
              "description": "Present when a live bid overlay is available, including a recent cached bid within the TTL: { price, currency, source, fetched_at }. price is the raw bid and equals current_price; fetched_at is a Unix timestamp in seconds — use it, not your own clock, to render “as of”. Its absence is not an error: it means the car is served at its cycle price, which for most listings is the only price there is."
            },
            {
              "name": "live_price_pending",
              "type": "boolean",
              "description": "True when the refreshed figure was not to hand in time for this response — your request is never held waiting on the auction house. Read the car once more after ~2s and then stop; the second read is served from cache. These responses carry Cache-Control: no-store and X-Live-Price: pending. This is the only case worth reading again for, and the only case that sets the flag."
            },
            {
              "name": "current_price / current_final / current_tax / current_tax_delivery / public_price_eur",
              "type": "number",
              "description": "JSON numbers, always. Recomputed from the bid when a live price happened, so a client that ignores live_price entirely still shows the right number. buy_now_price and buynow_final are never touched — a buy-now figure does not move with bidding."
            },
            {
              "name": "auction_id_str",
              "type": "string",
              "description": "The auction id as a string — the one to persist and to address the car with. japan ids exceed 2^53, so JavaScript and any other client parsing JSON numbers as IEEE-754 doubles silently rounds the numeric auction_id and the result no longer addresses the row it came from. auction_id is unchanged and stays a number for existing consumers."
            },
            {
              "name": "vehicle_id / manufacturer_slug / model_group_slug / fuel_type",
              "type": "integer | string",
              "description": "The same four catalog-shaped keys /api/listVehicles adds. vehicle_id is a duplicate of auction_id (same 2^53 caveat — prefer auction_id_str). manufacturer_slug and model_group_slug are the values the catalog routes are addressed by, so a detail response is enough to build a link to its manufacturer and model-group pages without a second lookup. fuel_type is present only when the row carries a fuel."
            },
            {
              "name": "is_blind",
              "type": "boolean | null",
              "description": "The source’s blind-auction flag, where it publishes one. A blind auction does not show a live bid, so current_price can be null while the lot is open. null means unknown, not false."
            },
            {
              "name": "details_pending",
              "type": "boolean",
              "description": "True while the full detail payload has not been fetched from the source yet. vehicle_details is omitted meanwhile and the response carries a short max-age so a polling client sees the details land. Not an error."
            },
            {
              "name": "auction_end_at",
              "type": "timestamp | null",
              "description": "Absolute UTC instant the lot closes, or null for stock with no deadline. Drive countdowns from this — not from batch_end_date, whose timezone differs per auction house, nor auction_sec_left, which is a snapshot taken at the last refresh rather than seconds from now."
            },
            {
              "name": "estimated_value_eur",
              "type": "number | null",
              "description": "Optional auction-house valuation in EUR where available. Treat it as an estimate, never a price you can pay or a settled result. Do not substitute it for current_price."
            },
            {
              "name": "steering",
              "type": "enum",
              "description": "left or right, never null. japan is the only source that publishes a steering field; for every other source the side is read off the car registration market — a car registered in a country that drives on the left (GB, IE, JP, CY, MT, AU, NZ, ZA, IN, TH, HK, SG) is right, everything else left. Also present on every search result card and on /api/car-details."
            },
            {
              "name": "co2",
              "type": "integer | null",
              "description": "CO2 in g/km as the auction house measured and published it. Null on roughly half of lots, which publish none. It carries no standard tag and mixes both EU test cycles by registration year — see the CO2 & emissions page."
            },
            {
              "name": "co2_estimated",
              "type": "integer | null",
              "description": "A derived CO2 figure for lots that publish none, inferred from other lots of the same specification. NEVER a measured value, and null wherever the specification does not decide it. It is a separate field from co2 on purpose, so an estimate can never be mistaken for a measurement."
            },
            {
              "name": "co2_estimated_standard",
              "type": "enum | null",
              "description": "NEDC or WLTP — which EU test cycle co2_estimated is expressed on. Always read the estimate together with this: the same physical car reads 12-25% higher under WLTP, so a figure without its standard is not comparable."
            },
            {
              "name": "vin_ocr",
              "type": "object | null",
              "description": "Auto1 only, and the reason an Auto1 car has a VIN at all: Auto1 publishes no chassis number but photographs the registration certificate, so the VIN is read off that paperwork. { vin, confidence, evidence } — confidence is high, stored or medium, and evidence identifies the document photo it was read from. A high or stored read also fills chassis_number when the row has none, so a client that only reads chassis_number needs no change. A medium read is labelled and never promoted to the car identity: show it as unconfirmed. Roughly 41% of Auto1 cars with a document photo end up with a published VIN — the ceiling is upstream, because Auto1 blurs the VIN field on ES, IT and PT certificates. Part of the vehicle_identity field grant."
            },
            {
              "name": "vin_ocr_pending",
              "type": "boolean",
              "description": "True when a VIN read was queued by this request and was not finished in time to answer it — your request is never held waiting on OCR. Read the car once more after a second or two and then stop; the answer is cached for a day once found. Behaves exactly like live_price_pending, and like it, is never an error."
            },
            {
              "name": "source_comparison",
              "type": "object | null",
              "description": "Korean sources only. The same car is often listed on both Encar and KB ChaChaCha; listings are grouped on a shared VIN, or a corroborated plate with make and model, and one is published while the rest are hidden. This block is the evidence: offers (every listing for the car, each with site, auction_id, price_eur, offer_link and eligible), selected (the one published), spread_eur (highest minus lowest eligible price — what the same car costs on the cheaper source), image_source (the listing the gallery came from; photo sets from two listings are never interleaved) and match_method (vin or plate_make_model). Requires the korea_merge field grant."
            },
            {
              "name": "canonical_offer",
              "type": "object | null",
              "description": "Present on a Korean listing that was resolved as a duplicate: { site, auction_id } for the representative published in its place. Also behind the korea_merge field grant."
            }
          ],
          "note": "Live prices: for a running openlane auction the bid is read from the source while this request is served, the price fields are rewritten and a live_price block is attached. Nothing enables it and there is no separate real-time endpoint. Every other listing is served at its cycle price from the latest successful source refresh, and for a car that is not taking bids that is simply its price. A refreshed price is reused for ttl_seconds (default 120) and Cache-Control: max-age drops to match, so polling faster gains nothing. Read live_prices from /api/contract rather than hardcoding which sources qualify. This API only observes auction prices — it never places, raises or withdraws a bid. Returns 404 when the source-aware listing does not exist. Auction fees, bid history, transport and every delivery*/selfpickup* key are removed at the response boundary — they are quoted to our buyer account and our delivery address, not yours. Published asking prices, VAT scheme, estimated repair cost and seller location are unaffected; for landed cost use the import calculator, which prices your own route."
        },
        {
          "method": "GET",
          "path": "/api/auction/batch",
          "summary": "Up to 100 auction details in one request.",
          "scope": "auctions",
          "params": [
            {
              "name": "ids",
              "type": "string",
              "example": "openlane:11409652,encar:38112900",
              "source": "site_name and auction_id_str of cars you already hold",
              "required": true,
              "description": "Comma-separated site:auction_id pairs, at most 100. Duplicates are ignored. A malformed pair, or more than 100, is a 400."
            }
          ],
          "examples": [
            "/api/auction/batch?ids=openlane:11409652,encar:38112900",
            "/api/auction/batch?ids=copart:50590868,auto1:10765001,bca:1234567"
          ],
          "response": "{\n  \"success\": true,\n  \"requested\": 2,\n  \"found\": 1,\n  \"auctions\": [\n    {\n      \"auction_id\": 11409652,\n      \"auction_id_str\": \"11409652\",\n      \"site_name\": \"openlane\",\n      \"clean_make\": \"BMW\",\n      \"clean_model\": \"320d\",\n      \"current_price\": 25900,\n      \"public_price_eur\": 33926,\n      \"vault_gallery\": {\n        \"images\": [],\n        \"count\": 28,\n        \"pending\": 0\n      }\n    }\n  ],\n  \"missing\": [\n    {\n      \"id\": \"encar:38112900\",\n      \"reason\": \"not_found\"\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "auctions",
              "type": "object[]",
              "description": "Each entry is exactly the auction object /api/auction/{site}/{id} returns, read from the same cache and including vault_gallery, in the order of ids."
            },
            {
              "name": "missing",
              "type": "{id, reason}[]",
              "description": "Ids that could not be served, with reason not_found or site_not_allowed (the source is not on the key’s allowlist). One bad id never fails the request."
            },
            {
              "name": "requested / found",
              "type": "integer",
              "description": "How many distinct ids were asked for and how many came back."
            }
          ],
          "note": "For a watchlist: 200 known cars are two requests instead of 200, and a batch counts as one request whatever the number of ids. Covered by the same permission as the detail endpoint. Two things the single endpoint does are deliberately not done here, because doing them for 100 cars at once would hit the sources 100 times: no on-demand detail fetch (the car is queued and comes back with details_pending: true), and no live-price refresh or VIN read is started (one already known is still applied). Use /api/auction/{site}/{id} for a car that needs those now."
        },
        {
          "method": "GET",
          "path": "/api/auction/{site_slug}/{auction_id}/price-history",
          "summary": "Every recorded price movement for one listing, oldest first.",
          "scope": "auctions",
          "params": [
            {
              "name": "site_slug",
              "type": "string",
              "location": "path",
              "example": "encar",
              "source": "/api/search -> results[].site_name",
              "required": true,
              "description": "Auction source slug."
            },
            {
              "name": "auction_id",
              "type": "integer",
              "location": "path",
              "example": "38112900",
              "source": "/api/search -> results[].auction_id",
              "required": true,
              "description": "Auction database id."
            }
          ],
          "examples": [
            "/api/auction/encar/38112900/price-history",
            "/api/auction/openlane/11125938/price-history"
          ],
          "response": "{\n  \"success\": true,\n  \"site\": \"encar\",\n  \"auction_id\": 38112900,\n  \"source_auction_id\": \"38112900\",\n  \"history\": [\n    {\n      \"event_type\": \"initial\",\n      \"source_auction_id\": \"38112900\",\n      \"changed_fields\": [\n        \"current_price\",\n        \"public_price_eur\"\n      ],\n      \"current_price\": 21500,\n      \"buy_now_price\": null,\n      \"start_price\": null,\n      \"final_price\": null,\n      \"current_final\": null,\n      \"buynow_final\": null,\n      \"public_price_eur\": 21500,\n      \"currency_code_id\": \"EUR\",\n      \"observed_at\": \"2026-07-10T08:00:00\",\n      \"created_at\": \"2026-07-10T08:00:05\"\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "event_type",
              "type": "enum",
              "description": "Exactly three values: initial (the first prices we ever recorded for this lot), baseline (the state before the first observed change, written once so a series never starts mid-move), and change (a subsequent movement). There is no price_change value — a client matching on that has been discarding every event."
            },
            {
              "name": "changed_fields",
              "type": "string[]",
              "description": "Which price fields moved in this event. Always an array, empty on a baseline row."
            },
            {
              "name": "current_price / buy_now_price / start_price / final_price / current_final / buynow_final / public_price_eur",
              "type": "number | null",
              "description": "The full price snapshot as of observed_at. JSON numbers or null."
            },
            {
              "name": "currency_code_id",
              "type": "string",
              "description": "Currency of the source-side figures."
            },
            {
              "name": "observed_at",
              "type": "timestamp",
              "description": "When the change was observed. The field to plot against."
            },
            {
              "name": "created_at",
              "type": "timestamp",
              "description": "When the row was written. Within seconds of observed_at; not a second data point."
            }
          ],
          "note": "A row is written only when a price actually changed, so the series is the movement itself rather than a per-cycle sample — a lot whose price never moved has exactly one row. Capped at 5,000 events, oldest first. For auto1, source_auction_id is the alphanumeric batch id; for every other source it is the numeric auction id as text. The top-level source_auction_id mirrors the newest event. An unknown auction is not a 404: it answers 200 with history: [] and source_auction_id echoing the id you asked for, because \"we hold no price movements for this lot\" and \"this lot does not exist\" are the same absence here. Check /api/auction/{site}/{id} if you need to tell them apart. Served with Cache-Control: public, max-age=60. This response carries no envelope metadata — read X-Request-ID from the header."
        },
        {
          "method": "GET",
          "path": "/api/auction-images/{site_slug}/{auction_id}",
          "summary": "Ordered gallery metadata backed by the image vault. Usually unnecessary — the same body rides on the auction detail response as vault_gallery.",
          "scope": "auctions",
          "params": [
            {
              "name": "site_slug",
              "type": "string",
              "location": "path",
              "example": "encar",
              "source": "/api/search -> results[].site_name",
              "required": true,
              "description": "Auction source slug."
            },
            {
              "name": "auction_id",
              "type": "integer",
              "location": "path",
              "example": "38112900",
              "source": "/api/search -> results[].auction_id",
              "required": true,
              "description": "Auction database id."
            }
          ],
          "examples": [
            "/api/auction-images/encar/38112900",
            "/api/auction-images/openlane/11125938"
          ],
          "response": "{\n  \"success\": true,\n  \"count\": 1,\n  \"pending\": 12,\n  \"images\": [\n    {\n      \"url\": \"https://cdn.example/photo_1.jpg\",\n      \"remote_url\": \"https://cdn.example/photo_1.jpg\",\n      \"served_url\": \"/image-vault/ab/cd/openlane_11125938_00_deadbeef.avif\",\n      \"thumbnail\": \"/image-vault/ab/cd/openlane_11125938_00_deadbeef.avif\",\n      \"image_status\": \"ready\",\n      \"image_source\": \"downloaded\",\n      \"index\": 0,\n      \"is_primary\": true,\n      \"source_section\": \"exterior\",\n      \"width\": 1024,\n      \"height\": 768\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "count",
              "type": "integer",
              "description": "How many photos this response carries."
            },
            {
              "name": "pending",
              "type": "integer",
              "description": "How many photos the auction still owes. Show placeholders for these instead of a silently short gallery."
            },
            {
              "name": "url",
              "type": "string",
              "description": "The best URL available right now — served_url once vaulted, remote_url until then."
            },
            {
              "name": "served_url",
              "type": "string",
              "description": "A path, not an absolute URL. It begins /image-vault/… and is relative to the API base, so join it: https://api.thecarapi.com + served_url. Load this directly once the photo is stored."
            },
            {
              "name": "remote_url",
              "type": "string",
              "description": "Always absolute — the source’s own URL. Load this through an image proxy until the photo is vaulted."
            },
            {
              "name": "image_status",
              "type": "enum",
              "description": "ready, pending, downloading, failed, blocked, or not_downloaded — but on this endpoint it is always ready, because only stored photos are listed at all. The other values appear on the image annotations attached to search cards, where a photo can still be in flight. failed and blocked mean the photo exhausted its retries and is not counted in pending, so a gallery can settle with pending: 0 and still be shorter than picture_count."
            },
            {
              "name": "image_source",
              "type": "enum",
              "description": "downloaded (in the vault) or proxied (still served from the source)."
            },
            {
              "name": "index / is_primary",
              "type": "integer / boolean",
              "description": "Gallery order and which photo is the thumbnail."
            },
            {
              "name": "source_section",
              "type": "string",
              "description": "Section the source filed the photo under, e.g. exterior, interior, damage."
            },
            {
              "name": "width / height",
              "type": "integer",
              "description": "Pixel dimensions of the stored photo."
            },
            {
              "name": "servedUrl / thumbnailUrl / image_index",
              "type": "string | integer",
              "description": "camelCase and legacy twins of served_url, thumbnail and index, kept for older clients and always equal to their counterparts. Prefer the snake_case names — the twins may be retired."
            },
            {
              "name": "local_status / local_rel_path",
              "type": "string",
              "description": "local_status is the same value as image_status; local_rel_path is the vault path served_url is built from."
            }
          ],
          "note": "You usually do not need this call: /api/auction/{site}/{id} embeds this same body as vault_gallery, so a detail page can be one request rather than two. This endpoint stays fully supported and is still the right one when you want the gallery on its own; both are served from a single cache entry, so they never disagree, and while pending is non-zero both carry max-age=10. Every entry carries a URL that works right now: served_url once the photo is stored on our CDN, remote_url until then. served_url and thumbnail are paths beginning /image-vault/… — join them to https://api.thecarapi.com. remote_url is always absolute. Load served_url directly and remote_url through an image proxy. /image-vault/* paths answer with a 302 to a pre-signed object URL rather than streaming bytes, so use a client that follows redirects — every mainstream one does by default, and the target object is immutable-cached. Stored photos are AVIF or WebP depending on what the source served — rely on the URL and content type, not the extension. An empty images array means nothing is stored yet; an invalid site returns 404."
        },
        {
          "method": "GET",
          "path": "/api/vin/{vin}/history",
          "summary": "Look up a full VIN across current and archived auction records.",
          "scope": "auctions",
          "params": [
            {
              "name": "vin",
              "type": "string",
              "location": "path",
              "example": "WBA8E9G50GNU12345",
              "source": "/api/auction/{site}/{id} -> auction.chassis_number",
              "required": true,
              "description": "Valid 17-character VIN. Matching ignores punctuation and case in the stored VIN, so a source that wrote it with separators still matches."
            }
          ],
          "examples": [
            "/api/vin/WBA8E9G50GNU12345/history",
            "/api/vin/KNAB3811ALT123456/history"
          ],
          "response": "{\n  \"success\": true,\n  \"vin\": \"WBA8E9G50GNU12345\",\n  \"match_count\": 2,\n  \"auctions\": [\n    {\n      \"site_name\": \"encar\",\n      \"auction_id\": 38112900,\n      \"clean_make\": \"BMW\",\n      \"clean_model\": \"320d\",\n      \"mileage\": 45000,\n      \"public_price_eur\": 21500,\n      \"first_seen_at\": \"2026-06-01T00:00:00\",\n      \"last_seen_at\": \"2026-07-10T00:00:00\",\n      \"archived\": false,\n      \"source_auction_id\": \"38112900\"\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "site_name / auction_id / source_auction_id",
              "type": "string | integer",
              "description": "Which source and lot. source_auction_id is the Auto1 batch id where one exists, otherwise the auction id as text."
            },
            {
              "name": "chassis_number",
              "type": "string",
              "description": "The VIN as the source published it (punctuation and case as stored)."
            },
            {
              "name": "clean_make / clean_model / model_display",
              "type": "string",
              "description": "Normalized identity."
            },
            {
              "name": "date_first_registration / mileage",
              "type": "string | integer",
              "description": "Registration date and the mileage recorded on that appearance."
            },
            {
              "name": "current_price / buy_now_price / start_price / final_price / current_final / buynow_final / public_price_eur",
              "type": "number | null",
              "description": "The whole price block. JSON numbers or null."
            },
            {
              "name": "currency_code_id",
              "type": "string",
              "description": "Currency of the source-side figures."
            },
            {
              "name": "first_seen_at / last_seen_at / last_available_at / created_at / updated_at / archived_at",
              "type": "timestamp | null",
              "description": "Lifecycle. archived_at is null on a live row."
            },
            {
              "name": "is_active / offer_link",
              "type": "boolean | string",
              "description": "Whether the lot is still open, and its listing URL."
            },
            {
              "name": "archived",
              "type": "boolean",
              "description": "True when this appearance comes from the archive rather than live inventory."
            }
          ],
          "note": "Not enabled on a new key by default — ask for it. Returns up to 100 matches, newest first by the most recent of archived_at / last_seen_at / updated_at / created_at, deduplicated on (source, auction id) so one lot never appears twice. This is the endpoint that tells you a car has been through auction more than once — two rows for one VIN, months apart, with different mileages, is the pattern worth surfacing. A VIN that is not 17 valid characters is a 400; a valid VIN we have never seen answers 200 with match_count: 0. This response carries no envelope metadata — read X-Request-ID from the header."
        }
      ]
    },
    {
      "id": "archive",
      "label": "Historical inventory",
      "description": "Ended listings moved into the archive, separate from ended rows still in the live table. Requires the archive scope. A last observed price is not a confirmed sale price.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/archive/search",
          "summary": "Search ended auctions",
          "scope": "archive",
          "params": [
            {
              "name": "auction_grade",
              "type": "string",
              "example": "4,4.5,5",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's grade, as a comma-separated list of the\ngrades to keep: `S`, `6`, `5`, `4.5`, `4`, `3.5`, `3`, `2`, `1`, `R` and\n`RA` (repaired) and `***` (ungraded). Matched exactly, not as a minimum,\nbecause the letters have no place on the numeric scale. Case and\nfull-width digits are folded; any other value is a 400 on `/api/search`\nand `/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a grade, so any value keeps only them. The\ngrade is the resellers' where they state one they agree on, else the one\nread off the lot's auction sheet. It is published on each result as\n`auction_grade`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "auction_house",
              "type": "string",
              "example": "uss,honda_tokyo",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house, as a comma-separated list of names in the\nslug form each result's `auction_house` carries (`uss_tokyo`,\n`honda_tokyo`). Spaces, case and punctuation are folded, so\n`HONDA TOKYO` works too.\n\nMatches whole words of the house name: `uss` finds every USS branch,\n`tokyo` every Tokyo house, `uss_tokyo` that one house; `oky` finds\nnothing. A value that is not a Latin-script name is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints. Only Japanese listings carry a house, so any value keeps\nonly them. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "",
              "source": "Listing values or your application settings.",
              "description": "`true` restricts to damaged/broken vehicles, `false` to undamaged ones. Also accepts `1`/`0`/`yes`/`no`."
            },
            {
              "name": "engine_from",
              "type": "number",
              "example": "",
              "source": "Listing values or your application settings.",
              "description": "Smallest engine in litres, e.g. `1.6` (matches from 1550 cc)."
            },
            {
              "name": "engine_to",
              "type": "number",
              "example": "",
              "source": "Listing values or your application settings.",
              "description": "Largest engine in litres, e.g. `2.0` (matches up to 2049 cc)."
            },
            {
              "name": "horsepower_from",
              "type": "integer",
              "example": "",
              "source": "Listing values or your application settings.",
              "description": ""
            },
            {
              "name": "horsepower_to",
              "type": "integer",
              "example": "",
              "source": "Listing values or your application settings.",
              "description": ""
            },
            {
              "name": "lot_number",
              "type": "string",
              "example": "80041",
              "source": "Listing values or your application settings.",
              "description": "The Japanese auction house's lot number, as a comma-separated list.\nLeading zeros and full-width digits are folded (`080041` finds `80041`);\na value that is not letters and digits is a 400 on `/api/search` and\n`/api/archive/search`, and ignored by the facet endpoints.\n\nOnly Japanese listings carry a lot number, so any value keeps only them.\nA house numbers its lots per sale day, so one number can match several\ncars: tell them apart by `auction_house` and `batch_end_date` (the sale\nday) on each result. It is published on each result as\n`lot_number`. Not in the precomputed search index, so a request that\nsends it is served by the live query and is slower.\n"
            },
            {
              "name": "model_code",
              "type": "string",
              "example": "USF40,ZC6",
              "source": "Listing values or your application settings.",
              "description": "A Japanese lot's chassis model code (e.g. `USF40`, `ZC6`), as a\ncomma-separated list. Compared without case or punctuation, so\n`u-30-5` finds `U-30-5`. A value with no letters or digits is a 400 on\n`/api/search` and `/api/archive/search`, and ignored by the facet\nendpoints.\n\nOnly Japanese listings carry a model code, so any value keeps only\nthem. Published on each result as `model_code`. Not in the precomputed\nsearch index, so a request that sends it is served by the live query\nand is slower.\n"
            },
            {
              "name": "site",
              "type": "string",
              "description": "Comma-separated source slugs from `/api/sites`, OR within the parameter.\nCase-insensitive; padding and empty members are ignored. An unknown slug\nis a `400` naming the offender.\n",
              "example": "openlane,auto1",
              "source": "Current API reference"
            },
            {
              "name": "site_exclude",
              "type": "string",
              "description": "Comma-separated source slugs to leave out. Same rules as `site`.",
              "example": "copart",
              "source": "Current API reference"
            },
            {
              "name": "brand",
              "type": "string",
              "description": "Brand display name, slug (from `/api/brands`), or numeric `brand_id`.",
              "example": "bmw",
              "source": "Current API reference"
            },
            {
              "name": "model",
              "type": "string",
              "description": "Model name or slug.",
              "example": "320d",
              "source": "Current API reference"
            },
            {
              "name": "country",
              "type": "string",
              "description": "ISO country code from `/api/countries`, or the pseudo-country `europe`.\nAccepted and ignored by `/api/sites` and `/load-models`.\n",
              "example": "DE",
              "source": "Current API reference"
            },
            {
              "name": "fuel",
              "type": "string",
              "description": "Canonical fuel group from `/api/fuels`.",
              "example": "Diesel",
              "source": "Current API reference"
            },
            {
              "name": "gearbox",
              "type": "string",
              "description": "Canonical gearbox group from `/api/gearboxes`.",
              "example": "Automatic",
              "source": "Current API reference"
            },
            {
              "name": "year_from",
              "type": "integer",
              "description": "Lower bound on registration year, inclusive.",
              "example": "2018",
              "source": "Current API reference"
            },
            {
              "name": "year_to",
              "type": "integer",
              "description": "Upper bound on registration year, inclusive.",
              "example": "2024",
              "source": "Current API reference"
            },
            {
              "name": "production_year_from",
              "type": "integer",
              "description": "Lower bound on production year, inclusive. `production_year` falls back to the registration year where the source publishes no build year, so missing build-year evidence falls back to the registration-year dimension.",
              "example": "2018",
              "source": "Current API reference"
            },
            {
              "name": "production_year_to",
              "type": "integer",
              "description": "Upper bound on production year, inclusive.",
              "example": "2024",
              "source": "Current API reference"
            },
            {
              "name": "kilometers_from",
              "type": "integer",
              "description": "Lower bound on mileage in kilometres.",
              "example": "0",
              "source": "Current API reference"
            },
            {
              "name": "kilometers_to",
              "type": "integer",
              "description": "Upper bound on mileage in kilometres.",
              "example": "150000",
              "source": "Current API reference"
            },
            {
              "name": "price_from",
              "type": "integer",
              "description": "Lower bound on the public EUR price.",
              "example": "1000",
              "source": "Current API reference"
            },
            {
              "name": "price_to",
              "type": "integer",
              "description": "Upper bound on the public EUR price.",
              "example": "30000",
              "source": "Current API reference"
            },
            {
              "name": "archived_from",
              "type": "string",
              "description": "Only lots archived on or after this date (`YYYY-MM-DD`). Anything else is a `400`.",
              "example": "2026-07-01",
              "source": "Current API reference"
            },
            {
              "name": "archived_to",
              "type": "string",
              "description": "Only lots archived on or before this date (`YYYY-MM-DD`). Inclusive of the whole day named.",
              "example": "2026-08-31",
              "source": "Current API reference"
            },
            {
              "name": "vin",
              "type": "string",
              "description": "Exact full-VIN filter. Punctuation and case are ignored. A value that is not a valid 17-character VIN is a `400`. VIN coverage varies; query /api/archive/stats before relying on it.",
              "example": "WBA8E9G50GNU12345",
              "source": "Current API reference"
            },
            {
              "name": "sort",
              "type": "string",
              "description": "Result ordering.",
              "example": "archived_desc",
              "source": "Current API reference"
            },
            {
              "name": "limit",
              "type": "integer",
              "description": "Page size. Capped at `pagination.max_limit` from `GET /api/contract`\n(100 by default; up to 500 for a key granted bulk pages); larger values\nare clamped down rather than rejected. Do not combine with `page_size`\n— that is a `400`.\n",
              "example": "20",
              "source": "Current API reference"
            },
            {
              "name": "offset",
              "type": "integer",
              "description": "0-based row offset. Do not combine with `page` — that is a `400`.\nBounded by `pagination.max_offset` (5000 by default) on archive, catalog, top-offers and classifieds; /api/search and /api/listVehicles have no offset cap.\n",
              "example": "0",
              "source": "Current API reference"
            },
            {
              "name": "page",
              "type": "integer",
              "description": "1-based page number; maps to `offset = (page - 1) * limit`. Do not combine with `offset`.",
              "example": "1",
              "source": "Current API reference"
            },
            {
              "name": "page_size",
              "type": "integer",
              "description": "Alias for `limit`. Do not combine with `limit`.",
              "example": "20",
              "source": "Current API reference"
            },
            {
              "name": "include_total",
              "type": "boolean",
              "description": "`false` returns `total: null` and skips the count, which is faster.",
              "example": "true",
              "source": "Current API reference"
            }
          ],
          "examples": [
            "/api/archive/search?site=japan&year_from=2020&limit=20",
            "/api/archive/search?brand=bmw&archived_from=2026-09-01"
          ],
          "response": "{\n  \"success\": true,\n  \"results\": [],\n  \"total\": 0,\n  \"limit\": 20,\n  \"offset\": 0,\n  \"sort\": \"archived_desc\",\n  \"page\": 1,\n  \"page_size\": 20,\n  \"total_pages\": 0,\n  \"max_page\": 0,\n  \"contract_version\": \"2026-10-05\"\n}",
          "note": "The historical counterpart to `/api/search`. It reads `auctions_archive`,\nwhich holds lots that ended, stayed inactive for the retention window,\nand were moved out of the live table with an `archived_at` stamp.\nNothing here is buyable.\n\n**This is not a sold-price index.** `public_price_eur` is the last public price observed. `final_price` may be absent or have an unverified result basis; it is not proof of a completed sale. Call `/api/archive/stats` for the per-source\ncoverage before building anything on a particular field.\n\n**No free-text `search` parameter.** The archive carries no trigram\nindex, and a substring scan across it costs seconds. `brand` and `model`\nmatch exactly (case- and slug-insensitive), as they do on `/api/search`.\n\nDefault ordering is newest-archived first. The general offset cap\n(`pagination.max_offset`, 5000 by default) applies — there is no\nunfiltered deep-paging path here.\n"
        },
        {
          "method": "GET",
          "path": "/api/archive/auction/{site_slug}/{auction_id}",
          "summary": "One archived auction in full",
          "scope": "archive",
          "params": [
            {
              "name": "site_slug",
              "type": "string",
              "description": "",
              "example": "openlane",
              "source": "Current API reference",
              "location": "path",
              "required": true
            },
            {
              "name": "auction_id",
              "type": "integer",
              "description": "The source's auction id, as published on the search card.",
              "example": "38112900",
              "source": "Current API reference",
              "location": "path",
              "required": true
            }
          ],
          "examples": [
            "/api/archive/auction/encar/38112900"
          ],
          "response": "{\n  \"success\": true,\n  \"auction\": {\n    \"site_name\": \"encar\",\n    \"auction_id\": 38112900,\n    \"auction_id_str\": \"38112900\",\n    \"archived\": true,\n    \"archived_at\": \"2026-09-01T12:00:00\",\n    \"final_price\": null\n  },\n  \"contract_version\": \"2026-10-05\"\n}",
          "note": "The archived counterpart to `/api/auction/{site_slug}/{auction_id}`, with\nthe same detail field set.\n\nTwo blocks are deliberately absent. There is no `live_price`: the lot is\nover, so there is no bid to refresh. There is no `vault_gallery`: that is\nserved from the live gallery route. `thumbnail_url` and whatever the row\nstored in `images` / `car_identification` are on the payload.\n\nAn archived row is never written again, so this response is cached for an\nhour.\n"
        },
        {
          "method": "GET",
          "path": "/api/archive/facets",
          "summary": "Counts of finished lots per filter value.",
          "scope": "archive",
          "params": [
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out. An unknown slug is a 400 naming the offender."
            },
            {
              "name": "brand / model",
              "type": "string",
              "example": "bmw / 320d",
              "source": "/api/brands -> brands[].slug, /api/models?brand=bmw -> models[].slug",
              "description": "Brand name, slug or brand_id, and model name or slug, matched exactly as on /api/archive/search. Giving brand adds a model facet."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical fuel and gearbox groups."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "Year bounds chosen by your user.",
              "description": "Registration-year range, inclusive."
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "Year bounds chosen by your user.",
              "description": "Production-year range. Falls back to registration year where the source publishes no build year."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range in kilometres."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "1000 / 30000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "engine_from / engine_to",
              "type": "number",
              "example": "1.6 / 2.0",
              "source": "Engine sizes chosen by your user.",
              "description": "Engine size in litres. 1.6 matches from 1550 cc; 2.0 matches up to 2049 cc."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "false",
              "source": "Your filter choice.",
              "description": "true keeps only damaged or broken vehicles, false only undamaged ones."
            },
            {
              "name": "archived_from / archived_to",
              "type": "string",
              "example": "2026-07-01 / 2026-09-30",
              "source": "Date range chosen by your user.",
              "description": "Lots that ended on or after / on or before this date (YYYY-MM-DD)."
            },
            {
              "name": "vin",
              "type": "string",
              "example": "WBA8E9G50GNU12345",
              "source": "A VIN you already have.",
              "description": "Exact full-VIN filter."
            },
            {
              "name": "limit",
              "type": "integer",
              "example": "100",
              "source": "How many values your control can hold.",
              "description": "Values per facet. Default 100, maximum 500."
            }
          ],
          "examples": [
            "/api/archive/facets?brand=bmw&year_from=2018",
            "/api/archive/facets?site=copart&archived_from=2026-07-01"
          ],
          "response": "{\n  \"success\": true,\n  \"total\": 48210,\n  \"facets\": {\n    \"site\": [\n      {\n        \"value\": \"openlane\",\n        \"count\": 18220\n      }\n    ],\n    \"brand\": [\n      {\n        \"value\": \"BMW\",\n        \"count\": 48210\n      }\n    ],\n    \"model\": [\n      {\n        \"value\": \"320d\",\n        \"count\": 3120\n      }\n    ],\n    \"country\": [\n      {\n        \"value\": \"DE\",\n        \"count\": 11840\n      }\n    ],\n    \"fuel\": [\n      {\n        \"value\": \"Diesel\",\n        \"count\": 30110\n      }\n    ],\n    \"gearbox\": [\n      {\n        \"value\": \"Automatic\",\n        \"count\": 35020\n      }\n    ],\n    \"year\": [\n      {\n        \"value\": 2019,\n        \"count\": 6120\n      }\n    ],\n    \"condition\": [\n      {\n        \"value\": \"normal\",\n        \"count\": 44090\n      }\n    ]\n  }\n}",
          "responseFields": [
            {
              "name": "total",
              "type": "integer",
              "description": "Finished lots matching the filters."
            },
            {
              "name": "facets.<dimension>",
              "type": "{value, count}[]",
              "description": "site, brand, country, fuel, gearbox, year and condition, plus model when brand is given — the values present under the other filters, biggest first."
            }
          ],
          "note": "Covers archived lots and lots that have ended but are not archived yet (a lot is archived 45 days after it ends). Rebuilt nightly and cached for an hour. The response carries no contract_version envelope. Takes the archive search filters except the Japanese sheet filters (auction_grade, lot_number, model_code, auction_house), which are a 400 here."
        },
        {
          "method": "GET",
          "path": "/api/archive/statistics",
          "summary": "Closing-price statistics of finished lots, grouped by one dimension.",
          "scope": "archive",
          "params": [
            {
              "name": "group_by",
              "type": "enum",
              "example": "month",
              "source": "site, country, brand, model, year, fuel, gearbox, condition, month or week.",
              "description": "The dimension to group by. Default month."
            },
            {
              "name": "site / site_exclude",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to include or leave out. An unknown slug is a 400 naming the offender."
            },
            {
              "name": "brand / model",
              "type": "string",
              "example": "bmw / 320d",
              "source": "/api/brands -> brands[].slug, /api/models?brand=bmw -> models[].slug",
              "description": "Brand name, slug or brand_id, and model name or slug, matched exactly as on /api/archive/search. Giving brand adds a model facet."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "ISO country code or europe."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[], /api/gearboxes -> gearboxes[]",
              "description": "Canonical fuel and gearbox groups."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "Year bounds chosen by your user.",
              "description": "Registration-year range, inclusive."
            },
            {
              "name": "production_year_from / production_year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "Year bounds chosen by your user.",
              "description": "Production-year range. Falls back to registration year where the source publishes no build year."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage range in kilometres."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "1000 / 30000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price range."
            },
            {
              "name": "horsepower_from / horsepower_to",
              "type": "integer",
              "example": "150 / 300",
              "source": "Power bounds chosen by your user.",
              "description": "Power range in HP."
            },
            {
              "name": "engine_from / engine_to",
              "type": "number",
              "example": "1.6 / 2.0",
              "source": "Engine sizes chosen by your user.",
              "description": "Engine size in litres. 1.6 matches from 1550 cc; 2.0 matches up to 2049 cc."
            },
            {
              "name": "damaged",
              "type": "boolean",
              "example": "false",
              "source": "Your filter choice.",
              "description": "true keeps only damaged or broken vehicles, false only undamaged ones."
            },
            {
              "name": "archived_from / archived_to",
              "type": "string",
              "example": "2026-07-01 / 2026-09-30",
              "source": "Date range chosen by your user.",
              "description": "Lots that ended on or after / on or before this date (YYYY-MM-DD)."
            },
            {
              "name": "vin",
              "type": "string",
              "example": "WBA8E9G50GNU12345",
              "source": "A VIN you already have.",
              "description": "Exact full-VIN filter."
            },
            {
              "name": "limit",
              "type": "integer",
              "example": "24",
              "source": "How many groups you will show.",
              "description": "Groups returned. Default 100, maximum 500."
            }
          ],
          "examples": [
            "/api/archive/statistics?brand=bmw&model=320d&group_by=month",
            "/api/archive/statistics?group_by=country&year_from=2019"
          ],
          "response": "{\n  \"success\": true,\n  \"group_by\": \"month\",\n  \"groups\": [\n    {\n      \"month\": \"2026-09\",\n      \"count\": 412,\n      \"priced_count\": 401,\n      \"result_count\": 140,\n      \"buy_now_count\": 37,\n      \"close_median_eur\": 18400,\n      \"close_p25_eur\": 14200,\n      \"close_p75_eur\": 22900,\n      \"close_mean_eur\": 18950,\n      \"buy_now_median_eur\": 21900,\n      \"mean_mileage_km\": 98000,\n      \"mean_hp\": 184,\n      \"damaged_share\": 0.08\n    }\n  ],\n  \"notes\": {\n    \"close_price\": \"the source's published result where it has one (result_count), else the last bid seen - not proof of a sale\",\n    \"coverage\": \"archived lots plus lots that ended and are not archived yet\"\n  }\n}",
          "responseFields": [
            {
              "name": "groups[]",
              "type": "object[]",
              "description": "Each group is keyed by its dimension (month: \"2026-09\", site: \"openlane\", ...) and carries count, priced_count, result_count (lots whose source published a result), buy_now_count, close_median_eur, close_p25_eur, close_p75_eur, close_mean_eur, buy_now_median_eur, mean_mileage_km, mean_hp and damaged_share (0–1). EUR and mean figures are whole numbers, null where nothing in the group had a value. month, week and year groups come back in chronological order, every other grouping biggest first."
            },
            {
              "name": "notes",
              "type": "object",
              "description": "Plain-language caveats: what close_price means and which lots are covered."
            }
          ],
          "note": "Same population and filters as /api/archive/facets. The closing price is the source’s published result where it has one, otherwise the last bid seen — it is not proof of a sale. Cached for an hour; no contract_version envelope."
        },
        {
          "method": "GET",
          "path": "/api/archive/stats",
          "summary": "Archive coverage per source",
          "scope": "archive",
          "params": [],
          "examples": [
            "/api/archive/stats"
          ],
          "response": "{\n  \"success\": true,\n  \"total\": 0,\n  \"archived_from\": null,\n  \"archived_to\": null,\n  \"sources\": [],\n  \"contract_version\": \"2026-10-05\"\n}",
          "note": "How much history exists, per source, and how much of it carries a price,\na reported final price, or a VIN. Read this before assuming a field is populated:\n`with_final_price` is the number that stops the archive being mistaken\nfor a sold-price feed.\n"
        }
      ]
    },
    {
      "id": "car-details",
      "label": "Vehicle details",
      "description": "Fetch a full source payload from cache or, when needed, from the upstream source.",
      "endpoints": [
        {
          "method": "GET | POST",
          "path": "/api/car-details",
          "summary": "Fetch full vehicle detail by source and listing identifier.",
          "scope": "details",
          "params": [
            {
              "name": "site",
              "type": "string",
              "example": "encar",
              "source": "/api/sites -> sites[].name",
              "required": true,
              "description": "schadeautos, auto1, openlane, copart, encar, kbchachacha,. Japan has stored auction detail only and is rejected here."
            },
            {
              "name": "id / link / identifier",
              "type": "string",
              "example": "38112900",
              "source": "/api/search -> results[].auction_id, or the source listing URL",
              "required": true,
              "description": "Provide any one source id or listing URL. For auto1 use the source offer identifier. KB ChaChaCha requires numeric carSeq. For Japan use the stored auction detail endpoint instead."
            },
            {
              "name": "search_id",
              "type": "string",
              "example": "vehicle-page-42",
              "source": "Optional correlation id generated by your application.",
              "description": "Optional correlation id."
            }
          ],
          "examples": [
            "/api/car-details?site=encar&id=38112900",
            "POST /api/car-details\n{\n  \"site\": \"openlane\",\n  \"identifier\": \"11125938\",\n  \"search_id\": \"vehicle-page-42\"\n}"
          ],
          "response": "{\n  \"success\": true,\n  \"site\": \"openlane\",\n  \"auction_id\": 11409652,\n  \"current_price\": 25900,\n  \"current_final\": 33566,\n  \"public_price_eur\": 33926,\n  \"vehicle_details\": {},\n  \"auction\": {\n    \"extracted_fields\": {\n      \"images\": []\n    }\n  },\n  \"live_price\": {\n    \"price\": 25900,\n    \"currency\": \"EUR\",\n    \"source\": \"openlane\",\n    \"fetched_at\": 1786659750\n  }\n}",
          "responseFields": [
            {
              "name": "vehicle_details",
              "type": "object | null",
              "description": "The same normalized block /api/auction/{site}/{id} returns, so the two detail endpoints agree. Null when the listing has no fetched details yet. This is the only part of the response with a stable cross-source contract — the remaining keys are source-shaped (data, auction, images, details depending on the source)."
            },
            {
              "name": "details_pending",
              "type": "boolean",
              "description": "True while the upstream fetch is still running — a fetch that outran the request time budget keeps running in the background. Poll rather than retrying in a tight loop. The X-Details-Fetch header carries the same signal as fetched, pending or miss, and is absent when the response was served from stored data. Cache-Control is max-age=5 while pending and max-age=60 once settled."
            },
            {
              "name": "current_price / current_final / public_price_eur",
              "type": "number",
              "description": "The same price fields as /api/auction/{site}/{id}, under the same names and with the same JSON types — but at the top level here, because this body is source-shaped and has no auction wrapper. Updates between separate requests can change prices; compare listing timestamps and live_price.fetched_at."
            },
            {
              "name": "live_price / live_price_pending",
              "type": "object | boolean",
              "description": "Identical contract to /api/auction/{site}/{id}, and attached the same way when this response is served from stored auction data — the normal case, for a running openlane auction. A first-ever fetch of a listing we hold no row for returns the source’s own fresh payload anyway."
            },
            {
              "name": "auction",
              "type": "object",
              "description": "Not the vehicle record. For openlane this is { extracted_fields: { images: […] } }, an image container that shares a name and nothing else. Read prices from the top level on this endpoint, and from inside auction on /api/auction/{site}/{id}."
            },
            {
              "name": "estimated_value_eur",
              "type": "number | null",
              "description": "Optional auction-house valuation in EUR where available. Treat it as an estimate, never a price you can pay or a settled result. Do not substitute it for current_price."
            },
            {
              "name": "steering",
              "type": "enum | null",
              "description": "Same derivation and spelling as the search cards and /api/auction/{site}/{id}, published at the top level here. Null only when the vehicle is not in our inventory at all, because there is then no registration country to derive a side from."
            }
          ],
          "note": "Parameters may be sent in the query string or a JSON body. This endpoint can be slower than canonical auction detail. Fee, transport, delivery and account-scoped fields are stripped here exactly as they are on auction detail. When no details are stored yet it fetches from the source during the request and returns what it just stored, rather than an empty shell — read details_pending in the body and X-Details-Fetch in the headers (fetched, pending, or miss; absent when served from stored data), and poll rather than retrying in a tight loop. This response carries no envelope metadata — no contract_version, request_id, server_time or data_updated_at in the body. Read the correlation id from the X-Request-ID header. The response also carries X-Data-Source (auctions, car_details_cache or scraped) telling you which path answered, and X-Cache-Age in seconds on a cache hit. Everything outside vehicle_details and the price fields is source-shaped (data, auction, images, details depending on the source); those two are the parts with a stable cross-source contract. Live prices apply here too when the response comes from stored auction data; note the one trap that catches people — for openlane this body’s auction key is an image container, not the vehicle record, so prices are read from the top level. If the source reports the listing is gone, that surfaces as a 404 rather than a 500."
        },
        {
          "method": "GET | POST",
          "path": "/api/listVehicles",
          "summary": "Catalog-shaped listing feed — a compatibility alias for search.",
          "scope": "details",
          "params": [
            {
              "name": "page / page_size",
              "type": "integer",
              "example": "1 / 50",
              "source": "Pagination state in your application.",
              "description": "1-based page and rows per page, as alternatives to offset and limit. Do not combine page with offset or page_size with limit."
            },
            {
              "name": "manufacturer_slug / manufacturer / brand",
              "type": "string",
              "example": "bmw",
              "source": "/api/catalog/manufacturers -> results[].slug",
              "description": "Brand, under any of the three accepted spellings."
            },
            {
              "name": "model_group_slug / model_group / model",
              "type": "string",
              "example": "320d",
              "source": "/api/catalog/model-groups?manufacturer__slug=bmw -> results[].slug",
              "description": "Model group, under any of the three accepted spellings."
            },
            {
              "name": "fuel_type / fuel",
              "type": "string",
              "example": "Diesel",
              "source": "/api/fuels -> fuels[]",
              "description": "Canonical fuel group."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Left- or right-hand drive, in any of the spellings /api/search accepts."
            },
            {
              "name": "min_year / max_year",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "Year bounds chosen by your user.",
              "description": "Registration-year range."
            },
            {
              "name": "max_mileage",
              "type": "integer",
              "example": "120000",
              "source": "Mileage ceiling chosen by your user.",
              "description": "Mileage ceiling."
            },
            {
              "name": "ordering",
              "type": "string",
              "example": "-created_at",
              "source": "created_at / created / published_at, price / price_eur, year / date_first_registration, each with an optional leading minus.",
              "description": "Sort order. An unrecognised value silently falls back to the default (photo-first, then newest) rather than erroring."
            },
            {
              "name": "limit / offset",
              "type": "integer",
              "example": "50 / 0",
              "source": "Pagination state in your application.",
              "description": "Or page_size / page. Default limit 50. No offset cap."
            }
          ],
          "examples": [
            "/api/listVehicles?manufacturer_slug=bmw&model_group_slug=320d&max_mileage=120000",
            "/api/listVehicles?brand=audi&min_year=2019&ordering=price&limit=50"
          ],
          "response": "{\n  \"success\": true,\n  \"vehicles\": [],\n  \"results\": [],\n  \"total\": 210,\n  \"limit\": 50,\n  \"offset\": 0,\n  \"total_pages\": 5,\n  \"max_page\": 5\n}",
          "responseFields": [
            {
              "name": "vehicles / results",
              "type": "object[]",
              "description": "The same array object served twice under two names for compatibility. Read either one, not both. Each row is a standard search result card plus four catalog-shaped keys: vehicle_id (the same value as auction_id — subject to the same 2^53 caveat, prefer auction_id_str), manufacturer_slug (slug of clean_make), model_group_slug (slug of clean_model), and fuel_type (normalized fuel, present only when the row carries one). Rows carry the same 8-photo images preview cap as /api/search."
            },
            {
              "name": "total / limit / offset / page / page_size / total_pages / max_page",
              "type": "integer | null",
              "description": "Standard pagination fields. There is no offset cap — max_page equals total_pages, as on /api/search. Default limit 50."
            }
          ],
          "note": "A compatibility alias kept for older integrations, also reachable at /listVehicles, and accepted as GET (query string) or POST (JSON body) with the same fields. Not enabled on a new key by default — ask for it. New integrations should use /api/search, which has more filters, cheaper counts, ETags and a stable envelope. This route exists so older clients keep working. ordering understands three fields, each with an optional minus for descending: created_at / created / published_at (default, photo-first then newest), -created_at / -created / -published_at (newest), price / price_eur (price_low), -price / -price_eur (price_high), year / date_first_registration (year_old), -year / -date_first_registration (year_new). An unrecognised value silently falls back to the default order rather than erroring. The response carries no envelope metadata and is sent Cache-Control: no-store, so it is never revalidated with an ETag."
        }
      ]
    },
    {
      "id": "top-offers",
      "label": "Top offers",
      "description": "Live auctions our pipeline judged to be priced below their market reference. Same deals as /api/search?sort=top_offers, but each card additionally carries the reference the verdict was made against.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/top-offers",
          "summary": "Feed of auctions priced below their market reference, newest comparison first.",
          "scope": "top-offers",
          "params": [
            {
              "name": "page / page_size",
              "type": "integer",
              "example": "1 / 50",
              "source": "Pagination state in your application.",
              "description": "1-based page and rows per page, as alternatives to offset and limit. Do not combine page with offset or page_size with limit."
            },
            {
              "name": "site",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs. An unknown value returns 400."
            },
            {
              "name": "site_exclude",
              "type": "string",
              "example": "copart",
              "source": "/api/sites -> sites[].name",
              "description": "Comma-separated source slugs to leave out. Same rules as on /api/search."
            },
            {
              "name": "steering",
              "type": "enum",
              "example": "left",
              "source": "left | lhd | left-hand | left_hand, or right | rhd | right-hand | right_hand — any other value is a 400.",
              "description": "Left- or right-hand drive, in any of the spellings /api/search accepts."
            },
            {
              "name": "brand",
              "type": "string",
              "example": "BMW",
              "source": "/api/search -> results[].clean_make",
              "description": "Exact clean_make, case-insensitive."
            },
            {
              "name": "model",
              "type": "string",
              "example": "3 Series",
              "source": "/api/search -> results[].clean_model",
              "description": "Exact clean_model, case-insensitive."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/countries -> country_details[].code",
              "description": "Vehicle country code, case-insensitive, matched exactly. Unlike /api/search, the europe pseudo-country is not supported here — it would match nothing. Pass ISO codes."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2024",
              "source": "Year bounds chosen by your user.",
              "description": "First-registration year bounds."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 150000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage bounds."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "5000 / 25000",
              "source": "EUR budget chosen by your user.",
              "description": "Public EUR price bounds."
            },
            {
              "name": "min_savings_pct",
              "type": "integer",
              "example": "20",
              "source": "Threshold chosen by your application.",
              "description": "Only offers saving at least this percentage."
            },
            {
              "name": "sort",
              "type": "enum",
              "example": "savings_pct",
              "source": "Choose savings_pct, savings, price_low, price_high, or newest.",
              "description": "Default savings_pct. An unknown value is a 400 listing the valid values."
            },
            {
              "name": "limit / offset",
              "type": "integer",
              "example": "24 / 0",
              "source": "Pagination state in your application.",
              "description": "Or page_size / page. Default 50, max 100 (up to 500 on a key granted bulk pages), offset capped at 5000. This envelope carries total, limit, offset and total_pages only — no page, page_size or max_page. Page by offset."
            }
          ],
          "examples": [
            "/api/top-offers?site=openlane&min_savings_pct=20&limit=24",
            "/api/top-offers?brand=BMW&country=DE&sort=savings&page=2&page_size=20"
          ],
          "response": "{\n  \"success\": true,\n  \"results\": [\n    {\n      \"auction_id\": 8842711,\n      \"site_name\": \"openlane\",\n      \"car_name_en\": \"BMW 320d Touring\",\n      \"clean_make\": \"BMW\",\n      \"clean_model\": \"3 Series\",\n      \"public_price_eur\": 9000,\n      \"is_top_offer\": true,\n      \"top_offer_savings\": 2500,\n      \"top_offer_savings_pct\": 21.7,\n      \"market_reference\": {\n        \"price_eur\": 11500,\n        \"mileage\": 165000,\n        \"km_difference\": -15000,\n        \"explanation\": \"Rule: this car must save at least 1,800 EUR — ...\"\n      }\n    }\n  ],\n  \"total\": 318,\n  \"limit\": 24,\n  \"offset\": 0,\n  \"total_pages\": 14\n}",
          "responseFields": [
            {
              "name": "results[]",
              "type": "object[]",
              "description": "A standard search result card plus the top-offer fields below. One difference from a search card: no image annotations. These rows carry thumbnail_url as stored, but not served_thumbnail_url, not images[], and not image_status / image_source. Fetch photos from /api/auction/{site}/{id} (vault_gallery) or /api/auction-images."
            },
            {
              "name": "is_top_offer",
              "type": "boolean",
              "description": "Always true on this feed. Also present on /api/search cards."
            },
            {
              "name": "top_offer_savings",
              "type": "number",
              "description": "Absolute EUR saving against the market reference."
            },
            {
              "name": "top_offer_savings_pct",
              "type": "number",
              "description": "Saving as a percentage of the reference price. Default sort key."
            },
            {
              "name": "market_reference.price_eur",
              "type": "number",
              "description": "The reference price this car was measured against."
            },
            {
              "name": "market_reference.mileage",
              "type": "integer",
              "description": "The reference mileage, so you can judge whether the comparison is like-for-like."
            },
            {
              "name": "market_reference.km_difference",
              "type": "integer",
              "description": "This car’s mileage minus the reference. Negative means this car has done fewer kilometres."
            },
            {
              "name": "market_reference.explanation",
              "type": "string",
              "description": "The rule that admitted this car to the feed, in words — e.g. the minimum EUR saving it had to clear."
            }
          ],
          "note": "A car qualifies only when its saving clears a continuous, reference-quality-aware bar — wider price spread, thinner evidence, and slower-selling models all raise it — so this is a shortlist, not everything below average. Rows leave the feed the moment the auction ends, so total shrinks through the day and grows again after each comparison run. The comparable’s own listing URL and identity are not exposed. A non-numeric value on a numeric parameter is a 400, not a dropped filter. Responses are cached for 60 seconds and served with an ETag."
        }
      ]
    },
    {
      "id": "theparking",
      "label": "European classifieds",
      "guide": {
        "title": "Calling it end to end",
        "steps": [
          "Call GET /api/classifieds/facets once and cache it. Everything sits under a facets key: countries, brands, fuels, gearboxes, sellers, sources, value_counts and totals. It takes no filters — it always describes the whole feed. Never hard-code these values: brand, model, fuel, gearbox and seller are compared case-sensitively, so copy the spellings from here.",
          "When the user picks a brand, call GET /api/classifieds/models?brand=BMW,Audi to fill the model select. Models come back most common first, capped at 400.",
          "Query GET /api/classifieds/listings with the chosen filters. Multi-value filters are comma-separated: they OR within a parameter and AND across parameters. Page with limit/offset or page/page_size — limit caps at 100 (500 on a key granted bulk pages). If you do not need a match count, pass include_total=false: it is the fastest way to page this feed.",
          "Render each row directly. There is no detail endpoint and no gallery: the listing object you receive is the entire record, with one remote thumbnail in image_url. Link out with source_url (the ad on the origin portal) and fall back to source_url_redirect when it is null; /api/classifieds/resolve resolves one listing on demand.",
          "To use it as a market reference, query the same brand, model, and year window you are about to bid on, read the price_eur spread by seller_type, and diff it against the auction lot’s public_price_eur."
        ]
      },
      "description": "A retail price surface of 9.8M listings in the 2026-08-18 coverage snapshot gathered from 681 origin portals — national classifieds sites, dealer groups and manufacturer stock pages — across 39 European countries: mobile.de, kleinanzeigen.de, autoscout24, leboncoin.fr, lacentrale.fr, autosupermarket.it, njuskalo.hr and hundreds more. Every listing keeps its origin portal in source_site, so the same feed answers a national question and a portal-level one. These are retail listings, not auction inventory: no bidding, no auction end date, no detail payload and no gallery, just a single remote thumbnail per listing. They live in their own tables with their own vocabulary (seller type, origin portal), which is why they get their own query surface instead of a site slug on /api/search. Ended and paid-placement (\"promoted\") rows are excluded from every response, and all three endpoints return 503 while the dataset has not been built yet. Scope: classifieds. Legacy /api/theparking/* paths and theparking grants remain compatible aliases.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/classifieds/listings",
          "summary": "Query retail classifieds aggregated from portals across Europe.",
          "scope": "classifieds",
          "params": [
            {
              "name": "resolve_urls",
              "type": "boolean",
              "example": "false",
              "source": "Your latency choice.",
              "description": "Default true: listings on the page whose source_url has never been looked up are resolved during the request, in parallel, and remembered. false answers faster with whatever has been resolved so far; source_url_redirect works either way."
            },
            {
              "name": "page / page_size",
              "type": "integer",
              "example": "1 / 50",
              "source": "Pagination state in your application.",
              "description": "1-based page and rows per page, as alternatives to offset and limit. Do not combine page with offset or page_size with limit."
            },
            {
              "name": "country",
              "type": "string",
              "example": "de,at",
              "source": "/api/classifieds/facets -> countries[].value",
              "description": "Comma-separated country codes, case-insensitive."
            },
            {
              "name": "brand",
              "type": "string",
              "example": "BMW,Audi",
              "source": "/api/classifieds/facets -> facets.brands[].value",
              "description": "Comma-separated brands, exact as published. Compared as stored — case-sensitive. bmw matches nothing where BMW matches. Take the spelling from the facets endpoint."
            },
            {
              "name": "model",
              "type": "string",
              "example": "3 Series",
              "source": "/api/classifieds/models?brand=BMW -> models[].value",
              "description": "Comma-separated models. Compared as stored — case-sensitive."
            },
            {
              "name": "fuel",
              "type": "string",
              "example": "diesel",
              "source": "/api/classifieds/facets -> facets.fuels[].value",
              "description": "Comma-separated normalized fuels. Exact, case-sensitive."
            },
            {
              "name": "gearbox",
              "type": "string",
              "example": "automatic",
              "source": "/api/classifieds/facets -> facets.gearboxes[].value",
              "description": "Comma-separated normalized gearboxes. Exact, case-sensitive."
            },
            {
              "name": "seller",
              "type": "string",
              "example": "dealer",
              "source": "/api/classifieds/facets -> facets.sellers[].value",
              "description": "Comma-separated seller types, such as dealer or private. Exact, case-sensitive."
            },
            {
              "name": "source",
              "type": "string",
              "example": "mobile.de",
              "source": "/api/classifieds/facets -> sources[].value",
              "description": "Comma-separated origin portals, case-insensitive. Unlike site on the auction side, an unrecognised portal is not an error — this vocabulary is drawn from the data itself rather than fixed, so an unknown value simply matches nothing."
            },
            {
              "name": "source_exclude",
              "type": "string",
              "example": "mobile.de",
              "source": "/api/classifieds/facets -> sources[].value",
              "description": "Comma-separated origin portals to leave out, case-insensitive. With ~681 portals in the vocabulary this is the only practical way to say “everything but”. A listing with no recorded portal survives an exclusion."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "5000 / 15000",
              "source": "EUR budget chosen by your user.",
              "description": "EUR bounds."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2016 / 2022",
              "source": "Year bounds chosen by your user.",
              "description": "Year bounds."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 200000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage bounds."
            },
            {
              "name": "q",
              "type": "string",
              "example": "touring",
              "source": "Free text entered by your user.",
              "description": "Case-insensitive substring match on the listing title or its description. The description itself is not returned, so a match can look unexplained — it is text you cannot see. Rare q values are also the usual cause of total_unavailable."
            },
            {
              "name": "with_photo",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Only listings that carry a thumbnail."
            },
            {
              "name": "sort",
              "type": "enum",
              "example": "price_low",
              "source": "Choose newest, price_low, price_high, year_new, year_old, or mileage_low.",
              "description": "Default newest."
            },
            {
              "name": "limit / offset",
              "type": "integer",
              "example": "50 / 0",
              "source": "Pagination state in your application.",
              "description": "Or page_size / page. Default 50, max 100 (up to 500 on a key granted bulk pages), offset capped at 5000. This envelope carries total, limit, offset and total_pages only — no page, page_size or max_page. Page by offset."
            },
            {
              "name": "include_total",
              "type": "boolean",
              "example": "false",
              "source": "Your pagination strategy.",
              "description": "Default true. Pass false to skip the match count entirely — total comes back null and the request is markedly faster."
            }
          ],
          "examples": [
            "/api/classifieds/listings?country=de,at&brand=BMW&price_to=15000",
            "/api/classifieds/listings?seller=dealer&source=mobile.de&sort=price_low&with_photo=true",
            "/api/classifieds/listings?country=de&source_exclude=mobile.de,kleinanzeigen.de&include_total=false"
          ],
          "response": "{\n  \"success\": true,\n  \"listings\": [\n    {\n      \"reference_id\": \"tp-91744022\",\n      \"title\": \"BMW 320d Touring\",\n      \"brand\": \"BMW\",\n      \"model\": \"3 Series\",\n      \"engine\": \"320d\",\n      \"year\": 2016,\n      \"price_eur\": 12500.5,\n      \"mileage_km\": 180000,\n      \"fuel_norm\": \"diesel\",\n      \"gearbox_norm\": \"automatic\",\n      \"colour\": \"black\",\n      \"doors\": \"5\",\n      \"country\": \"Germany\",\n      \"country_code\": \"de\",\n      \"region\": \"Bayern\",\n      \"seller_type\": \"dealer\",\n      \"source_site\": \"mobile.de\",\n      \"photo_count\": 12,\n      \"offer_url\": \"https://www.theparking.eu/...\",\n      \"source_url\": \"https://www.mobile.de/...\",\n      \"source_url_redirect\": \"https://www.theparking.eu/tools/ETHCMOLB/0/F.html\",\n      \"image_url\": \"https://img.leparking.fr/...\",\n      \"published\": \"2026-07-30\",\n      \"first_seen_at\": \"2026-07-30T04:11:02\",\n      \"last_seen_at\": \"2026-08-05T04:09:55\"\n    }\n  ],\n  \"total\": 4412,\n  \"total_capped\": false,\n  \"total_unavailable\": false,\n  \"limit\": 50,\n  \"offset\": 0,\n  \"total_pages\": 89\n}",
          "responseFields": [
            {
              "name": "reference_id",
              "type": "string",
              "description": "Stable identity for the listing, prefixed tp-. Use it to de-duplicate across polls."
            },
            {
              "name": "title",
              "type": "string",
              "description": "Listing headline as published by the origin portal."
            },
            {
              "name": "brand / model / engine",
              "type": "string",
              "description": "Brand and model as published, plus the engine or trim string when the portal supplies one."
            },
            {
              "name": "year",
              "type": "integer",
              "description": "Registration year."
            },
            {
              "name": "price_eur",
              "type": "number",
              "description": "Retail asking price in EUR. This is an asking price, not a transaction price."
            },
            {
              "name": "mileage_km",
              "type": "integer",
              "description": "Odometer reading in kilometres."
            },
            {
              "name": "fuel_norm",
              "type": "string",
              "description": "Normalized fuel, e.g. diesel, petrol, electric, hybrid. Matches the fuel filter vocabulary."
            },
            {
              "name": "gearbox_norm",
              "type": "string",
              "description": "Normalized gearbox, e.g. automatic, manual. Matches the gearbox filter vocabulary."
            },
            {
              "name": "colour",
              "type": "string",
              "description": "Exterior colour as published."
            },
            {
              "name": "doors",
              "type": "string",
              "description": "Door count as published by the portal."
            },
            {
              "name": "country / country_code",
              "type": "string",
              "description": "Country the car is listed in — display name and lower-case code. country_code is what the country filter accepts."
            },
            {
              "name": "region",
              "type": "string",
              "description": "Sub-national region or state when the portal publishes one."
            },
            {
              "name": "seller_type",
              "type": "string",
              "description": "dealer or private. The single most useful axis for separating trade from retail asking prices."
            },
            {
              "name": "source_site",
              "type": "string",
              "description": "Origin portal the listing came from, e.g. mobile.de. Matches the source / source_exclude filter vocabulary, ~681 values wide. This is the attribution that makes a country figure readable — half of all German inventory is one portal."
            },
            {
              "name": "photo_count",
              "type": "integer",
              "description": "How many photos the origin listing carries. Only one thumbnail is exposed here."
            },
            {
              "name": "offer_url",
              "type": "string",
              "description": "The aggregator’s own detail page for the listing — a theparking.eu URL, not the ad itself. Read source_site, not this URL, to know which portal the listing belongs to."
            },
            {
              "name": "source_url",
              "type": "string | null",
              "description": "The ad on the origin portal named by source_site. Null until it has been resolved, and on a listing the portal has already dropped — fall back to source_url_redirect."
            },
            {
              "name": "source_url_redirect",
              "type": "string | null",
              "description": "The aggregator’s outbound tracker, which redirects to the origin ad. Present whenever offer_url is, so it is the link to use while source_url is null."
            },
            {
              "name": "image_url",
              "type": "string",
              "description": "Single remote thumbnail on the aggregator’s own CDN. Not copied into our image vault — treat it as a hotlink that disappears with the listing."
            },
            {
              "name": "published",
              "type": "date",
              "description": "Date the origin portal published the listing."
            },
            {
              "name": "first_seen_at / last_seen_at",
              "type": "timestamp",
              "description": "When our sweep first and most recently observed the listing. Use last_seen_at to judge staleness."
            },
            {
              "name": "total / limit / offset / total_pages",
              "type": "integer",
              "description": "Standard pagination envelope for the matching set. total is exact for ordinary filters, and null when total_unavailable is true."
            },
            {
              "name": "total_capped",
              "type": "boolean",
              "description": "True when counting stopped at 100,000. Read total as “100,000+” and page by offset rather than trusting total_pages. An unfiltered total is precomputed and never capped."
            },
            {
              "name": "total_unavailable",
              "type": "boolean",
              "description": "True when total is null because the count did not finish inside its budget — typically a q= search or a rare source. The listings themselves are complete and correct; only the count is missing, and the verdict is cached for 5 minutes."
            }
          ],
          "note": "Every multi-value filter is OR within the parameter and AND across parameters — country=de,at&brand=BMW,Audi means (DE or AT) and (BMW or Audi). A non-numeric value on a numeric parameter is a 400, not a dropped filter. Match values exactly: country, source and source_exclude are case-insensitive; brand, model, fuel, gearbox and seller are compared as stored, so take the spellings from /api/classifieds/facets rather than typing them. An unknown value is never an error here, it simply matches nothing. Counting is bounded rather than exhaustive on a feed this size: read total together with total_capped and total_unavailable, or skip it with include_total=false. Responses are served with Cache-Control: public, max-age=60 and no ETag, so conditional requests do not help here — include_total=false is the lever that does. There is no detail endpoint for these rows: those 25 keys are the whole record. To sync the whole feed rather than page through it, see the bulk export below — it is locked to approved accounts. Returns 503 while the dataset has not been built yet."
        },
        {
          "method": "GET",
          "path": "/api/classifieds/facets",
          "summary": "Counted filter vocabulary for the classifieds dataset.",
          "scope": "classifieds",
          "params": [
            {
              "name": "limit",
              "type": "integer",
              "example": "0",
              "source": "How many values your picker can hold.",
              "description": "Values returned per dimension, default 50. 0 returns all of them — the only way to get the full 681-portal sources list."
            }
          ],
          "examples": [
            "/api/classifieds/facets",
            "/api/classifieds/facets?limit=0",
            "curl -H \"X-API-Key: $API_KEY\" \"https://api.thecarapi.com/api/classifieds/facets\""
          ],
          "response": "{\n  \"success\": true,\n  \"facets\": {\n    \"countries\": [\n      {\n        \"value\": \"de\",\n        \"count\": 1840221\n      }\n    ],\n    \"brands\": [\n      {\n        \"value\": \"BMW\",\n        \"count\": 402118\n      }\n    ],\n    \"fuels\": [\n      {\n        \"value\": \"diesel\",\n        \"count\": 4110882\n      }\n    ],\n    \"gearboxes\": [\n      {\n        \"value\": \"manual\",\n        \"count\": 5233901\n      }\n    ],\n    \"sellers\": [\n      {\n        \"value\": \"dealer\",\n        \"count\": 8901233\n      }\n    ],\n    \"sources\": [\n      {\n        \"value\": \"mobile.de\",\n        \"count\": 913442\n      }\n    ],\n    \"value_counts\": {\n      \"countries\": 41,\n      \"brands\": 128,\n      \"fuels\": 7,\n      \"gearboxes\": 3,\n      \"sellers\": 2,\n      \"sources\": 683\n    },\n    \"totals\": {\n      \"total\": 9714882,\n      \"total_capped\": false,\n      \"price_min\": 50,\n      \"price_max\": 4500000,\n      \"year_min\": 1920,\n      \"year_max\": 2027\n    }\n  }\n}",
          "responseFields": [
            {
              "name": "facets.countries / brands / fuels / gearboxes / sellers / sources",
              "type": "{value, count}[]",
              "description": "Each dimension is a list of {value, count}, most common first, nested under the facets key rather than at the top level. The values are exactly what the listings feed’s filters match on, so copy them verbatim: brands, fuels, gearboxes and sellers are compared case-sensitively downstream."
            },
            {
              "name": "facets.value_counts.<dimension>",
              "type": "integer",
              "description": "How many distinct values that dimension really has, before limit trimmed it. Check value_counts.sources against the length of sources to know whether you are looking at all of them."
            },
            {
              "name": "facets.totals.total",
              "type": "integer",
              "description": "Listings in the feed."
            },
            {
              "name": "facets.totals.total_capped",
              "type": "boolean",
              "description": "True when counting stopped at 100,000 — read total as \"100,000+\"."
            },
            {
              "name": "facets.totals.price_min / price_max / year_min / year_max",
              "type": "number | integer",
              "description": "EUR and year bounds across the feed, for sizing sliders."
            }
          ],
          "note": "Takes no filters — it always describes the whole feed. This is the discovery endpoint for every other classifieds filter: always build selects from here rather than hard-coding values, because brand/model/fuel/gearbox/seller are compared case-sensitively. The default trim of 50 matters most for sources (~680 origin portals); value_counts.sources tells you how many exist and limit=0 returns them all. The vocabulary changes only when the feed is rebuilt, so it is cached for an hour and served with Cache-Control: public, max-age=600. One cached vocabulary answers every limit, so asking for limit=0 once and trimming client-side costs the same as asking for limit=10. Returns 503 while the dataset has not been built yet."
        },
        {
          "method": "GET",
          "path": "/api/classifieds/models",
          "summary": "List models available for one or more brands.",
          "scope": "classifieds",
          "params": [
            {
              "name": "brand",
              "type": "string",
              "example": "BMW,Audi",
              "source": "/api/classifieds/facets -> facets.brands[].value",
              "required": true,
              "description": "Comma-separated brand list. Required. Matched exactly and case-sensitively, like the same parameter on the listings feed — pass the spellings from the brands facet."
            }
          ],
          "examples": [
            "/api/classifieds/models?brand=BMW",
            "/api/classifieds/models?brand=BMW,Audi"
          ],
          "response": "{\n  \"success\": true,\n  \"models\": [\n    {\n      \"value\": \"3 Series\",\n      \"count\": 8841\n    },\n    {\n      \"value\": \"5 Series\",\n      \"count\": 6002\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "models",
              "type": "{value, count}[]",
              "description": "Model names available for the requested brands, most common first, capped at 400. value goes straight back into the model filter on /api/classifieds/listings."
            }
          ],
          "note": "brand is required — calling without it returns 400. Because brands are published verbatim by each portal, always take the brand value from /api/classifieds/facets rather than typing it. Cached for 600 seconds. Returns 503 while the dataset has not been built yet."
        },
        {
          "method": "GET",
          "path": "/api/classifieds/resolve",
          "summary": "Resolve the origin portal URL of one classified listing.",
          "scope": "classifieds",
          "params": [
            {
              "name": "reference_id",
              "type": "string",
              "example": "9190319266",
              "source": "/api/classifieds/listings -> listings[].reference_id",
              "description": "The listing’s stable identifier. One of reference_id, offer_url or slug is needed."
            },
            {
              "name": "offer_url",
              "type": "string",
              "example": "https://www.theparking.eu/used-cars-detail/vehicle/ad/CODU642L.html",
              "source": "/api/classifieds/listings -> listings[].offer_url",
              "description": "The aggregator detail URL, as an alternative to reference_id."
            },
            {
              "name": "slug",
              "type": "string",
              "example": "CODU642L",
              "source": "The last path segment of offer_url.",
              "description": "The aggregator listing slug, as an alternative to reference_id."
            },
            {
              "name": "redirect",
              "type": "boolean",
              "example": "true",
              "source": "Your link-handling choice.",
              "description": "true answers with a 302 straight to the origin ad (or the tracker fallback) instead of JSON — usable as the href of an outbound link. Default false."
            }
          ],
          "examples": [
            "/api/classifieds/resolve?reference_id=9190319266",
            "/api/classifieds/resolve?slug=CODU642L&redirect=true"
          ],
          "response": "{\n  \"success\": true,\n  \"reference_id\": \"9190319266\",\n  \"slug\": \"CODU642L\",\n  \"offer_url\": \"https://www.theparking.eu/used-cars-detail/vehicle/ad/CODU642L.html\",\n  \"source_site\": \"autoscout24.be\",\n  \"source_url\": \"https://www.autoscout24.be/fr/offres/-ac3cffef-3c79-4a97-b1e3-a08f3e776a00\",\n  \"source_url_redirect\": \"https://www.theparking.eu/tools/ETHCMOLB/0/F.html\",\n  \"status\": \"resolved\"\n}",
          "responseFields": [
            {
              "name": "source_url",
              "type": "string | null",
              "description": "The ad on the origin portal named by source_site. Null when it could not be resolved."
            },
            {
              "name": "source_url_redirect",
              "type": "string | null",
              "description": "The aggregator’s outbound tracker, which redirects to the origin ad. Use it whenever source_url is null."
            },
            {
              "name": "status",
              "type": "enum",
              "description": "resolved (source_url is set), expired (the portal no longer has the ad) or unresolved (the lookup did not answer this time)."
            }
          ],
          "note": "Already-resolved listings answer from storage; anything else is looked up during the request and remembered. With redirect=true a failed lookup falls back to the tracker link, and only a listing with neither answers 404. The legacy /api/theparking/resolve path is the same endpoint. Returns 503 while the dataset has not been built yet."
        },
        {
          "method": "GET",
          "path": "/api/classifieds/export",
          "summary": "Bulk export of the classifieds feed, up to 1,000 listings per request at any depth.",
          "scope": "classifieds-export",
          "locked": "Approved accounts only. Bulk export is its own permission, granted only to accounts we have approved: the classifieds scope does not include it, and any other key gets 403. Contact us to apply for access.",
          "params": [
            {
              "name": "after",
              "type": "integer",
              "example": "0",
              "source": "The previous page’s next_after.",
              "description": "Cursor. Omit (or send 0) for the first page, then pass each response’s next_after until it comes back null."
            },
            {
              "name": "limit",
              "type": "integer",
              "example": "1000",
              "source": "Page size chosen by your sync job.",
              "description": "Listings per page. Default and maximum 1000; larger values are lowered to 1000."
            },
            {
              "name": "fields",
              "type": "string",
              "example": "reference_id,brand,model,year,price_eur,source_url",
              "source": "Any of: reference_id, title, brand, model, engine, year, price_eur, mileage_km, fuel_norm, gearbox_norm, colour, doors, country, country_code, region, seller_type, source_site, photo_count, offer_url, source_url, image_url, published, first_seen_at, last_seen_at, source_url_redirect.",
              "description": "Comma-separated columns to return, in this order. Defaults to all of them. An unknown name is a 400 that lists the valid ones."
            },
            {
              "name": "country",
              "type": "string",
              "example": "de,at",
              "source": "/api/classifieds/facets -> facets.countries[].value",
              "description": "Comma-separated country codes, case-insensitive."
            },
            {
              "name": "brand / model",
              "type": "string",
              "example": "BMW / 3 Series",
              "source": "/api/classifieds/facets, /api/classifieds/models",
              "description": "Comma-separated, exact and case-sensitive as on /api/classifieds/listings."
            },
            {
              "name": "fuel / gearbox / seller",
              "type": "string",
              "example": "diesel / automatic / dealer",
              "source": "/api/classifieds/facets",
              "description": "Comma-separated normalized values, exact and case-sensitive."
            },
            {
              "name": "source / source_exclude",
              "type": "string",
              "example": "mobile.de",
              "source": "/api/classifieds/facets -> facets.sources[].value",
              "description": "Comma-separated origin portals to keep or leave out, case-insensitive."
            },
            {
              "name": "price_from / price_to",
              "type": "integer",
              "example": "5000 / 15000",
              "source": "EUR budget chosen by your user.",
              "description": "EUR bounds."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2016 / 2022",
              "source": "Year bounds chosen by your user.",
              "description": "Year bounds."
            },
            {
              "name": "kilometers_from / kilometers_to",
              "type": "integer",
              "example": "0 / 200000",
              "source": "Mileage bounds chosen by your user.",
              "description": "Mileage bounds."
            },
            {
              "name": "q",
              "type": "string",
              "example": "touring",
              "source": "Free text.",
              "description": "Substring match on title and description. Unindexed, so slow on a narrow match."
            },
            {
              "name": "with_photo",
              "type": "boolean",
              "example": "true",
              "source": "Your filter choice.",
              "description": "Only listings that carry a thumbnail."
            }
          ],
          "examples": [
            "/api/classifieds/export?country=de&fields=reference_id,brand,model,year,price_eur,source_url",
            "/api/classifieds/export?country=de&after=91744022"
          ],
          "response": "{\n  \"success\": true,\n  \"columns\": [\n    \"reference_id\",\n    \"brand\",\n    \"model\",\n    \"year\",\n    \"price_eur\",\n    \"source_url\"\n  ],\n  \"rows\": [\n    [\n      \"tp-91744022\",\n      \"BMW\",\n      \"3 Series\",\n      2016,\n      12500.5,\n      \"https://www.mobile.de/...\"\n    ],\n    [\n      \"tp-91744023\",\n      \"Audi\",\n      \"A4\",\n      2018,\n      17900,\n      null\n    ]\n  ],\n  \"count\": 2,\n  \"next_after\": null,\n  \"contract_version\": \"2026-10-05\"\n}",
          "responseFields": [
            {
              "name": "columns",
              "type": "string[]",
              "description": "The name of each position in a row — your fields, in your order."
            },
            {
              "name": "rows",
              "type": "array[]",
              "description": "One array per listing, values in columns order. Each key is sent once per page rather than once per listing, which keeps a 1,000-row page small."
            },
            {
              "name": "count",
              "type": "integer",
              "description": "Rows on this page."
            },
            {
              "name": "next_after",
              "type": "integer | null",
              "description": "Pass as after for the next page. null on the last page."
            }
          ],
          "note": "For syncing the feed rather than browsing it. Rows come in a fixed id order and paging is a cursor, so page 5,000 costs what page 1 does and the 5,000-row offset ceiling of /api/classifieds/listings does not apply. Listings added while you page appear at the end; a listing that ends drops out. Lighter by design: there is no total, and no origin URL is resolved during the request — source_url is whatever has been resolved so far, and source_url_redirect always reaches the ad. Takes the same filters as /api/classifieds/listings. Served with Cache-Control: public, max-age=60. Returns 503 while the dataset has not been built yet, or when a page times out."
        }
      ]
    },
    {
      "id": "market",
      "label": "Market intelligence",
      "description": "What a car is worth, and the price snapshots behind it. /api/market/valuation answers for a make and model worldwide and per country, from retail, auction, unsold and buy-now prices, with trends, time to sell and a price model’s estimate. The two snapshots cover a brand, model and year window — one for the Bulgarian retail market, one for our own auction inventory. Scope: market, granted per contract; none of the three is enabled on a new key by default. All three are read-only lookups into data built nightly or on the pipeline’s own schedule. On the two snapshots a 404 means \"no snapshot exists for that window\", the normal answer for a thin brand/model/year combination — not an error to retry — and neither snapshot route carries envelope metadata; read X-Request-ID from the header.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/market/valuation",
          "summary": "What a make and model is worth — retail, auction, unsold and buy-now prices, worldwide and per country.",
          "scope": "market",
          "params": [
            {
              "name": "brand / make",
              "type": "string",
              "example": "BMW",
              "source": "/api/brands -> brands[].name",
              "required": true,
              "description": "Brand name, under either spelling."
            },
            {
              "name": "model",
              "type": "string",
              "example": "320d",
              "source": "/api/models?brand=bmw -> models[].name",
              "required": true,
              "description": "Model name."
            },
            {
              "name": "year",
              "type": "integer",
              "example": "2019",
              "source": "The car being valued.",
              "description": "Exact registration year for the exact block; the range block widens it by year_flex."
            },
            {
              "name": "year_flex",
              "type": "integer",
              "example": "1",
              "source": "How wide a range you want.",
              "description": "Years either side of year for the range block. Default 1, maximum 10."
            },
            {
              "name": "year_from / year_to",
              "type": "integer",
              "example": "2018 / 2021",
              "source": "Year bounds chosen by your user.",
              "description": "Range bounds when year is not given."
            },
            {
              "name": "fuel",
              "type": "string",
              "example": "Diesel",
              "source": "Petrol, Diesel, Hybrid, Electric, LPG, CNG or Hydrogen.",
              "description": "Plug-in and mild hybrids count as Hybrid."
            },
            {
              "name": "gearbox",
              "type": "string",
              "example": "Automatic",
              "source": "Automatic or Manual.",
              "description": "Gearbox."
            },
            {
              "name": "engine",
              "type": "number",
              "example": "2.0",
              "source": "The car being valued.",
              "description": "Displacement in litres."
            },
            {
              "name": "engine_flex",
              "type": "number",
              "example": "0.2",
              "source": "How wide a range you want.",
              "description": "Litres either side of engine for the range block. Default 0.2."
            },
            {
              "name": "condition",
              "type": "enum",
              "example": "normal",
              "source": "normal, damaged or any.",
              "description": "normal (default) excludes damaged lots, damaged keeps only them, any pools both. Classifieds are always normal."
            },
            {
              "name": "mileage",
              "type": "integer",
              "example": "120000",
              "source": "The car’s odometer in km.",
              "description": "Adds mileage_adjusted_eur — the median moved along the group’s own price/mileage slope, capped at ±35% — and sets the mileage of prediction."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "ISO 3166-1 alpha-2 country of the car.",
              "description": "Narrows exact, range, trend and time_to_sell. worldwide and countries always cover every country."
            },
            {
              "name": "kinds",
              "type": "string",
              "example": "retail,auction",
              "source": "retail, auction, buy_now, unsold.",
              "description": "Comma-separated subset of price kinds to return."
            },
            {
              "name": "trend_months",
              "type": "integer",
              "example": "12",
              "source": "How much history to chart.",
              "description": "Months of trend history including the current one. Default and maximum 12."
            },
            {
              "name": "trend_weeks",
              "type": "integer",
              "example": "12",
              "source": "How much history to chart.",
              "description": "Weeks of weekly trend history. Default 12, maximum 26."
            }
          ],
          "examples": [
            "/api/market/valuation?brand=BMW&model=320d&year=2019&fuel=Diesel&mileage=120000",
            "/api/market/valuation?brand=Toyota&model=Corolla&year_from=2018&year_to=2021&country=DE&kinds=retail,auction"
          ],
          "response": "{\n  \"success\": true,\n  \"valuation\": {\n    \"vehicle\": {\n      \"brand\": \"BMW\",\n      \"model\": \"320d\",\n      \"year\": 2019,\n      \"year_from\": 2018,\n      \"year_to\": 2020,\n      \"fuel\": \"Diesel\",\n      \"condition\": \"normal\",\n      \"mileage_km\": 120000,\n      \"country\": null\n    },\n    \"exact\": {\n      \"retail\": {\n        \"count\": 640,\n        \"median_eur\": 21900,\n        \"p25_eur\": 19800,\n        \"p75_eur\": 24100,\n        \"p10_eur\": 17500,\n        \"p90_eur\": 26900,\n        \"mean_mileage_km\": 98000\n      },\n      \"auction\": {\n        \"count\": 212,\n        \"median_eur\": 16400,\n        \"p25_eur\": 14100,\n        \"p75_eur\": 18900,\n        \"p10_eur\": 12200,\n        \"p90_eur\": 21000,\n        \"mean_mileage_km\": 131000\n      }\n    },\n    \"range\": {},\n    \"worldwide\": {},\n    \"countries\": [\n      {\n        \"country_code\": \"de\",\n        \"retail\": {\n          \"count\": 410,\n          \"median_eur\": 21200\n        },\n        \"auction\": {\n          \"count\": 96,\n          \"median_eur\": 16100\n        },\n        \"count\": 506,\n        \"time_to_sell\": null\n      }\n    ],\n    \"ranking\": {\n      \"retail\": {\n        \"cheapest\": [\n          {\n            \"country_code\": \"pl\",\n            \"count\": 88,\n            \"median_eur\": 18900\n          }\n        ],\n        \"most_expensive\": [\n          {\n            \"country_code\": \"ch\",\n            \"count\": 41,\n            \"median_eur\": 27400\n          }\n        ]\n      }\n    },\n    \"trend\": {},\n    \"time_to_sell\": null,\n    \"price_bands\": {},\n    \"prediction\": {\n      \"estimates\": {},\n      \"accuracy\": {}\n    },\n    \"methodology\": {},\n    \"built\": null\n  },\n  \"contract_version\": \"2026-10-05\"\n}",
          "responseFields": [
            {
              "name": "exact / range",
              "type": "object",
              "description": "Per price kind, a summary of count, median_eur, p10/p25/p75/p90_eur and mean_mileage_km (plus mileage_adjusted_eur when mileage is sent), or null when no car matched. Kinds: retail (classified asking prices in Europe, Korea and China), auction (closing price of finished lots — the published result where the source has one, else the last bid seen), unsold (closing prices the car did not sell at) and buy_now (B2B buy-now prices). exact applies year and engine as given; range widens them."
            },
            {
              "name": "worldwide / countries / ranking",
              "type": "object",
              "description": "worldwide is the same summary across every country; countries[] has count and median per kind for each country_code; ranking.<kind> lists the cheapest and most_expensive markets (countries with at least 5 observations)."
            },
            {
              "name": "trend",
              "type": "object",
              "description": "Per kind: weekly and change_pct (latest four weeks against the four before), monthly and monthly_change_pct (latest three months against the three before)."
            },
            {
              "name": "time_to_sell",
              "type": "object | null",
              "description": "Overall and per country, for retail and auction: Kaplan-Meier median days to sell (median_days, or over_days while over half are unsold), sold_within_pct within sold_within_days, sold and listings. Retail measures until a classified left the market, which is not proof of a sale. Null below 5 sales."
            },
            {
              "name": "price_bands",
              "type": "object",
              "description": "For retail and auction, five bands around that kind’s median (under -15%, -15..-5%, ±5%, +5..+15%, over +15%) with the time to sell at that price, and for auctions closes and sold_pct."
            },
            {
              "name": "prediction",
              "type": "object",
              "description": "A price model’s estimate, available even where few or no cars match: estimates.<kind> with estimate_eur, range_50_eur and range_80_eur (where an individual car’s price falls), basis (model, blended or comparables), per_year_older_pct, per_10k_km_pct, by_mileage and by_year. accuracy.<kind> reports held-out test error."
            },
            {
              "name": "built",
              "type": "object | null",
              "description": "When the nightly build that answered this ran."
            }
          ],
          "note": "Granted per contract — not on a new key by default. Built nightly from the market_valuation tables, so repeated calls for the same car return the same figures until the next build. 404 means the brand or model is not in the catalogue; a thin model answers with small counts and a prediction rather than an error. Before the first monthly build, monthly arrays are empty and time-to-sell values are null."
        },
        {
          "method": "GET",
          "path": "/api/cars-bg-market",
          "summary": "Cars.bg Bulgarian retail market snapshot.",
          "scope": "market",
          "params": [
            {
              "name": "brand / make",
              "type": "string",
              "example": "BMW",
              "source": "/api/brands -> brands[].name",
              "required": true,
              "description": "Brand name. Either spelling. Resolved through a fuzzy brand matcher, so display names work."
            },
            {
              "name": "model",
              "type": "string",
              "example": "320d",
              "source": "/api/models?brand=bmw -> models[].name",
              "required": true,
              "description": "Model name. Resolved to one or more catalogue model ids."
            },
            {
              "name": "year",
              "type": "integer",
              "example": "2019",
              "source": "Centre year chosen by your application.",
              "required": true,
              "description": "The centre year."
            },
            {
              "name": "flex",
              "type": "integer",
              "example": "2",
              "source": "Tolerance chosen by your application.",
              "description": "Year tolerance ± (0–10, default 1). A value outside that range is a 400."
            }
          ],
          "examples": [
            "/api/cars-bg-market?brand=BMW&model=320d&year=2019",
            "/api/cars-bg-market?make=Audi&model=A4&year=2020&flex=2"
          ],
          "response": "{\n  \"success\": true,\n  \"snapshot\": {\n    \"brand_id\": \"12\",\n    \"model_ids\": [\n      \"4411\"\n    ],\n    \"year_center\": 2019,\n    \"year_flex\": 2,\n    \"year_from\": 2017,\n    \"year_to\": 2021,\n    \"listing_count\": 84,\n    \"avg_price_eur\": 24310.5,\n    \"median_price_eur\": 23900,\n    \"min_price_eur\": 15500,\n    \"max_price_eur\": 41000,\n    \"offers\": [],\n    \"raw_matches\": [],\n    \"computed_at\": \"2026-08-27T02:14:11\",\n    \"updated_at\": \"2026-08-27T02:14:11\"\n  }\n}",
          "responseFields": [
            {
              "name": "year_from / year_to",
              "type": "integer",
              "description": "The window year ± flex actually resolved to."
            },
            {
              "name": "listing_count",
              "type": "integer",
              "description": "How many retail listings the aggregate is built from. Read this first — a median over four cars is not a market price."
            },
            {
              "name": "avg_price_eur / median_price_eur / min_price_eur / max_price_eur",
              "type": "number",
              "description": "EUR aggregates as JSON numbers. Prefer the median; retail asking prices have a long right tail."
            },
            {
              "name": "offers",
              "type": "object[]",
              "description": "The listings behind the aggregate."
            },
            {
              "name": "raw_matches",
              "type": "object[]",
              "description": "Everything the model matcher considered, including rows the aggregate excluded. Useful for auditing a surprising number."
            },
            {
              "name": "computed_at / updated_at",
              "type": "timestamp",
              "description": "When the snapshot was built. It is not recomputed per request."
            }
          ],
          "note": "Cars.bg (Bulgarian retail market) snapshot: what cars of this specification are asked for on the Bulgarian classifieds market. This is the reference the top-offers verdicts are ultimately measured against. brand/model/year missing → 400. flex outside 0–10 → 400. Brand or model that the matcher cannot resolve → 404 (Unknown brand / Unknown model). Resolved fine but no snapshot built for that window → 404. The market dataset being unavailable altogether → 503. No envelope metadata — read X-Request-ID from the header."
        },
        {
          "method": "GET",
          "path": "/api/auction-market",
          "summary": "Auction-market price snapshot for the same brand, model, and year window.",
          "scope": "market",
          "params": [
            {
              "name": "brand / make",
              "type": "string",
              "example": "BMW",
              "source": "/api/brands -> brands[].name",
              "required": true,
              "description": "Brand name. Matched against the normalized clean_make."
            },
            {
              "name": "model",
              "type": "string",
              "example": "320d",
              "source": "/api/models?brand=bmw -> models[].name",
              "required": true,
              "description": "Model name. Matched against clean_model."
            },
            {
              "name": "year",
              "type": "integer",
              "example": "2019",
              "source": "Centre year chosen by your application.",
              "required": true,
              "description": "The centre year."
            },
            {
              "name": "flex",
              "type": "integer",
              "example": "2",
              "source": "Tolerance chosen by your application.",
              "description": "Year tolerance ± (0–10, default 1)."
            },
            {
              "name": "scope",
              "type": "enum",
              "example": "active",
              "source": "Choose all, active, or inactive.",
              "description": "Default all. Anything else is a 400. Each is a separately built snapshot, so active can exist while inactive does not."
            }
          ],
          "examples": [
            "/api/auction-market?brand=BMW&model=320d&year=2019&scope=active",
            "/api/auction-market?brand=Kia&model=EV6&year=2023&flex=1"
          ],
          "response": "{\n  \"success\": true,\n  \"snapshot\": {\n    \"clean_make\": \"BMW\",\n    \"clean_model\": \"320d\",\n    \"year_center\": 2019,\n    \"year_flex\": 1,\n    \"year_from\": 2018,\n    \"year_to\": 2020,\n    \"scope\": \"all\",\n    \"auction_count\": 412,\n    \"avg_price_eur\": 18220.4,\n    \"median_price_eur\": 17800,\n    \"p10_price_eur\": 11900,\n    \"p25_price_eur\": 14750,\n    \"p75_price_eur\": 21400,\n    \"p90_price_eur\": 26100,\n    \"min_price_eur\": 6200,\n    \"max_price_eur\": 44900,\n    \"first_seen_min\": \"2026-01-04T00:00:00\",\n    \"first_seen_max\": \"2026-08-22T00:00:00\",\n    \"auctions\": [],\n    \"computed_at\": \"2026-08-27T02:41:03\",\n    \"updated_at\": \"2026-08-27T02:41:03\"\n  }\n}",
          "responseFields": [
            {
              "name": "scope",
              "type": "enum",
              "description": "Echoes the request: active restricts to lots still open, inactive to closed ones, all covers both."
            },
            {
              "name": "auction_count",
              "type": "integer",
              "description": "Lots behind the aggregate. Sanity-check it before quoting a percentile."
            },
            {
              "name": "p10_price_eur / p25_price_eur / p75_price_eur / p90_price_eur",
              "type": "number",
              "description": "Percentiles — the reason to prefer this over the Cars.bg snapshot when you want a spread rather than a point estimate. A wide p10–p90 band means the specification is not pinning the price down."
            },
            {
              "name": "first_seen_min / first_seen_max",
              "type": "timestamp",
              "description": "The observation window the lots span. A snapshot whose window is eight months wide is not a spot price."
            },
            {
              "name": "auctions",
              "type": "object[]",
              "description": "The individual lots the aggregate was computed from."
            }
          ],
          "note": "The same question asked of auction inventory rather than retail: what lots of this specification have cost at auction. Missing brand/model/year → 400; flex outside 0–10 → 400; invalid scope → 400; no snapshot for that window → 404. Retail and auction prices are not comparable line for line — the Cars.bg figure is a Bulgarian asking price including everything a retail seller bundles; the auction figure is a lot price before fees, transport, duty and VAT. Put the auction figure through the import calculator before setting the two side by side. No envelope metadata — read X-Request-ID from the header."
        }
      ]
    },
    {
      "id": "ships",
      "label": "Ship tracking",
      "description": "Find any vessel and follow its position, voyage and route, and register your own shipments by tracking, B/L or container number so their vessel is followed for you. Scope: ships, granted per contract — not on a new key by default. Shipments are scoped to the key that registered them.",
      "guide": {
        "title": "Following a shipment",
        "steps": [
          "Register it with POST /api/shipments, giving the vessel as mmsi, imo or vessel_name plus whichever of your own numbers you want to search by later. A name is resolved to an MMSI; vessel_resolved says whether that worked.",
          "Read GET /api/shipments/{shipment_id} for the shipment, its vessel exactly as /api/vessels/{mmsi} returns it, and the route since you registered it.",
          "Mark it arrived with PATCH (status: arrived) or stop following it with DELETE. Either way it stays readable."
        ]
      },
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/vessels/search",
          "summary": "Find vessels by name, MMSI, IMO or call sign.",
          "scope": "ships",
          "params": [
            {
              "name": "q",
              "type": "string",
              "example": "MSC OSCAR",
              "source": "What your user typed.",
              "required": true,
              "description": "Exact MMSI (9 digits), IMO (7 digits, IMO prefix optional), call sign, or any part of the name. At least 3 characters."
            },
            {
              "name": "limit",
              "type": "integer",
              "example": "20",
              "source": "How many results you show.",
              "description": "Default 20, maximum 100."
            },
            {
              "name": "online",
              "type": "boolean",
              "example": "false",
              "source": "Your latency choice.",
              "description": "Default true: when nothing is known and q is a name, the exact name is looked up on a public vessel directory and remembered (source: vesselfinder). false answers from what we already know."
            }
          ],
          "examples": [
            "/api/vessels/search?q=MSC%20OSCAR",
            "/api/vessels/search?q=9703291"
          ],
          "response": "{\n  \"success\": true,\n  \"source\": \"known\",\n  \"vessels\": [\n    {\n      \"mmsi\": \"355906000\",\n      \"imo\": \"9703291\",\n      \"name\": \"MSC OSCAR\",\n      \"call_sign\": \"3FOB4\",\n      \"ship_type\": \"cargo\",\n      \"length_m\": 395,\n      \"width_m\": 59,\n      \"destination\": \"ROTTERDAM\",\n      \"last_position\": {\n        \"latitude\": 51.95,\n        \"longitude\": 4.05,\n        \"speed_knots\": 0.1,\n        \"time\": \"2026-10-18T09:12:00Z\"\n      }\n    }\n  ],\n  \"contract_version\": \"2026-10-05\"\n}",
          "responseFields": [
            {
              "name": "source",
              "type": "enum | null",
              "description": "known (from our AIS feed and earlier look-ups) or vesselfinder (found by the online look-up just now). null when nothing matched."
            },
            {
              "name": "vessels[]",
              "type": "object[]",
              "description": "mmsi, imo, name, call_sign, ship_type, length_m, width_m, destination and last_position { latitude, longitude, speed_knots, time } or null."
            }
          ]
        },
        {
          "method": "GET",
          "path": "/api/vessels/{mmsi}",
          "summary": "Everything known about one vessel: identity, declared voyage, latest position and how much track we hold.",
          "scope": "ships",
          "params": [
            {
              "name": "mmsi",
              "type": "string",
              "location": "path",
              "example": "355906000",
              "source": "/api/vessels/search -> vessels[].mmsi",
              "required": true,
              "description": "The vessel’s 9-digit MMSI."
            },
            {
              "name": "refresh",
              "type": "boolean",
              "example": "false",
              "source": "Your latency choice.",
              "description": "Default true: when our latest fix is older than 30 minutes it is refreshed from a public AIS source during the request. false answers from what is stored."
            }
          ],
          "examples": [
            "/api/vessels/355906000",
            "/api/vessels/355906000?refresh=false"
          ],
          "response": "{\n  \"success\": true,\n  \"vessel\": {\n    \"mmsi\": \"355906000\",\n    \"imo\": \"9703291\",\n    \"name\": \"MSC OSCAR\",\n    \"call_sign\": \"3FOB4\",\n    \"ship_type\": \"cargo\",\n    \"length_m\": 395,\n    \"width_m\": 59,\n    \"max_draught_m\": 16,\n    \"declared_destination\": \"ROTTERDAM\",\n    \"declared_eta\": \"2026-10-19T06:00:00Z\",\n    \"position\": {\n      \"latitude\": 51.95,\n      \"longitude\": 4.05,\n      \"speed_knots\": 0.1,\n      \"course\": 182,\n      \"heading\": 180,\n      \"nav_status\": \"moored\",\n      \"area\": \"North Sea\",\n      \"time\": \"2026-10-18T09:12:00Z\",\n      \"source\": \"AISStream\"\n    },\n    \"voyage\": {\n      \"departure_port\": \"Algeciras\",\n      \"departed_at\": \"2026-10-14T21:30:00Z\",\n      \"progress_pct\": 96\n    },\n    \"track\": {\n      \"points\": 1840,\n      \"first\": \"2026-08-01T00:04:00Z\",\n      \"last\": \"2026-10-18T09:12:00Z\"\n    },\n    \"tracked_live\": true\n  },\n  \"contract_version\": \"2026-10-05\"\n}",
          "responseFields": [
            {
              "name": "position",
              "type": "object | null",
              "description": "Latest fix: latitude, longitude, speed_knots, course, heading, nav_status, area, time and source."
            },
            {
              "name": "voyage",
              "type": "object",
              "description": "departure_port, departed_at and progress_pct where known."
            },
            {
              "name": "track",
              "type": "object",
              "description": "How many track points we hold and the first and last of them. Read them with /api/vessels/{mmsi}/track."
            },
            {
              "name": "tracked_live",
              "type": "boolean",
              "description": "Whether the vessel is in the live AIS subscription right now."
            }
          ],
          "note": "A request can reach upstream when the stored fix is stale, so it can take a few seconds; pass refresh=false where freshness does not matter. 404 when no vessel with that MMSI is known or could be found."
        },
        {
          "method": "GET",
          "path": "/api/vessels/{mmsi}/track",
          "summary": "The vessel’s recorded route, oldest first.",
          "scope": "ships",
          "params": [
            {
              "name": "mmsi",
              "type": "string",
              "location": "path",
              "example": "355906000",
              "source": "/api/vessels/search -> vessels[].mmsi",
              "required": true,
              "description": "The vessel’s 9-digit MMSI."
            },
            {
              "name": "since",
              "type": "string",
              "example": "2026-10-01",
              "source": "The start of the window you draw.",
              "description": "ISO 8601 date or datetime."
            },
            {
              "name": "limit",
              "type": "integer",
              "example": "500",
              "source": "How many points your map can draw.",
              "description": "The track is thinned evenly to at most this many points. Default 500, maximum 5000."
            }
          ],
          "examples": [
            "/api/vessels/355906000/track?since=2026-10-01&limit=500"
          ],
          "response": "{\n  \"success\": true,\n  \"mmsi\": \"355906000\",\n  \"track\": [\n    {\n      \"latitude\": 36.1,\n      \"longitude\": -5.4,\n      \"speed_knots\": 14.2,\n      \"course\": 12,\n      \"time\": \"2026-10-14T22:00:00Z\"\n    }\n  ],\n  \"contract_version\": \"2026-10-05\"\n}"
        },
        {
          "method": "GET",
          "path": "/api/shipments",
          "summary": "The calling key’s shipments, newest first.",
          "scope": "ships",
          "params": [
            {
              "name": "q",
              "type": "string",
              "example": "MSCU1234567",
              "source": "Any number you registered.",
              "description": "Matches the reference, tracking / B/L / container / booking number, vessel name, MMSI or IMO, ignoring case and punctuation."
            },
            {
              "name": "status",
              "type": "enum",
              "example": "active",
              "source": "active, arrived or cancelled.",
              "description": "Only shipments in this state."
            },
            {
              "name": "limit / offset",
              "type": "integer",
              "example": "50 / 0",
              "source": "Pagination state in your application.",
              "description": "Page size and row offset."
            }
          ],
          "examples": [
            "/api/shipments?status=active",
            "/api/shipments?q=MSCU1234567"
          ],
          "response": "{\n  \"success\": true,\n  \"shipments\": [\n    {\n      \"id\": 42,\n      \"reference\": \"PO-1189\",\n      \"container_number\": \"MSCU1234567\",\n      \"vessel_name\": \"MSC OSCAR\",\n      \"mmsi\": \"355906000\",\n      \"imo\": \"9703291\",\n      \"destination_port\": \"Varna\",\n      \"eta\": \"2026-11-02\",\n      \"status\": \"active\",\n      \"vessel_resolved\": true,\n      \"created_at\": \"2026-10-12T08:00:00Z\"\n    }\n  ],\n  \"total\": 1,\n  \"limit\": 50,\n  \"offset\": 0,\n  \"contract_version\": \"2026-10-05\"\n}",
          "note": "Shipments are scoped to the key that created them: another key, even on the same account, does not see them."
        },
        {
          "method": "POST",
          "path": "/api/shipments",
          "summary": "Register a shipment so its vessel is followed.",
          "scope": "ships",
          "params": [
            {
              "name": "mmsi / imo / vessel_name",
              "type": "string",
              "example": "355906000",
              "source": "/api/vessels/search, or your booking paperwork.",
              "description": "The vessel, given any one way. A name or IMO is resolved to an MMSI; vessel_resolved in the response says whether that worked."
            },
            {
              "name": "reference",
              "type": "string",
              "example": "PO-1189",
              "source": "Your own reference.",
              "description": "Free text to search by later."
            },
            {
              "name": "tracking_number / bl_number / container_number / booking_number",
              "type": "string",
              "example": "MSCU1234567",
              "source": "Your shipping documents.",
              "description": "Numbers to search by later. Container numbers are upper-cased and stripped of punctuation."
            },
            {
              "name": "carrier",
              "type": "string",
              "example": "MSC",
              "source": "Your shipping documents.",
              "description": "The shipping line."
            },
            {
              "name": "origin_port / destination_port",
              "type": "string",
              "example": "Antwerp / Varna",
              "source": "Your shipping documents.",
              "description": "Free-text ports."
            },
            {
              "name": "eta",
              "type": "string",
              "example": "2026-11-02",
              "source": "Your shipping documents.",
              "description": "Expected arrival, ISO date. The vessel is followed until 14 days after it (90 days after creation without one)."
            },
            {
              "name": "notes",
              "type": "string",
              "example": "2 cars, BMW 320d and Audi A4",
              "source": "Anything you want to keep with it.",
              "description": "Free text."
            }
          ],
          "examples": [
            "POST /api/shipments\n{\"reference\": \"PO-1189\", \"container_number\": \"MSCU1234567\", \"vessel_name\": \"MSC OSCAR\", \"destination_port\": \"Varna\", \"eta\": \"2026-11-02\"}"
          ],
          "response": "{\n  \"success\": true,\n  \"shipment\": {\n    \"id\": 42,\n    \"reference\": \"PO-1189\",\n    \"container_number\": \"MSCU1234567\",\n    \"vessel_name\": \"MSC OSCAR\",\n    \"mmsi\": \"355906000\",\n    \"status\": \"active\",\n    \"vessel_resolved\": true\n  },\n  \"contract_version\": \"2026-10-05\"\n}",
          "note": "Answers 201. Active shipments are followed by the live AIS feed within a system-wide 50-vessel subscription that our own shipments fill first. At most 25 active shipments per key — a 26th is a 409."
        },
        {
          "method": "GET",
          "path": "/api/shipments/{shipment_id}",
          "summary": "One shipment with its vessel and its route since registration.",
          "scope": "ships",
          "params": [
            {
              "name": "shipment_id",
              "type": "integer",
              "location": "path",
              "example": "42",
              "source": "/api/shipments -> shipments[].id",
              "required": true,
              "description": "The shipment id."
            },
            {
              "name": "track_limit",
              "type": "integer",
              "example": "500",
              "source": "How many points your map can draw.",
              "description": "Track points, thinned evenly. Default 500, maximum 5000."
            }
          ],
          "examples": [
            "/api/shipments/42"
          ],
          "response": "{\n  \"success\": true,\n  \"shipment\": {\n    \"id\": 42,\n    \"reference\": \"PO-1189\",\n    \"status\": \"active\",\n    \"mmsi\": \"355906000\",\n    \"vessel\": {\n      \"mmsi\": \"355906000\",\n      \"name\": \"MSC OSCAR\",\n      \"position\": {\n        \"latitude\": 51.95,\n        \"longitude\": 4.05\n      }\n    },\n    \"track\": []\n  },\n  \"contract_version\": \"2026-10-05\"\n}",
          "note": "404 when the shipment does not exist or belongs to another key."
        },
        {
          "method": "PATCH",
          "path": "/api/shipments/{shipment_id}",
          "summary": "Change a shipment.",
          "scope": "ships",
          "params": [
            {
              "name": "shipment_id",
              "type": "integer",
              "location": "path",
              "example": "42",
              "source": "/api/shipments -> shipments[].id",
              "required": true,
              "description": "The shipment id."
            },
            {
              "name": "status",
              "type": "enum",
              "example": "arrived",
              "source": "active, arrived or cancelled.",
              "description": "The new state."
            },
            {
              "name": "any create field",
              "type": "string",
              "example": "eta",
              "source": "POST /api/shipments body fields.",
              "description": "Any field from the create body. A new vessel name or IMO re-resolves the MMSI."
            }
          ],
          "examples": [
            "PATCH /api/shipments/42\n{\"status\": \"arrived\"}"
          ],
          "response": "{\n  \"success\": true,\n  \"shipment\": {\n    \"id\": 42,\n    \"status\": \"arrived\"\n  },\n  \"contract_version\": \"2026-10-05\"\n}"
        },
        {
          "method": "DELETE",
          "path": "/api/shipments/{shipment_id}",
          "summary": "Stop following a shipment.",
          "scope": "ships",
          "params": [
            {
              "name": "shipment_id",
              "type": "integer",
              "location": "path",
              "example": "42",
              "source": "/api/shipments -> shipments[].id",
              "required": true,
              "description": "The shipment id."
            }
          ],
          "examples": [
            "DELETE /api/shipments/42"
          ],
          "response": "{\n  \"success\": true,\n  \"shipment\": {\n    \"id\": 42,\n    \"status\": \"cancelled\"\n  },\n  \"contract_version\": \"2026-10-05\"\n}",
          "note": "Marks it cancelled; it stays readable."
        }
      ]
    },
    {
      "id": "watches",
      "label": "Vehicle watches",
      "description": "Saved vehicle searches matched against every auction source your key can use and, if it has classifieds, the European classifieds — scanned every 10 minutes, with new matches delivered by polling or a signed webhook. Scope: watches. Granted per contract — not on a new key by default. Up to 10 criteria per watch and 25 active watches per key.",
      "guide": {
        "title": "From a watch to a match",
        "steps": [
          "Create the watch with POST /api/watches and keep the webhook_secret from the response — it is shown only here and when rotated.",
          "The first scan records what already matches, flagged initial: true. Later scans add only cars listed since.",
          "Either poll GET /api/watches/{watch_id}/matches with after_id set to the last next_after_id you saw, or receive each batch of new matches on your webhook_url.",
          "Verify every webhook delivery: X-Watch-Signature is sha256=<hex HMAC-SHA256 of \"<X-Watch-Timestamp>.<raw body>\"> under your webhook_secret."
        ]
      },
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/watches",
          "summary": "The calling key’s watches, with their match counts.",
          "scope": "watches",
          "examples": [
            "/api/watches"
          ],
          "response": "{\n  \"success\": true,\n  \"watches\": [\n    {\n      \"id\": 7,\n      \"name\": \"Diesel 3 Series under 20k\",\n      \"criteria\": [\n        {\n          \"make\": \"BMW\",\n          \"model\": \"3 Series\",\n          \"fuel\": \"Diesel\",\n          \"year_from\": 2018,\n          \"max_price\": 20000\n        }\n      ],\n      \"include_classifieds\": true,\n      \"webhook_url\": \"https://example.com/hooks/watch\",\n      \"webhook_enabled\": true,\n      \"active\": true,\n      \"match_count\": 132\n    }\n  ],\n  \"contract_version\": \"2026-10-05\"\n}"
        },
        {
          "method": "POST",
          "path": "/api/watches",
          "summary": "Save a vehicle configuration to keep searching for.",
          "scope": "watches",
          "params": [
            {
              "name": "criteria",
              "type": "object | object[]",
              "example": "{\"make\": \"BMW\", \"model\": \"3 Series\", \"fuel\": \"Diesel\", \"year_from\": 2018, \"max_price\": 20000}",
              "source": "/api/brands, /api/models, /api/fuels, /api/gearboxes",
              "required": true,
              "description": "One criterion or a list of up to 10; a car matching any of them matches the watch. Each needs make, and may give model, fuel, gearbox, country (ISO or europe), year_from / year_to, kilometers_from / kilometers_to, horsepower_from / horsepower_to (auctions only) and price_from / max_price."
            },
            {
              "name": "name",
              "type": "string",
              "example": "Diesel 3 Series under 20k",
              "source": "Your label.",
              "description": "Free text."
            },
            {
              "name": "include_classifieds",
              "type": "boolean",
              "example": "true",
              "source": "Your choice.",
              "description": "Default true. Also scan the European classifieds — only when the key has the classifieds scope."
            },
            {
              "name": "webhook_url",
              "type": "string",
              "example": "https://example.com/hooks/watch",
              "source": "Your receiver.",
              "description": "https on a public address. Each batch of new matches is POSTed here, signed."
            }
          ],
          "examples": [
            "POST /api/watches\n{\"name\": \"Diesel 3 Series under 20k\", \"criteria\": [{\"make\": \"BMW\", \"model\": \"3 Series\", \"fuel\": \"Diesel\", \"year_from\": 2018, \"max_price\": 20000}], \"webhook_url\": \"https://example.com/hooks/watch\"}"
          ],
          "response": "{\n  \"success\": true,\n  \"watch\": {\n    \"id\": 7,\n    \"name\": \"Diesel 3 Series under 20k\",\n    \"criteria\": [\n      {\n        \"make\": \"BMW\",\n        \"model\": \"3 Series\",\n        \"fuel\": \"Diesel\",\n        \"year_from\": 2018,\n        \"max_price\": 20000\n      }\n    ],\n    \"include_classifieds\": true,\n    \"webhook_url\": \"https://example.com/hooks/watch\",\n    \"webhook_enabled\": true,\n    \"webhook_secret\": \"3f9c…\",\n    \"active\": true\n  },\n  \"contract_version\": \"2026-10-05\"\n}",
          "note": "Answers 201. price_from and max_price compare against the landed-in-Bulgaria price, which includes duty and import VAT for cars outside the EU; every match also carries the source price. A webhook that fails 20 times in a row is switched off; matches keep accumulating for polling. A 26th active watch is a 409."
        },
        {
          "method": "GET",
          "path": "/api/watches/{watch_id}",
          "summary": "One watch.",
          "scope": "watches",
          "params": [
            {
              "name": "watch_id",
              "type": "integer",
              "location": "path",
              "example": "7",
              "source": "/api/watches -> watches[].id",
              "required": true,
              "description": "The watch id."
            }
          ],
          "examples": [
            "/api/watches/7"
          ],
          "response": "{\n  \"success\": true,\n  \"watch\": {\n    \"id\": 7,\n    \"name\": \"Diesel 3 Series under 20k\",\n    \"active\": true,\n    \"match_count\": 132\n  },\n  \"contract_version\": \"2026-10-05\"\n}"
        },
        {
          "method": "PATCH",
          "path": "/api/watches/{watch_id}",
          "summary": "Change a watch.",
          "scope": "watches",
          "params": [
            {
              "name": "watch_id",
              "type": "integer",
              "location": "path",
              "example": "7",
              "source": "/api/watches -> watches[].id",
              "required": true,
              "description": "The watch id."
            },
            {
              "name": "name / criteria / include_classifieds",
              "type": "string | object[] | boolean",
              "example": "criteria",
              "source": "As on create.",
              "description": "New criteria restart the baseline: the next scan records what matches then as initial."
            },
            {
              "name": "webhook_url / webhook_enabled",
              "type": "string | boolean",
              "example": "\"\"",
              "source": "Your receiver.",
              "description": "An empty webhook_url removes it."
            },
            {
              "name": "active",
              "type": "boolean",
              "example": "false",
              "source": "Your choice.",
              "description": "Pause or resume scanning."
            },
            {
              "name": "rotate_secret",
              "type": "boolean",
              "example": "true",
              "source": "Your choice.",
              "description": "true issues a new webhook_secret, returned in this response only."
            }
          ],
          "examples": [
            "PATCH /api/watches/7\n{\"active\": false}",
            "PATCH /api/watches/7\n{\"rotate_secret\": true}"
          ],
          "response": "{\n  \"success\": true,\n  \"watch\": {\n    \"id\": 7,\n    \"active\": false\n  },\n  \"contract_version\": \"2026-10-05\"\n}"
        },
        {
          "method": "DELETE",
          "path": "/api/watches/{watch_id}",
          "summary": "Delete a watch and its matches.",
          "scope": "watches",
          "params": [
            {
              "name": "watch_id",
              "type": "integer",
              "location": "path",
              "example": "7",
              "source": "/api/watches -> watches[].id",
              "required": true,
              "description": "The watch id."
            }
          ],
          "examples": [
            "DELETE /api/watches/7"
          ],
          "response": "{\n  \"success\": true,\n  \"contract_version\": \"2026-10-05\"\n}"
        },
        {
          "method": "GET",
          "path": "/api/watches/{watch_id}/matches",
          "summary": "Matches found for a watch, in the order they were found.",
          "scope": "watches",
          "params": [
            {
              "name": "watch_id",
              "type": "integer",
              "location": "path",
              "example": "7",
              "source": "/api/watches -> watches[].id",
              "required": true,
              "description": "The watch id."
            },
            {
              "name": "after_id",
              "type": "integer",
              "example": "0",
              "source": "The last next_after_id you received.",
              "description": "Stable cursor, so nothing is missed or repeated. Default 0."
            },
            {
              "name": "include_initial",
              "type": "boolean",
              "example": "false",
              "source": "Your choice.",
              "description": "Default true. false skips what already matched when the watch was created."
            },
            {
              "name": "source",
              "type": "enum",
              "example": "auction",
              "source": "auction or classified.",
              "description": "Only matches from one side."
            },
            {
              "name": "limit",
              "type": "integer",
              "example": "100",
              "source": "Page size.",
              "description": "Default 100, maximum 500."
            }
          ],
          "examples": [
            "/api/watches/7/matches?after_id=0",
            "/api/watches/7/matches?after_id=1880&include_initial=false"
          ],
          "response": "{\n  \"success\": true,\n  \"matches\": [\n    {\n      \"id\": 1881,\n      \"source\": \"auction\",\n      \"initial\": false,\n      \"matched_at\": \"2026-10-18T09:20:00Z\",\n      \"vehicle\": {\n        \"site\": \"openlane\",\n        \"auction_id\": 11409652,\n        \"title\": \"BMW 320d Touring\",\n        \"price_eur\": 16900,\n        \"landed_price_eur\": 18450,\n        \"api_url\": \"/api/auction/openlane/11409652\"\n      }\n    }\n  ],\n  \"next_after_id\": 1881,\n  \"has_more\": false,\n  \"contract_version\": \"2026-10-05\"\n}",
          "responseFields": [
            {
              "name": "matches[].vehicle",
              "type": "object",
              "description": "The car as it looked when found, with api_url (auctions) or url (classifieds, the original ad) to read it now."
            },
            {
              "name": "next_after_id / has_more",
              "type": "integer / boolean",
              "description": "Send next_after_id as after_id next time; has_more says whether to ask again straight away."
            }
          ]
        }
      ]
    },
    {
      "id": "carvalue",
      "label": "CarValue",
      "description": "The CarValue tool over the API: value a car by VIN or by make and model, and read a VIN from a photo. Scope: carvalue, granted per contract, and the key’s account must also be granted CarValue.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/carvalue/access",
          "summary": "Whether the key’s account may use CarValue.",
          "scope": "carvalue",
          "examples": [
            "/api/carvalue/access"
          ],
          "response": "{\n  \"success\": true\n}",
          "note": "Needs the key to be linked to a portal account that has been granted the tool in the admin; a key without that grant gets 403 even with the scope. Responses are the tool’s own body ({ success: true, ... } or { success: false, message }) — the same backend the portal page uses — so they carry no contract_version envelope. Read X-Request-ID from the header."
        },
        {
          "method": "GET",
          "path": "/api/carvalue/options",
          "summary": "What the valuation form can offer: makes, a make’s models, or a model’s years, engines, fuels, gearboxes and countries.",
          "scope": "carvalue",
          "params": [
            {
              "name": "brand_id",
              "type": "string",
              "example": "12",
              "source": "/api/carvalue/options -> makes",
              "description": "With it, that make’s models."
            },
            {
              "name": "model_id",
              "type": "string",
              "example": "340",
              "source": "/api/carvalue/options?brand_id=12 -> models",
              "description": "With brand_id, the model’s years, engines, fuels, gearboxes and countries."
            }
          ],
          "examples": [
            "/api/carvalue/options",
            "/api/carvalue/options?brand_id=12",
            "/api/carvalue/options?brand_id=12&model_id=340"
          ],
          "response": "{\n  \"success\": true,\n  \"makes\": [\n    {\n      \"id\": 12,\n      \"name\": \"BMW\"\n    }\n  ]\n}",
          "note": "Read from the data the valuation holds, so every option offered has prices behind it."
        },
        {
          "method": "GET",
          "path": "/api/carvalue/valuation",
          "summary": "What a car is worth, by VIN or by make and model.",
          "scope": "carvalue",
          "params": [
            {
              "name": "vin",
              "type": "string",
              "example": "WBA8E9G50GNU12345",
              "source": "The car’s VIN, or POST /api/carvalue/scan.",
              "description": "Decoded to make, model, year, engine, fuel and gearbox, which then override those parameters."
            },
            {
              "name": "brand_id / model_id",
              "type": "string",
              "example": "12 / 340",
              "source": "/api/carvalue/options",
              "description": "The make and model, when no VIN is given."
            },
            {
              "name": "year",
              "type": "integer",
              "example": "2019",
              "source": "The car being valued.",
              "description": "Registration year."
            },
            {
              "name": "year_flex",
              "type": "integer",
              "example": "1",
              "source": "How wide a range you want.",
              "description": "Years either side of year for the range block. Default 1, maximum 5."
            },
            {
              "name": "engine",
              "type": "number",
              "example": "2.0",
              "source": "The car being valued.",
              "description": "Litres."
            },
            {
              "name": "engine_flex",
              "type": "number",
              "example": "0.2",
              "source": "How wide a range you want.",
              "description": "Litres either side of engine. Default 0.2, maximum 1."
            },
            {
              "name": "fuel / gearbox",
              "type": "string",
              "example": "Diesel / Automatic",
              "source": "/api/carvalue/options",
              "description": "Fuel, and Automatic or Manual."
            },
            {
              "name": "condition",
              "type": "enum",
              "example": "normal",
              "source": "normal, damaged or any.",
              "description": "Default normal."
            },
            {
              "name": "mileage",
              "type": "integer",
              "example": "120000",
              "source": "The odometer in km.",
              "description": "Adjusts the estimate."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "/api/carvalue/options",
              "description": "The car’s country."
            },
            {
              "name": "trend_months",
              "type": "integer",
              "example": "12",
              "source": "How much history to chart.",
              "description": "Maximum 24."
            },
            {
              "name": "offers_kind / offers_offset",
              "type": "string / integer",
              "example": "auction / 24",
              "source": "Your offer list state.",
              "description": "Limit the reference offers to one kind of price, and page through them 24 at a time."
            }
          ],
          "examples": [
            "/api/carvalue/valuation?vin=WBA8E9G50GNU12345&mileage=120000",
            "/api/carvalue/valuation?brand_id=12&model_id=340&year=2019&fuel=Diesel"
          ],
          "response": "{\n  \"success\": true,\n  \"valuation\": {\n    \"exact\": {},\n    \"range\": {},\n    \"countries\": [],\n    \"trend\": {},\n    \"time_to_sell\": null,\n    \"offers\": []\n  },\n  \"vehicle\": {\n    \"vin\": \"WBA8E9G50GNU12345\",\n    \"make\": \"BMW\",\n    \"model\": \"320d\",\n    \"year\": 2019\n  }\n}",
          "responseFields": [
            {
              "name": "valuation",
              "type": "object",
              "description": "Retail, auction and buy-now prices, per country, trend, time to sell, and reference offers 24 per page from offers_offset — the same document as /api/market/valuation, plus the offers."
            },
            {
              "name": "vehicle",
              "type": "object | null",
              "description": "What the VIN decoded to; null without a VIN."
            }
          ],
          "note": "422 when the VIN decoded but not to a model: vehicle carries what was read, so retry with brand_id and model_id. Needs the key to be linked to a portal account that has been granted the tool in the admin; a key without that grant gets 403 even with the scope. Responses are the tool’s own body ({ success: true, ... } or { success: false, message }) — the same backend the portal page uses — so they carry no contract_version envelope. Read X-Request-ID from the header."
        },
        {
          "method": "POST",
          "path": "/api/carvalue/scan",
          "summary": "Read VINs from one photo or camera frame.",
          "scope": "carvalue",
          "params": [
            {
              "name": "body",
              "type": "binary",
              "example": "@frame.jpg",
              "source": "A camera frame or photo of the VIN plate or paperwork.",
              "required": true,
              "description": "The image itself as the request body, Content-Type image/jpeg or image/png, at most 1 MB (413 above)."
            }
          ],
          "examples": [
            "curl -sS -X POST -H \"X-API-Key: $API_KEY\" -H \"Content-Type: image/jpeg\" --data-binary @frame.jpg \"https://api.thecarapi.com/api/carvalue/scan\""
          ],
          "response": "{\n  \"success\": true,\n  \"reads\": [\n    {\n      \"vin\": \"WBA8E9G50GNU12345\",\n      \"quad\": [\n        [\n          0.21,\n          0.44\n        ],\n        [\n          0.78,\n          0.44\n        ],\n        [\n          0.78,\n          0.52\n        ],\n        [\n          0.21,\n          0.52\n        ]\n      ],\n      \"votes\": 2\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "reads[]",
              "type": "object[]",
              "description": "Up to three VINs, best first, each with quad (where it is printed, as fractions of the image size, or null) and votes (how many agreeing reads a scanner should collect before accepting it)."
            }
          ],
          "note": "A 429 means the scanner is busy: send the next frame after Retry-After. A busy answer is not charged to your quota."
        }
      ]
    },
    {
      "id": "vehiclescan",
      "label": "Vehicle Scan",
      "description": "The Vehicle Scan tool over the API: saved vehicle requests and the auction and classifieds offers that match them. These are the same requests the account sees in the portal. Scope: vehiclescan, granted per contract, and the key’s account must also be granted Vehicle Scan.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/vehiclescan/access",
          "summary": "Whether the key’s account may use Vehicle Scan, and its limits.",
          "scope": "vehiclescan",
          "examples": [
            "/api/vehiclescan/access"
          ],
          "response": "{\n  \"success\": true,\n  \"limits\": {\n    \"requests\": 20,\n    \"vehicles_per_request\": 5,\n    \"matches_per_request\": 200\n  }\n}",
          "note": "limits: requests per account, vehicles per request, matches returned per request. Needs the key to be linked to a portal account that has been granted the tool in the admin; a key without that grant gets 403 even with the scope. Responses are the tool’s own body ({ success: true, ... } or { success: false, message }) — the same backend the portal page uses — so they carry no contract_version envelope. Read X-Request-ID from the header."
        },
        {
          "method": "GET",
          "path": "/api/vehiclescan/options",
          "summary": "Makes, fuels and gearboxes for a request, or one make’s models.",
          "scope": "vehiclescan",
          "params": [
            {
              "name": "brand",
              "type": "string",
              "example": "BMW",
              "source": "/api/vehiclescan/options -> makes",
              "description": "Returns this make’s models instead."
            },
            {
              "name": "country",
              "type": "string",
              "example": "DE",
              "source": "Your user’s market.",
              "description": "Narrows the options to one country."
            }
          ],
          "examples": [
            "/api/vehiclescan/options",
            "/api/vehiclescan/options?brand=BMW"
          ],
          "response": "{\n  \"success\": true,\n  \"makes\": [\n    \"BMW\"\n  ],\n  \"fuels\": [\n    \"Diesel\"\n  ],\n  \"gearboxes\": [\n    \"Automatic\"\n  ]\n}"
        },
        {
          "method": "GET",
          "path": "/api/vehiclescan/requests",
          "summary": "The account’s vehicle requests.",
          "scope": "vehiclescan",
          "examples": [
            "/api/vehiclescan/requests"
          ],
          "response": "{\n  \"success\": true,\n  \"requests\": [\n    {\n      \"id\": 5,\n      \"active\": true,\n      \"criteria\": [\n        {\n          \"make\": \"BMW\",\n          \"model\": \"320d\",\n          \"year_from\": 2018\n        }\n      ],\n      \"matches_found\": 41\n    }\n  ]\n}"
        },
        {
          "method": "POST",
          "path": "/api/vehiclescan/requests",
          "summary": "Save a vehicle request.",
          "scope": "vehiclescan",
          "params": [
            {
              "name": "criteria",
              "type": "object[]",
              "example": "[{\"make\": \"BMW\", \"model\": \"320d\", \"year_from\": 2018}]",
              "source": "/api/vehiclescan/options",
              "required": true,
              "description": "1 to 5 vehicles, each with a make."
            }
          ],
          "examples": [
            "POST /api/vehiclescan/requests\n{\"criteria\": [{\"make\": \"BMW\", \"model\": \"320d\", \"year_from\": 2018}]}"
          ],
          "response": "{\n  \"success\": true,\n  \"id\": 5,\n  \"matches_found\": 41\n}",
          "note": "Offers already listed are matched at once (matches_found) but not marked new; the scraper cycle marks later ones is_new. Up to 20 requests per account."
        },
        {
          "method": "PUT",
          "path": "/api/vehiclescan/requests/{request_id}",
          "summary": "Replace a request’s vehicles.",
          "scope": "vehiclescan",
          "params": [
            {
              "name": "request_id",
              "type": "integer",
              "location": "path",
              "example": "5",
              "source": "/api/vehiclescan/requests -> requests[].id",
              "required": true,
              "description": "The request id."
            },
            {
              "name": "criteria",
              "type": "object[]",
              "example": "[{\"make\": \"BMW\", \"model\": \"330d\"}]",
              "source": "/api/vehiclescan/options",
              "required": true,
              "description": "1 to 5 vehicles, each with a make. Matching restarts from the offers listed now."
            }
          ],
          "examples": [
            "PUT /api/vehiclescan/requests/5\n{\"criteria\": [{\"make\": \"BMW\", \"model\": \"330d\"}]}"
          ],
          "response": "{\n  \"success\": true,\n  \"id\": 5,\n  \"matches_found\": 12\n}"
        },
        {
          "method": "DELETE",
          "path": "/api/vehiclescan/requests/{request_id}",
          "summary": "Delete a request.",
          "scope": "vehiclescan",
          "params": [
            {
              "name": "request_id",
              "type": "integer",
              "location": "path",
              "example": "5",
              "source": "/api/vehiclescan/requests -> requests[].id",
              "required": true,
              "description": "The request id."
            }
          ],
          "examples": [
            "DELETE /api/vehiclescan/requests/5"
          ],
          "response": "{\n  \"success\": true\n}"
        },
        {
          "method": "POST",
          "path": "/api/vehiclescan/requests/{request_id}/active",
          "summary": "Pause or resume a request.",
          "scope": "vehiclescan",
          "params": [
            {
              "name": "request_id",
              "type": "integer",
              "location": "path",
              "example": "5",
              "source": "/api/vehiclescan/requests -> requests[].id",
              "required": true,
              "description": "The request id."
            },
            {
              "name": "active",
              "type": "boolean",
              "example": "false",
              "source": "Your choice.",
              "required": true,
              "description": "false pauses matching, true resumes it."
            }
          ],
          "examples": [
            "POST /api/vehiclescan/requests/5/active\n{\"active\": false}"
          ],
          "response": "{\n  \"success\": true\n}"
        },
        {
          "method": "GET",
          "path": "/api/vehiclescan/requests/{request_id}/matches",
          "summary": "Offers matching a request.",
          "scope": "vehiclescan",
          "params": [
            {
              "name": "request_id",
              "type": "integer",
              "location": "path",
              "example": "5",
              "source": "/api/vehiclescan/requests -> requests[].id",
              "required": true,
              "description": "The request id."
            }
          ],
          "examples": [
            "/api/vehiclescan/requests/5/matches"
          ],
          "response": "{\n  \"success\": true,\n  \"offers\": [\n    {\n      \"site_name\": \"openlane\",\n      \"auction_id\": 11409652,\n      \"is_new\": true\n    }\n  ],\n  \"retail\": [\n    {\n      \"reference_id\": \"tp-91744022\",\n      \"is_new\": false\n    }\n  ],\n  \"limited\": false\n}",
          "responseFields": [
            {
              "name": "offers / retail",
              "type": "object[]",
              "description": "Auctions and classifieds respectively, up to 200 each. is_new marks offers found since the request was saved; dismissed offers are left out."
            },
            {
              "name": "limited",
              "type": "boolean",
              "description": "True when there were more than 200 of either."
            }
          ]
        },
        {
          "method": "DELETE",
          "path": "/api/vehiclescan/requests/{request_id}/matches/{auction_id}",
          "summary": "Dismiss an auction offer from a request.",
          "scope": "vehiclescan",
          "params": [
            {
              "name": "request_id",
              "type": "integer",
              "location": "path",
              "example": "5",
              "source": "/api/vehiclescan/requests -> requests[].id",
              "required": true,
              "description": "The request id."
            },
            {
              "name": "auction_id",
              "type": "integer",
              "location": "path",
              "example": "11409652",
              "source": "/api/vehiclescan/requests/{request_id}/matches -> offers[].auction_id",
              "required": true,
              "description": "The auction to dismiss."
            }
          ],
          "examples": [
            "DELETE /api/vehiclescan/requests/5/matches/11409652"
          ],
          "response": "{\n  \"success\": true\n}"
        },
        {
          "method": "DELETE",
          "path": "/api/vehiclescan/requests/{request_id}/retail/{reference_id}",
          "summary": "Dismiss a classifieds offer from a request.",
          "scope": "vehiclescan",
          "params": [
            {
              "name": "request_id",
              "type": "integer",
              "location": "path",
              "example": "5",
              "source": "/api/vehiclescan/requests -> requests[].id",
              "required": true,
              "description": "The request id."
            },
            {
              "name": "reference_id",
              "type": "string",
              "location": "path",
              "example": "tp-91744022",
              "source": "/api/vehiclescan/requests/{request_id}/matches -> retail[].reference_id",
              "required": true,
              "description": "The classified listing to dismiss."
            }
          ],
          "examples": [
            "DELETE /api/vehiclescan/requests/5/retail/tp-91744022"
          ],
          "response": "{\n  \"success\": true\n}"
        }
      ]
    },
    {
      "id": "calculator",
      "label": "Import calculator",
      "description": "Estimate the landed cost of importing a vehicle. These are estimates, not a binding quote.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/calculator/countries",
          "summary": "List supported origin and destination countries with EU membership and VAT rates.",
          "scope": "calculator",
          "examples": [
            "/api/calculator/countries",
            "curl -H \"X-API-Key: $API_KEY\" \"https://api.thecarapi.com/api/calculator/countries\""
          ],
          "response": "{\n  \"success\": true,\n  \"countries\": [\n    {\n      \"code\": \"DE\",\n      \"name\": \"Germany\",\n      \"eu\": true,\n      \"vat\": 0.19\n    },\n    {\n      \"code\": \"KR\",\n      \"name\": \"South Korea\",\n      \"eu\": false,\n      \"vat\": 0\n    }\n  ]\n}",
          "responseFields": [
            {
              "name": "code",
              "type": "string",
              "description": "Country code. The United Kingdom is UK here, not GB — this list is the calculator’s own country vocabulary and is separate from the vehicle-origin codes on /api/countries, where the same country is canonicalised to GB. If you feed a vehicle’s country_code straight into the calculator, map GB to UK on the way."
            },
            {
              "name": "eu",
              "type": "boolean",
              "description": "True for all 27 EU member states; false for the rest."
            },
            {
              "name": "vat",
              "type": "number",
              "description": "VAT as a fraction (0.19 = 19%), unlike /api/calculator/calculate which returns rates as percentages."
            }
          ],
          "note": "The 36 supported origin/destination countries. This is the authoritative list: a code absent from it is a 400 on /api/calculator/calculate, so read it once at startup instead of hardcoding. An unknown origin or destination is never silently substituted."
        },
        {
          "method": "POST",
          "path": "/api/calculator/calculate",
          "summary": "Estimate duty, VAT, fees, and the landed total for one lot price.",
          "scope": "calculator",
          "params": [
            {
              "name": "price",
              "type": "number",
              "example": "15000",
              "source": "The source lot price, not a precomputed landed total.",
              "required": true,
              "description": "Lot price before import fees. Must be between 0 and 10,000,000. Do not pass public_price_eur or you can count import costs twice."
            },
            {
              "name": "origin",
              "type": "string",
              "example": "KR",
              "source": "/api/calculator/countries -> countries[].code",
              "description": "Origin country code. Default KR."
            },
            {
              "name": "destination",
              "type": "string",
              "example": "BG",
              "source": "/api/calculator/countries -> countries[].code",
              "description": "Destination country code. Default BG."
            },
            {
              "name": "car_type",
              "type": "string",
              "example": "standard",
              "source": "Choose standard or classic.",
              "description": "Default standard."
            },
            {
              "name": "currency",
              "type": "string",
              "example": "EUR",
              "source": "Currency label used by your application.",
              "description": "Response label only; no foreign-exchange conversion is performed. Source profiles use EUR constants, so supply an EUR-normalized price and use EUR."
            },
            {
              "name": "site_name",
              "type": "string",
              "example": "openlane",
              "source": "/api/sites -> sites[].name",
              "description": "Source whose fee model to price against: encar, openlane, auto1, schadeautos, copart. Omitted, it defaults to encar when origin is KR and otherwise falls back to a generic model that is not what any of our own listings are priced with. kbchachacha and japan have no dedicated profile and take the generic path too when explicitly supplied. JP is not currently in the calculator country catalogue, so origin=JP is rejected. Do not substitute another country to obtain a Japanese import quote. Send it whenever you know it."
            },
            {
              "name": "db_tax / db_delivery",
              "type": "number",
              "example": "450",
              "source": "Precomputed values from your own records.",
              "description": "For a recognized source profile, db_tax is ignored and the fee is computed from the profile; a nonzero db_delivery overrides shipping (zero uses the profile default). The generic path accepts both overrides. Values must already use the same EUR basis as price."
            },
            {
              "name": "is_margin",
              "type": "boolean",
              "example": "false",
              "source": "/api/auction/{site}/{id} -> auction.is_margin",
              "description": "Whether the vehicle sells under the VAT margin scheme. Read the listing’s own flag when estimating landed cost."
            }
          ],
          "examples": [
            "POST /api/calculator/calculate\n{\n  \"price\": 15000,\n  \"origin\": \"KR\",\n  \"destination\": \"BG\",\n  \"site_name\": \"encar\"\n}",
            "POST /api/calculator/calculate\n{\n  \"price\": 9800,\n  \"origin\": \"BE\",\n  \"destination\": \"BG\",\n  \"site_name\": \"openlane\"\n}"
          ],
          "response": "{\n  \"success\": true,\n  \"currency\": \"EUR\",\n  \"breakdown\": {\n    \"lot_price\": 15000,\n    \"auction_fee\": 0,\n    \"trucking\": 0,\n    \"shipping\": 1800,\n    \"our_fee\": 700,\n    \"subtotal_customs_value\": 16800,\n    \"duty_rate\": 10,\n    \"duty_amount\": 1680,\n    \"vat_rate\": 20,\n    \"vat_amount\": 3696,\n    \"customs_agency\": 1425,\n    \"custom_clearance_total\": 6801,\n    \"estimated_total\": 24301\n  }\n}",
          "responseFields": [
            {
              "name": "lot_price",
              "type": "number",
              "description": "The price you sent, echoed."
            },
            {
              "name": "auction_fee",
              "type": "number",
              "description": "The auction house’s own buyer fee for that source and lot price."
            },
            {
              "name": "trucking",
              "type": "number",
              "description": "Inland haulage, on the generic model only; 0 under a source profile, where it is folded into shipping."
            },
            {
              "name": "shipping",
              "type": "number",
              "description": "Transport to the destination. Your db_delivery overrides it when supplied."
            },
            {
              "name": "our_fee",
              "type": "number",
              "description": "Our service fee. Charged outside the duty/VAT base."
            },
            {
              "name": "subtotal_customs_value",
              "type": "number",
              "description": "lot_price + auction_fee + trucking + shipping — the value duty is assessed on."
            },
            {
              "name": "duty_rate / duty_amount",
              "type": "number",
              "description": "Rate as a percentage (10.0), and the amount. 0 for a car already in free circulation in the EU."
            },
            {
              "name": "vat_rate / vat_amount",
              "type": "number",
              "description": "Percentage and amount. VAT is charged on subtotal_customs_value + duty_amount, not on the lot price alone."
            },
            {
              "name": "customs_agency",
              "type": "number",
              "description": "Fixed post-import costs outside the duty/VAT base (port clearance, agency work)."
            },
            {
              "name": "custom_clearance_total",
              "type": "number",
              "description": "duty_amount + vat_amount + customs_agency."
            },
            {
              "name": "estimated_total",
              "type": "number",
              "description": "subtotal_customs_value + custom_clearance_total + our_fee. The landed figure."
            }
          ],
          "note": "Duty and VAT rates are returned as percentages. Missing price → 400. A price that is not a number, is negative, or exceeds 10,000,000 → 400. A car_type other than standard or classic → 400. An origin or destination code not in /api/calculator/countries → 400 naming it. Source profiles mirror the stored Bulgarian landed-price model. Supply the same origin, destination, margin flag and delivery assumptions to compare totals; stored figures can lag configuration or source changes. For copart, price a running lot from its final_price, which tracks the live high bid; current_price trails it. These are estimates, not a binding quote. No envelope metadata — read X-Request-ID from the header."
        }
      ]
    },
    {
      "id": "ops",
      "label": "Health & contract",
      "description": "Check service dependencies and inspect the API index.",
      "endpoints": [
        {
          "method": "GET",
          "path": "/api/health/live",
          "summary": "Unauthenticated process liveness probe.",
          "scope": "none — no API key required",
          "examples": [
            "/api/health/live"
          ],
          "response": "{\n  \"status\": \"ok\",\n  \"service\": \"car-details-api\",\n  \"contract_version\": \"2026-10-05\"\n}",
          "note": "One of only two endpoints in the whole API that need no key. Always 200 while the process is up. This is the right target for an uptime monitor — it says nothing about whether the data behind the API is reachable, which is what /api/health/ready and /api/health are for."
        },
        {
          "method": "GET",
          "path": "/api/health/ready",
          "summary": "Unauthenticated readiness probe — is the data layer reachable?",
          "scope": "none — no API key required",
          "examples": [
            "/api/health/ready"
          ],
          "response": "{\n  \"status\": \"ready\"\n}",
          "note": "Unauthenticated readiness: 200 {\"status\": \"ready\"} once the service can reach its database, 503 {\"status\": \"not_ready\"} until then. Use live for a liveness probe and ready for a readiness probe; neither consumes quota. No key required."
        },
        {
          "method": "GET",
          "path": "/api/health",
          "summary": "Return service, data-feed and schema health.",
          "scope": "ops",
          "examples": [
            "/api/health",
            "curl -H \"Authorization: Bearer $API_KEY\" \"https://api.thecarapi.com/api/health\""
          ],
          "response": "{\n  \"status\": \"healthy\",\n  \"service\": \"car-details-api\",\n  \"worker_pid\": 41,\n  \"contract_version\": \"2026-10-05\",\n  \"typesense\": {\n    \"enabled\": true,\n    \"healthy\": true\n  },\n  \"public_auction_feed\": {\n    \"enabled\": true,\n    \"fresh\": true\n  }\n}",
          "note": "status is healthy or degraded. A degraded service still answers 200, with the reason under schema_findings — so alert on the status field, not on the HTTP code. typesense.healthy being false is what turns free-text ?search= into a plain database match rather than a full-text one, and is the usual explanation for a text search returning less than you expected."
        },
        {
          "method": "GET",
          "path": "/api/contract",
          "summary": "Machine-readable schema catalog, pagination limits, and live-price capability.",
          "scope": "ops",
          "examples": [
            "/api/contract",
            "curl -H \"X-API-Key: $API_KEY\" \"https://api.thecarapi.com/api/contract\""
          ],
          "response": "{\n  \"success\": true,\n  \"version\": \"2026-10-05\",\n  \"schemas\": {\n    \"search_result_card\": {\n      \"required\": [],\n      \"optional\": []\n    }\n  },\n  \"pagination\": {\n    \"max_limit\": 100,\n    \"max_offset\": 5000\n  },\n  \"live_prices\": {\n    \"enabled\": true,\n    \"sites\": [\n      \"openlane\"\n    ],\n    \"ttl_seconds\": 120\n  }\n}",
          "responseFields": [
            {
              "name": "version",
              "type": "string",
              "description": "The contract version this deployment serves. Matches the contract_version on response bodies that carry one. This response itself carries no envelope metadata — version is the contract version."
            },
            {
              "name": "schemas",
              "type": "object",
              "description": "Required and optional keys per response shape. This is the authoritative machine-readable schema; prefer it to hardcoding field lists. As of 2026-08-19, auction_detail.optional lists vault_gallery and brands_models_facets.optional lists errors — gate on those rather than comparing version strings."
            },
            {
              "name": "pagination.max_limit",
              "type": "integer",
              "description": "The page-size ceiling. Clamp your own paging to it rather than discovering it by being clamped."
            },
            {
              "name": "pagination.max_offset",
              "type": "integer",
              "description": "The offset cap on the capped endpoints (catalog, archive, top-offers, classifieds listings). Search and listVehicles have no offset cap."
            },
            {
              "name": "live_prices",
              "type": "object",
              "description": "Whether this deployment reads a running auction’s bid when its detail is requested, which sources it covers, and how long a refreshed price stays current ({ enabled, sites, ttl_seconds }). The values are illustrative, not guaranteed — read them at startup rather than hardcoding them. The whole block is absent where the feature does not exist at all, and that absence is the only way to tell “live prices are unavailable” from “this car has none right now”, because both look identical in a detail response."
            }
          ],
          "note": "Read this once at startup. It is the correct answer to “does this deployment support live prices, and for which sources” — a missing live_price block on a detail response is not an error and never distinguishes the two cases on its own. This response carries no envelope metadata — its own version field is the contract version."
        },
        {
          "method": "GET",
          "path": "/",
          "summary": "Return the human-readable API index and service version.",
          "scope": "ops",
          "examples": [
            "/",
            "curl -H \"X-API-Key: $API_KEY\" \"https://api.thecarapi.com/\""
          ],
          "response": "{\n  \"service\": \"Car Details API\",\n  \"version\": \"2.0.0\",\n  \"endpoints\": {\n    \"GET /api/health\": \"Health check endpoint\"\n  },\n  \"typesense\": {\n    \"enabled\": true,\n    \"running\": true\n  }\n}",
          "note": "A smoke-test target, not a discovery document — this reference and GET /api/contract are what describe the surface."
        }
      ]
    }
  ]
}
