{
  "openapi": "3.0.3",
  "info": {
    "title": "carstandings.com API",
    "version": "1.0.0",
    "description": "Car data per market and model year.\n\n## Two products, and which one a market-year sells\nEach market-year's `score_scale` says which it is.\n\nA market-year with `score_scale.product = \"figures\"` publishes NO overall score:\nno `composite_score`, no `rank`, no `factor_scores`. It publishes the measured\nspecs themselves — `published_figures` holds a raw value only where a publisher\nmeasured that quantity for that car, `figure_provenance` names who measured it and\non what basis, and `figures_withheld` says why each absent spec is absent. Lists\nare ordered by ONE of those specs at a time (`?sort=`), each list makes exactly one\nclaim, and the response carries it as `claim`. `sort=catalogue` (or `sort=overall`,\nits alias) lists the market in a documented order and makes no claim at all. A car\nwith no value for the chosen spec is not in the list; `listed_cars` and\n`unlisted_cars` say how many are and are not. `score_scale.no_composite` explains, in\nfull, why there is no overall score for that market.\n\nAny other market-year publishes a composite as before: `composite_score`, `rank`, and\n`factor_scores` over the factors its `score_scale.factors_used` names.\n\n## Markets\nOnly launched markets are served. GET /api/v1/countries lists them (with an account\nor a key). Any endpoint given a country that is not listed there answers 404 with\nmarket_not_available=true, whoever the caller is — except that on the six endpoints\nbelow that need authentication, a caller with no credential is refused with 401\nfirst and is told nothing about markets.\n\n## Authentication, and what is free\nAUTHENTICATION IS REQUIRED to read the catalogue. GET /api/v1/car,\n/api/v1/market, /api/v1/countries, /api/v1/factors, /api/v1/stats and\n/api/v1/images answer 401 with code `authentication_required`, and no data, to a\nrequest that carries neither a signed-in session nor an X-API-Key. There is no\ncrawler exception: a search or AI-answer engine's crawler gets the same 401 as\nanyone else, and nothing about a request's User-Agent or any bot signal changes\nthat. A key that does not exist is 401 `invalid_api_key`, a revoked one is 401\n`api_key_revoked`, and one past its expiry date is 401 `api_key_expired`. These refusals are never cached. Create a free account at\nhttps://carstandings.com/account/?auth=signup; plans are at\nhttps://carstandings.com/pricing/. GET /api/v1/schema and GET /api/v1/llms.txt,\nwhich describe the API and hold no data, stay open.\n\nAny authenticated caller passes that first gate, and what the plan then reads\ndepends on it. A FREE account reads the FOUR OPEN SPECS on ANY car: combined fuel\neconomy, CO2, NHTSA overall crash rating and recall count, each with publisher and\nbasis, specs those publishers already make freely available. On\nGET /api/v1/car the car's identity and those four specs come back with values and\nevery other spec withheld with `reason: \"requires_account\"`, counted in\n`figures_requiring_account`. The values are NOT in the response: this is a\nserver-side withholding, not a display effect. The limit is 25 DIFFERENT cars per\nUTC day per account, counted on the server. Reading a car you have already read\nthat day costs nothing. The TEN MOST-REGISTERED CARS of each market-year (the\nTop 10, the same ten the website shows a signed-out visitor) are never counted:\nany signed-in free account reads them whatever its day's count, they are not\nadded to the 25, and the cap never refuses them. The 26th different car\noutside the Top 10 answers 429, code `daily_car_limit`, with\n`reads_left_today: 0`, `quota.resets_at` (the next 00:00 UTC) and a\n`Retry-After` header; cars already read that day and the Top 10 stay readable.\nEvery successful free read of a counted car states `reads_left_today` and\n`quota`, and a Top-10 read states `quota` and `counts_toward_limit: false`. The\ncounter fails closed: if it cannot be read or written, a free account's car\nread answers 503, code `quota_unavailable`, with `Retry-After: 30`, rather\nthan being served uncounted (a Top-10 car, and a Data or API plan, need no\ncounter and are unaffected). A Data or API plan reads every car in full with\nno daily limit and no counter.\n\nThe \"Try it\" examples on the developers page run through a shared demonstration\nkey that the site holds. It answers only the fixed preset examples, exactly as\nshown there and for model year 2026, and refuses anything else with 401\n`playground_preset_only`. It returns what an API-plan key returns for the same\ncall, no more. It is limited per visitor (429 `playground_rate_limited` with\n`Retry-After`), and it does not create keys or touch any account. It is not a\ncredential you can use: API keys come with the API plan, and you create them\nin your account.\n\nA request pattern that looks like bulk copying (many different cars in an hour,\nan alphabetical sweep, many accounts from one address, or one key on many\naddresses) is first noted, then slowed (the data is still correct and a\n`Retry-After` header is set), and for clear abuse the account and its key are\nsuspended with 403 `account_suspended`. Data is never altered. If you were\nsuspended by mistake, write to support@carstandings.com and it will be lifted.\n\nGET /api/v1/market returns a whole market-year in one response and lists EVERY car\nwith the four open specs on every row, for every authenticated caller, free\nincluded (no per-car restriction, and it is not counted against the 25 cars). The\nheld specs belong to /api/v1/car, to the bulk record and to the derived endpoints\nbelow, never to a market-wide list.\n\n/api/v1/factors, /api/v1/countries, /api/v1/stats and /api/v1/images need an account\nor a key but no particular plan: any authenticated caller reads them.\n\nTHREE TIERS SELL PROGRAMMATIC ACCESS AND BULK DOWNLOAD: free, data ($15/mo) and\napi ($75/mo) are the only tiers for sale — sellable_tiers lists exactly these\nthree, and a subscription can only ever be created on one of them.\n\nA PAID plan (data or api) buys the specs themselves in full, per car at\nGET /api/v1/car and assembled at GET /api/v1/bulk — the whole market-year as one\nattributed file. Both cover every launched market: plans differ by what you can DO,\nnever by how many markets they cover. GET /api/v1/bulk has no free form: a\nfree caller is refused (403) and pointed at the endpoints above.\n\nThe api plan additionally buys the derived endpoints: /api/v1/compare,\n/api/v1/manufacturer, /api/v1/vin and /api/v1/ask. A plan below it is refused on\nthose (403) and told where to read the same underlying data free.\n\nAPI keys come with the api plan only. A free account has no key: it reads with\nits signed-in session. The data plan has no key either; it reaches /api/v1/bulk\nwith a signed-in session, because that plan buys data rather than programmatic\naccess.\n\n## API keys\nAn account on the api plan holds up to 10 active keys at once, managed at\n/api/v1/keys (from the account page, or with a signed-in session). Each key has\na name, an expiry date and a last-used time. A key is 32 characters after the\n`cs_live_` prefix, shown once when it is created and never again (only a hash\nis stored), so a lost key is revoked and replaced, not recovered.\n- Expiry: choose 30, 90, 180 or 365 days, or no expiry, when creating a key\n  (90 days if you do not choose). You can move the date later while the key is\n  live. A key past its date is refused with 401 `api_key_expired`. It is listed\n  as `expiring` for its last 14 days.\n- Revoke: a revoked key is refused at once with 401 `api_key_revoked`.\n- Other accounts' keys: never listed or reachable. Asking for a key that is not\n  yours is 404 `key_not_found`.\n- If the plan ends, every key on the account is suspended and refused. Keys are\n  not switched back on when a plan returns: create new ones.\n\n## Rate limits and daily caps\n- api: 1,000 requests a UTC day per ACCOUNT, shared by all of the account's\n  keys (ten keys do not mean ten times the allowance). Every keyed request to\n  the data endpoints counts: car, market, countries, factors, stats, images,\n  compare, manufacturer, vin, ask and bulk. Every keyed response carries\n  RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds to the next\n  UTC midnight). Over the cap the response is 429 `rate_limit_exceeded` with\n  Retry-After. GET /api/v1/usage reports today's count, and the account page\n  shows it.\n- free: no key. Reads with its session: four specs on any car, 25 different cars\n  a UTC day (see above).\n- data: no key, so no request quota. Its one paid endpoint is /api/v1/bulk,\n  reached by session, and it is capped at 25 market-year downloads a day. Over\n  the cap the response is 429 and names the cap, when it resets, and that\n  browsing is unaffected.\n- There is no per-second or per-minute limit. A separate protection against\n  bulk copying applies to every caller (described above) and is not a quota.\n\n## Provenance\nEvery car record names the data behind it. `sources` is every dataset cited by\nthe row: each one that put a published value on it, including the ones named\nonly in a field's own provenance key (curb_weight_source, horsepower_source).\n`contributing_sources` is the narrower list the confidence badge is counted\nfrom — the datasets that measured something published on THAT row, which\nexcludes tables we compile ourselves and a dataset that was read and produced\nnothing about the car. `contributing_publishers` folds those datasets onto the\norganisations that published them, so one agency counts once however many of\nits files a row uses; `source_count` is the length of that list and\n`confidence` is the label it produces. A spec that needs a basis carries one\n(horsepower_basis, curb_weight_basis, price_basis, co2_source): quote the spec\nwith its basis.\n\n## Price\n`price_usd` is a BASE PRICE IN US DOLLARS. On United States rows of model years\n2020, 2022 and 2024 it is NHTSA's CAFE compliance file's MSRP column, averaged by\nDOT staff across the trims matching that compliance configuration — no single car\nis sold for it — and it is published only where every configuration matching the\ncar states the same price. Canadian rows carry NO price: this is a US-market\nspec and nothing converts it into a Canadian one. It is a published fact and no\nscore, depreciation estimate or five-year cost is derived from it. Read it with\n`price_source`, `price_basis`, `price_as_of` and `price_cross_check`, and quote\nthose wherever you quote the spec.\n\n## Attribution\nSome of this data is published under a licence that makes an acknowledgement a\nCONDITION of reuse, and that condition passes to you. Any response carrying\nsuch data includes an `attribution` array of acknowledgements to reproduce\nverbatim wherever you publish the data or anything derived from it, and an\n`X-Data-Attribution` header naming the datasets they are owed for. Canadian\nrows carry NRCan's fuel-consumption data, published under the Open Government\nLicence – Canada, and its acknowledgement is the licence's own sentence:\nContains information licensed under the Open Government Licence – Canada.\nA response with no such data carries no `attribution` field, and you may not\nclaim an acknowledgement that is not there.\n\n## Base URL\nhttps://api.carstandings.com — the API. https://carstandings.com is the website,\nand answers every /api/ path with its own HTML page.\n",
    "contact": {
      "name": "carstandings.com support",
      "url": "https://carstandings.com"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://carstandings.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.carstandings.com",
      "description": "Production"
    },
    {
      "url": "http://localhost:8787",
      "description": "Local wrangler dev"
    }
  ],
  "tags": [
    {
      "name": "Rankings",
      "description": "Car ranking data by country and year"
    },
    {
      "name": "Reference",
      "description": "Countries, factors, and aggregate stats"
    },
    {
      "name": "Vehicles",
      "description": "Per-car, per-manufacturer, and comparison lookups"
    },
    {
      "name": "VIN",
      "description": "VIN decoding and ranking match"
    },
    {
      "name": "Media",
      "description": "Image manifests and logos"
    },
    {
      "name": "Auth",
      "description": "Clerk session and API key management"
    },
    {
      "name": "Billing",
      "description": "Paddle checkout and webhooks"
    },
    {
      "name": "Bulk",
      "description": "The assembled, attributed record — the paid artefact"
    },
    {
      "name": "Meta",
      "description": "API schema and LLM discovery files"
    }
  ],
  "paths": {
    "/api/v1/market": {
      "get": {
        "tags": [
          "Rankings"
        ],
        "summary": "Get car rankings for a country/year",
        "description": "Authentication required: a request with no session and no X-API-Key is refused\nwith 401 `authentication_required` and no data (a crawler is not exempt).\n\nproduct = \"figures\" (US, CA — the only launched markets today): the whole\nmarket-year in one response. EVERY row, for every authenticated caller (free\nand paid alike), carries the four open specs (fuel economy, CO2, crash\nrating, recalls); there is no per-car restriction and the list is not counted\nagainst a free account's 25 cars a day. Never metered for a paid plan; a free\naccount is asked to pass a check after a volume of whole market-years a person\ndoes not reach in a day. The held specs and\nheld-spec sorts are not on this endpoint for anyone: a reader after\nhorsepower, weight, warranty, price or complaint rate reads it per car at\n/api/v1/car (a paid plan reads every spec; a free account reads the four open specs, 25 different cars a day), or as\nthe bulk record on a paid plan.\n\nOtherwise (a composite market): the full ranked car list, sorted by the\nrequested factor. A free account receives a free-tier preview (top 3 cars\nonly) with free_tier=true. With a valid paid key, returns the full ranking for\nthe countries in the key's plan. For a launched country outside the plan, a\npaid caller receives the same free preview with a notice explaining why, never\nless than a free account. A country that has not launched returns 404\nwith market_not_available=true for every authenticated caller, with no preview.\n",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "us"
            },
            "description": "Launched market code, as listed by /api/v1/countries. Any other code returns 404."
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 2025,
              "minimum": 1984,
              "maximum": 2026
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "overall",
              "enum": [
                "overall",
                "catalogue",
                "fuel_economy",
                "fuel_economy_mpge",
                "co2",
                "co2_charge_weighted",
                "horsepower",
                "horsepower_engine",
                "weight",
                "test_weight",
                "warranty",
                "recalls",
                "crash_rating",
                "complaint_rate",
                "price",
                "fuel_efficiency",
                "co2_emissions",
                "safety_crash",
                "safety_recalls",
                "price_value",
                "reliability",
                "owner_satisfaction",
                "power_performance",
                "weight_efficiency",
                "depreciation",
                "insurance",
                "tco_5yr"
              ]
            },
            "description": "Which sorts a market-year serves depends on the product its score_scale declares, and the two sets do not overlap.\nproduct = \"figures\": \"catalogue\" (and \"overall\", its alias) lists the market in a documented order and makes no claim. Any other sort must be one of the PUBLIC SET's own spec ids (fuel_economy, fuel_economy_mpge, co2, co2_charge_weighted, crash_rating, recalls) — this endpoint carries no other spec for any caller, so it cannot sort by one either. A held spec — horsepower, weight, warranty, complaint_rate, price — is a valid /api/v1/car and bulk-record spec but is refused here with 400 (code sort_not_listed), naming in valid_sorts the sorts this endpoint does serve, not the market-year's whole spec set, and in figure_endpoint (/api/v1/car) where the spec itself is read. Such a list is ordered by the RAW MEASURED SPEC — miles per gallon, NHTSA stars, recall campaigns — best end first as that spec's `order` says, then by the documented tie-break (score_scale.tie_break): make, model, powertrain, then the row id, all ascending and text compared lower-cased. A car with NO value for that spec is not in the list. It follows the list, and its row says which spec it lacks and why (figures_withheld). The response carries `claim` (the one claim the list makes), `figure`, `unit`, `listed_cars` and `unlisted_cars`. Note fuel_economy (miles per actual gallon, petrol/diesel/hybrid) and fuel_economy_mpge (electric and hydrogen, energy converted at 33.7 kWh per gallon) are different quantities and are never mixed in one list. The same holds for co2 (the publisher's tailpipe rating for the car driven as rated, which includes an electric or hydrogen car's measured zero) and co2_charge_weighted (a plug-in hybrid's composite, weighted by the share of the miles the publisher assumes are driven on grid electricity at zero tailpipe, and so smaller than what the car emits with the battery empty).\nproduct absent (a composite market): \"overall\" = composite score, in rank order. A factor orders the same rows by that factor's published score: cars with a score first, best first, a tie by overall rank; then the cars without one, in overall rank order, unranked last. Only a factor in the market-year's score_scale.factors_used is a sort.\nIn both products a sort the market-year cannot serve — a spec it does not publish, a factor not in its ranking, a sort belonging to the other product, any sort where no score_scale is published, and any sort no car in the market-year carries a value for — is refused with 400 (sort_unavailable) and a valid_sorts list, rather than served as the unordered list under another name.\nBrowsing a \"figures\" market needs an account or a key, and every row carries the public set only, whoever is asking. No preview applies to it — there is nothing further to unlock at this endpoint, on any plan. The free preview of a composite market's factor sort is the top three cars that have a score for it."
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            },
            "description": "Cars per page, with page. Omit both for the whole market-year in one response (a caller entitled to the market). A value outside 1-50 is refused with 400, not clamped. (A limit parameter was documented here and never read; it is not a parameter.)"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page number, with per_page. The free preview has one page, the top three ranked cars."
          }
        ],
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Rankings retrieved",
            "headers": {
              "X-Data-Attribution": {
                "$ref": "#/components/headers/XDataAttribution"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Rankings"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "free_tier": {
                          "type": "boolean",
                          "description": "A composite market only. True when the response is the top-3 preview: a free plan, or a country the caller's plan does not include. A \"figures\" market (US, CA) never sets this — every row carries the four open specs."
                        },
                        "notice": {
                          "type": "string",
                          "description": "Why a signed-in caller received the preview."
                        },
                        "upgrade_required": {
                          "type": "boolean"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "404": {
            "$ref": "#/components/responses/MarketNotAvailable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/countries": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "List the launched markets",
        "description": "Returns the launched markets, the only ones any endpoint serves, with coverage years and car counts. Every listed market can be included in a paid plan. Authentication required (any plan): 401 with no session and no key.",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Country list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer",
                      "example": 2
                    },
                    "countries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Country"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          }
        }
      }
    },
    "/api/v1/factors": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "What a market-year publishes about a car",
        "description": "With ?country= and ?year= for a market-year whose score_scale declares product = \"figures\": the measured SPECS that market-year publishes — id, label, unit, the one claim a list ordered by it makes, its publishers, the row fields carrying its basis, its coverage and denominator, and the coverage bar this project publishes for it, beside the coverage actually measured (requirement_bar, requirement_coverage, meets_requirement). The bars are PUBLISHED, NOT ENFORCING: a spec that misses one is published with the shortfall shown rather than withheld. The response also carries no_composite, which says in full why that market publishes no overall score, and figures_absent for the specs it does not publish and why.\n\nOtherwise: the 13 scoring factors with labels, descriptions, weights, units and inversion direction. WEIGHTS ARE PER MARKET. Pass ?country= to get the weighting actually used for that market; the response echoes weights_from so you can tell whose weighting you received. Without it you get the default market. A country that has not launched returns 404 with market_not_available=true. Authentication required (any plan): 401 with no session and no key.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "ISO 3166-1 alpha-2 market code. Defaults to us.",
            "schema": {
              "type": "string",
              "default": "us"
            }
          }
        ],
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Factor list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer",
                      "example": 13
                    },
                    "factors": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Factor"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          }
        }
      }
    },
    "/api/v1/stats": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "Aggregate statistics for a country/year",
        "description": "Returns min/max/avg for CO2 and horsepower, plus a powertrain breakdown. ONE AGGREGATE IS ONE QUANTITY, exactly as one list is: co2_g_per_km covers only the tailpipe-rated rows and horsepower only the whole car's output, with co2_g_per_km_charge_weighted and horsepower_engine_only carrying the other basis. Each of those two appears ONLY where the market-year has such a row, so an absent key means no row of that basis rather than a market without plug-in hybrids. An electric or hydrogen car's measured 0 g/km is in co2_g_per_km, being a measurement. tco_5yr is returned only by a market-year that publishes a composite; one publishing measured specs derives no cost of ownership and the key is absent rather than three nulls. Such a market-year also returns no score range and no top or bottom 5 — nothing there is ranked against anything else — and carries no_composite saying why. Authentication required (any plan): 401 with no session and no key.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "us"
            }
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 2025
            }
          }
        ],
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Statistics",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "404": {
            "description": "No data for country/year",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/bulk": {
      "get": {
        "tags": [
          "Bulk"
        ],
        "summary": "The whole market-year as one assembled, attributed file",
        "description": "THE PAID ARTEFACT. A free account reads the four open specs of any car at /api/v1/car (25 different cars a day) and on every row of /api/v1/market; every other spec, with no daily limit, is what this and the per-car endpoint sell, together with the ASSEMBLY: one record per car and one per car-and-spec, each spec beside the publisher that measured it, the unit it is in and the basis it was measured on; each ABSENT spec beside the reason it is absent; and the licence position of every source, with any acknowledgement that licence makes a condition of reuse.\n\nStreamed as NDJSON (default), CSV or XML. In NDJSON the FIRST line is a manifest naming every column of every record with its unit, its publisher and its licence, plus the commit and the source dates the file was built from — so the file is usable without this document. The LAST line carries the counts of every record type, so a truncated download is detectable. Every line parses independently. XML carries the same fields, one <car> element per car with a <spec> child per spec. XLSX is not streamed — it is a two-sheet workbook (Data plus a Methodology sheet with the terms, licence acknowledgements and source list) built and returned whole.\n\nIncluded in every paid plan. Data reaches it with a signed-in session and no API key, because that plan buys data rather than programmatic access; API may use either. A free account is refused with 403 and pointed at the endpoints it can read, and a caller with no credential is refused. The response is never cached — it depends on the caller's entitlement, not on the URL.\n\nA market-year whose licence gate has not passed is refused with 503 rather than served without its provenance.\n\nOne market-year per request. GET /api/v1/countries (any account or key) lists every launched market with `years_available`, so the set of files a plan covers is discoverable without guessing at years.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "us"
            }
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 2025
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "jsonl (default) — the full artefact as NDJSON, manifest first. csv — one table of car-and-spec rows with the car's identity inlined; the manifest is not in it, so fetch format=manifest beside it for the column, unit and licence definitions. xml — the same records as csv, one <car> element per car with a <spec> child per spec. xlsx — a two-sheet workbook: Data (the same rows as csv) and Methodology (the manifest's terms, licence acknowledgements and source list). manifest — the self-description alone, as JSON.",
            "schema": {
              "type": "string",
              "enum": [
                "jsonl",
                "csv",
                "xml",
                "xlsx",
                "manifest"
              ],
              "default": "jsonl"
            }
          }
        ],
        "security": [
          {
            "BearerAuth": []
          },
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The artefact. Content-Type application/x-ndjson, or text/csv for format=csv, application/xml for format=xml, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for format=xlsx, or application/json for format=manifest.",
            "headers": {
              "X-Data-Attribution": {
                "$ref": "#/components/headers/XDataAttribution"
              }
            },
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              },
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Unknown format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "No plan covering this market — free, or a paid plan that does not include it. The body names what a free account reads instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "This market has not launched",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "No bulk record is published for this market-year",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/compare": {
      "get": {
        "tags": [
          "Vehicles"
        ],
        "summary": "Compare cars across manufacturers",
        "description": "Returns all cars from the specified manufacturers for a country/year.\nPass \"make\" at least twice (e.g. ?make=Toyota&make=Honda). A derived\nendpoint (with /api/v1/manufacturer, /api/v1/vin and /api/v1/ask): on a\n\"figures\" market (US, CA — the only launched markets) this is api-plan\nonly, refused with 403 for a signed-out, free or data caller, since\nthe specs it would compare are read per car at /api/v1/car and\n/api/v1/market. On a composite market a caller with no\nkey instead gets the same top-3 preview /api/v1/market gives one. No\ncredential is required to CALL the endpoint either way — security is\nempty here because the plan gate is enforced in the response, not at\nthe door — so \"Public\" describes the request, never the data you get\nback for a specs market.\n",
        "parameters": [
          {
            "name": "make",
            "in": "query",
            "required": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "minItems": 2
            },
            "style": "form",
            "explode": true,
            "description": "Manufacturer names. Repeat the param for each (min 2)."
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "us"
            }
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 2025
            }
          }
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Comparison result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comparison": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Car"
                      }
                    },
                    "country": {
                      "type": "string"
                    },
                    "year": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/api/v1/manufacturer": {
      "get": {
        "tags": [
          "Vehicles"
        ],
        "summary": "Manufacturer summary",
        "description": "Returns a manufacturer's model lineup for a country/year with average score, best/worst model, and powertrain breakdown. A derived endpoint, gated the same way /api/v1/compare is: api-plan only on a \"figures\" market (US, CA), the composite market's own top-3 preview otherwise, no credential required to call it either way.",
        "parameters": [
          {
            "name": "make",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Manufacturer name (e.g. Toyota)."
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "us"
            }
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 2025
            }
          }
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Manufacturer summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManufacturerSummary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/car": {
      "get": {
        "tags": [
          "Vehicles"
        ],
        "summary": "Single car detail",
        "description": "Returns the full record for one make/model in a country/year. Authentication required: a request with no session and no X-API-Key is refused with 401 `authentication_required` and no data, and a crawler signal changes nothing. For a market-year whose score_scale declares product = \"figures\" (the product US and CA sell) the plan decides what comes back:\nSigned in on the FREE plan, on ANY car within 25 different cars per UTC day: the four open specs (fuel economy, CO2, crash rating, recalls), every other spec withheld with reason requires_account and counted in figures_requiring_account. `access` is `\"account_required\"`, and the body carries `reads_left_today` and `quota` (`limit`, `resets_at`) and `counts_toward_limit: true`. Reading a car already read that day costs nothing.\nThe TOP 10 (the ten most-registered cars of the market-year, `teaser: true`) are not counted: the same four open specs, `counts_toward_limit: false`, `quota` but no `reads_left_today`, never added to the day's set and never refused by the cap, even after 25 other cars.\nSigned in on the FREE plan, asking for a car outside the Top 10 that it has not read today after 25 different cars: 429, code `daily_car_limit`, `reads_left_today: 0`, `quota.resets_at` and a `Retry-After` header. Cars already read that day and the Top 10 stay open until 00:00 UTC.\nSigned in on the FREE plan when the per-account counter cannot be read or written: 503, code `quota_unavailable`, `Retry-After: 30`, `Cache-Control: private, no-store`. The counter fails closed, so the car is not served. Does not apply to a Top-10 car or to a Data or API plan.\nSigned in on Data or API — the full record always, `access: \"paid\"`, on any car, with no daily limit.\nA market-year that still publishes a composite requires a paid API key and previews the same way /api/v1/market does.",
        "parameters": [
          {
            "name": "make",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "model",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Matched as a case-insensitive substring of the model name."
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 2025
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "us"
            }
          }
        ],
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Car detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "car": {
                      "$ref": "#/components/schemas/Car"
                    },
                    "country": {
                      "type": "string"
                    },
                    "year": {
                      "type": "integer"
                    },
                    "access": {
                      "type": "string",
                      "enum": [
                        "account_required",
                        "paid"
                      ],
                      "description": "product = \"figures\" only. `account_required` is a free account on any car within its 25 a day, or on a Top-10 car at any time: the public set only, the rest named and locked. (The 26th different non-Top-10 car gets a 429, not this body.) `paid` is Data or API: the full record, on any car."
                    },
                    "reads_left_today": {
                      "type": "integer",
                      "description": "A free account only, on a counted car. Different cars this account can still read today (UTC), after this one. Absent on a Top-10 car, which this read did not count."
                    },
                    "counts_toward_limit": {
                      "type": "boolean",
                      "description": "A free account only. False on a Top-10 car (never counted against the 25), true on any other."
                    },
                    "quota": {
                      "type": "object",
                      "description": "A free account only.",
                      "properties": {
                        "limit": {
                          "type": "integer",
                          "example": 25
                        },
                        "resets_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "The next 00:00 UTC."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/DailyCarLimit"
          },
          "503": {
            "$ref": "#/components/responses/QuotaUnavailable"
          }
        }
      }
    },
    "/api/v1/vin": {
      "get": {
        "tags": [
          "VIN"
        ],
        "summary": "VIN decode and ranking match",
        "description": "Decodes a VIN via the NHTSA vPIC API and attempts to match the decoded\nmake/model to a car in the rankings for the given country/year. Requires\na paid API key.\n",
        "parameters": [
          {
            "name": "vin",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 11,
              "maxLength": 17
            },
            "description": "Vehicle Identification Number (min 11 chars)."
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "us"
            }
          }
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "VIN decoded and matched",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "vin": {
                      "type": "string"
                    },
                    "decoded": {
                      "$ref": "#/components/schemas/VinDecoded"
                    },
                    "ranking_match": {
                      "nullable": true,
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Car"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/images": {
      "get": {
        "tags": [
          "Media"
        ],
        "summary": "Image manifests, logos, and car images",
        "description": "Verified, commercially-licensed car photographs. Every image was checked per file against a commercial-use licence allowlist and carries its licence, its author and a link to its source; attribution is a condition of the CC-BY and CC-BY-SA licences most of them use.\n\nBy \"type\":\n- car: one image for a make and model. Not year-specific — a photograph of a model illustrates it across the years we publish. Add powertrain for the photo made for that class: an electric or hydrogen car never gets a petrol car's photo, so it can come back {found:false} where the plain make and model has one. Returns {found:false} when no image qualified, which is a normal answer rather than an error.\n- manifest (default): a page of the whole set, with limit and offset. Keys are make/model, plus /powertrain for a per-powertrain photo.\n- logos: always empty. Manufacturer marks are trademarks, and a permissive image licence is not a trademark licence.\n\nAuthentication required (any plan): 401 with no session and no key.\n",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "manifest",
              "enum": [
                "manifest",
                "logos",
                "car"
              ]
            }
          },
          {
            "name": "make",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Required when type=car."
          },
          {
            "name": "model",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Required when type=car."
          },
          {
            "name": "powertrain",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "gas",
                "diesel",
                "hybrid",
                "phev",
                "ev",
                "hydrogen"
              ]
            },
            "description": "With type=car, the photo made for this powertrain class. Omitted, the plain make and model photo."
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "us"
            }
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "2025"
            }
          }
        ],
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Image data",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "manifest": {
                          "type": "object",
                          "additionalProperties": true
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "logos": {
                          "type": "object",
                          "additionalProperties": true
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "image": {
                          "type": "object",
                          "additionalProperties": true
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/usage": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "API key usage statistics",
        "description": "Returns the calling key's tier, rate limit, country access, and today's request count against it. Requires a valid API key.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Usage stats",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "invalid_api_key (missing or unrecognised), api_key_revoked (existed, was turned off) or api_key_expired (past its expiry date).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/verify": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Verify an API key",
        "description": "Validates the X-API-Key header and returns the key's tier, countries, and status. Note - this endpoint uses GET (not POST) in the current implementation.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Key is valid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "example": true
                    },
                    "tier": {
                      "type": "string",
                      "example": "api"
                    },
                    "countries": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "status": {
                      "type": "string",
                      "example": "active"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/keys": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "List the account's API keys",
        "description": "Every key the signed-in account holds, newest first, with name, prefix, state (active, expiring, expired, revoked or suspended), expiry, last-used time and revoked time, plus today's account usage. Never the key itself. Requires a signed-in session, not an API key. An account without the api plan gets `api_access: false`, an empty list and a message.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account's keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Create an API key",
        "description": "Creates a key on the api plan. The plaintext key is returned once, here, and nowhere else. Body (optional): `name` (up to 60 characters) and `expires_in_days` (1 to 730, or null for no expiry; 90 if omitted). An account holds at most 10 active keys (409 `key_limit_reached`). Requires a signed-in session and an active subscription that includes API access (403 `plan_required` otherwise).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 60,
                    "example": "Production"
                  },
                  "expires_in_days": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 1,
                    "maximum": 730,
                    "example": 90
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The key was created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_key": {
                      "type": "string",
                      "example": "cs_live_ab12cd34ef56gh78ij90kl12mn34op56"
                    },
                    "key": {
                      "$ref": "#/components/schemas/ApiKeyRecord"
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A name or expiry that is not valid (code invalid_request).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The plan has no API access (code plan_required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The account already holds 10 active keys (code key_limit_reached).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/keys/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The key's id from the list (key_ and 16 hex characters).",
          "schema": {
            "type": "string",
            "example": "key_0123456789abcdef"
          }
        }
      ],
      "patch": {
        "tags": [
          "Auth"
        ],
        "summary": "Rename a key or change its expiry",
        "description": "Send `name`, `expires_in_days` (counted from now; null removes the expiry), or both. Only a live key can change: an expired or revoked one is 409 `key_inactive`, so create a new key instead.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "expires_in_days": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 1,
                    "maximum": 730
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "$ref": "#/components/schemas/ApiKeyRecord"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Nothing to change, or a value that is not valid (code invalid_request).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such key on this account (code key_not_found).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The key is expired or revoked (code key_inactive).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Auth"
        ],
        "summary": "Revoke a key",
        "description": "Turns the key off at once; requests that carry it are refused with 401 `api_key_revoked`. Revoking is permanent and repeating it is harmless.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The key is revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "type": "boolean",
                      "example": true
                    },
                    "already": {
                      "type": "boolean",
                      "description": "Present and true when the key was revoked before."
                    },
                    "key": {
                      "$ref": "#/components/schemas/ApiKeyRecord"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such key on this account (code key_not_found).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/account": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Account and subscription summary, by API key",
        "description": "The calling key's tier, country access and status, plus the subscription row it belongs to (email, billing cycle, current period end, whether it cancels at period end). Keyed, not sessioned — this is the api-tier programmatic view; the account PAGE reads the richer session-based /api/v1/auth/me instead.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Account summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tier": {
                      "type": "string",
                      "example": "api"
                    },
                    "countries": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "status": {
                      "type": "string",
                      "example": "active"
                    },
                    "subscription": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "email": {
                          "type": "string",
                          "format": "email"
                        },
                        "billing_cycle": {
                          "type": "string",
                          "enum": [
                            "monthly",
                            "annual"
                          ]
                        },
                        "current_period_end": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "cancel_at_period_end": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/ask": {
      "post": {
        "tags": [
          "Vehicles"
        ],
        "summary": "Ask a question about a market-year, in natural language",
        "description": "A derived endpoint like /api/v1/compare, /api/v1/manufacturer and /api/v1/vin: matches the question's keywords against the market-year's cars and returns the closest ones. Requires a Clerk session or an API key (either authenticates); the api plan is what the derived endpoints themselves need — a caller on a lower plan is refused (403) and told the same data is free to read at /api/v1/market and /api/v1/car.",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "us"
            }
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 2025
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "question": {
                    "type": "string",
                    "example": "What's the most fuel-efficient SUV?"
                  }
                }
              }
            }
          },
          "description": "question in the body, or ?q= in the query string — one of the two is required."
        },
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Matches, best first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "question": {
                      "type": "string"
                    },
                    "country": {
                      "type": "string"
                    },
                    "year": {
                      "type": "integer"
                    },
                    "matches": {
                      "type": "array",
                      "description": "The closest cars, best keyword overlap first. A specs market's matches carry make, model, powertrain and published_figures; a composite market's carry rank, composite_score and key_factor instead — never both, since a specs market has no rank to offer.",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Signed in, but not on the api plan — see free_alternative.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/checkout": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "Create a Paddle checkout session",
        "description": "Creates a Paddle transaction for the requested tier and billing cycle.\nReturns a hosted checkout URL. Requires a Clerk session (401 with\nsign_in_required=true without one). Who is buying, and the email the\nPaddle customer is filed under, come from that session — the verified\nprimary address Clerk holds — and never from the request body.\nAn account holds one plan at a time: a signed-in caller whose subscription is\nstill active receives 409 with already_subscribed=true. A plan with no price\nin the worker's Paddle environment receives 409 with plan_not_on_sale=true.\nWhile the worker runs Paddle's SANDBOX, only the accounts named in its\nPADDLE_SANDBOX_TESTERS may buy; anyone else receives 409 with\nsales_not_open=true (\"Paid plans open at launch.\"), and /api/v1/countries\nreports every plan unsellable.\nMarkets stay fixed for the life of the subscription.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tier",
                  "billing_cycle"
                ],
                "properties": {
                  "tier": {
                    "type": "string",
                    "enum": [
                      "data",
                      "api"
                    ],
                    "description": "Only the tiers on sale. A retired tier's price is gone from every Paddle environment, so checkout can only ever refuse it (409 plan_not_on_sale) and a customer must not be invited to type a name that can never succeed."
                  },
                  "billing_cycle": {
                    "type": "string",
                    "enum": [
                      "monthly",
                      "annual"
                    ]
                  },
                  "countries": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "DO NOT SEND THIS. Every paid plan covers every launched market, so there is nothing to select and any list is REFUSED. The field is documented because a caller written against the older plans may still send one, and a silent acceptance would record a plan narrower than the one sold."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "IGNORED. The email is the session's."
                  },
                  "clerk_user_id": {
                    "type": "string",
                    "description": "IGNORED. The user is the session's."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout session created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "transaction_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Not sold to this caller now: already_subscribed, plan_not_on_sale, or sales_not_open (sandbox, and not a named tester).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/webhook": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "Paddle webhook receiver",
        "description": "Receives Paddle subscription lifecycle events. Verifies the\nPaddle-Signature header (HMAC-SHA256) against PADDLE_WEBHOOK_SECRET,\nthen provisions API keys on transaction.completed /\nsubscription.created and suspends keys on cancellation. Idempotent via\nthe webhook_events table. Not intended for direct client use.\n\nThe plan stored is the one whose PRICE the event's items carry, in the\nworker's Paddle environment; custom_data.tier may only agree with it.\nAn event whose items carry no plan's price, the prices of more than one\nplan, or a custom_data.tier that disagrees is refused with 422 and not\nrecorded, so it stays visible as a failed delivery. A\nsubscription.updated that leaves the subscription active re-derives the\nplan from its items the same way. In sandbox, a subscription for an\naccount not named in PADDLE_SANDBOX_TESTERS is refused the same way.\n",
        "security": [],
        "parameters": [
          {
            "name": "Paddle-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ts=<timestamp>;h1=<hmac> signature from Paddle."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Event received",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "received": {
                      "type": "boolean",
                      "example": true
                    },
                    "duplicate": {
                      "type": "boolean",
                      "description": "True if event already processed."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Refused and not recorded — see above.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/auth/me": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Get current user from Clerk session",
        "description": "Validates the Clerk session token (from Authorization Bearer or the\n__clerk_db_jwt cookie) and returns the linked subscription tier and\ncountries. Unauthenticated requests return 200 with a free-tier shape\n(authenticated=false), not 401 — this is a \"who is calling, if anyone\"\ncheck, not a gate.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "responses": {
          "200": {
            "description": "Session info",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthMe"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/logout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Logout",
        "description": "Clears the caller's session. Returns { logged_out: true }.",
        "security": [],
        "responses": {
          "200": {
            "description": "Logged out",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "logged_out": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/schema": {
      "get": {
        "tags": [
          "Meta"
        ],
        "summary": "OpenAPI spec",
        "description": "Returns this OpenAPI 3.0 document as JSON.",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI JSON",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/llms.txt": {
      "get": {
        "tags": [
          "Meta"
        ],
        "summary": "LLM discovery file",
        "description": "Returns the llms.txt plain-text file describing the service and API for LLM agents.",
        "security": [],
        "responses": {
          "200": {
            "description": "llms.txt content",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key issued on subscription activation. Format cs_live_<32 chars>."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Clerk session token (Authorization Bearer <token>)."
      }
    },
    "headers": {
      "XDataAttribution": {
        "description": "The datasets in this response whose licences require an acknowledgement, by the name the licence register uses, comma-separated (e.g. \"nrcan\"). Present only when one is owed. The acknowledgements themselves are in the response body's `attribution` field, in the licence's own words — they are there and not here because a header may carry only Latin-1 and the words may not be altered to fit it.",
        "schema": {
          "type": "string",
          "example": "nrcan"
        }
      },
      "RateLimitLimit": {
        "description": "The calling key's daily request cap Present on every response an API key authenticated.",
        "schema": {
          "type": "integer",
          "example": 1000
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current UTC day, this one already deducted.",
        "schema": {
          "type": "integer",
          "example": 998
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the count reads zero again (the next UTC midnight) — the IETF RateLimit header field's own unit.",
        "schema": {
          "type": "integer",
          "example": 43200
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Bad request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No credential, or one that does not validate. code distinguishes why: invalid_api_key (missing, mistyped or never issued), api_key_revoked (issued, then turned off), api_key_expired (past its expiry date) or authentication_required (an endpoint that needs either a session or a key, and got neither). Never cached (`Cache-Control: private, no-store`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PlanRequired": {
        "description": "Authenticated, but the plan does not read this. code is plan_required: an X-API-Key belonging to an account whose plan carries no API key, or a paid-only endpoint asked by a free account (`upgrade_required: true`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "DailyCarLimit": {
        "description": "A free account asked GET /api/v1/car for a car it has not read today after already reading 25 different cars in the UTC day (Top-10 cars are not counted and are never refused). code is `daily_car_limit`, and the body carries `upgrade_required: true`, `limit`, `reads_left_today: 0` and `quota.resets_at`, and the response has `Retry-After` (seconds to 00:00 UTC). Never cached (`Cache-Control: private, no-store`). Cars already read that day and Top-10 cars still answer 200; a Data or API plan is never counted.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "QuotaUnavailable": {
        "description": "A free account asked GET /api/v1/car for a car outside the Top 10 and the per-account daily counter could not be read or written. code is `quota_unavailable`. The counter fails closed: the car is not served. `Retry-After: 30`. Never cached (`Cache-Control: private, no-store`). Never returned to a Data or API plan, or for a Top-10 car.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before trying again.",
            "schema": {
              "type": "integer",
              "example": 30
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MarketNotAvailable": {
        "description": "The country has not launched",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/MarketNotAvailable"
            }
          }
        }
      },
      "RateLimited": {
        "description": "The account's daily allowance is used up. code is rate_limit_exceeded.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the limit resets.",
            "schema": {
              "type": "integer",
              "example": 43200
            }
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "example": "rate_limit_exceeded"
                },
                "docs_url": {
                  "type": "string",
                  "format": "uri"
                },
                "limit": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "What went wrong",
            "in a sentence a person reads.": null
          },
          "code": {
            "type": "string",
            "description": "A stable snake_case identifier to switch on — invalid_api_key, api_key_revoked, api_key_expired, authentication_required, plan_required, key_limit_reached, rate_limit_exceeded, and so on. Present on every refusal built through errorResponse(); an older route not yet moved onto it may omit it."
          },
          "docs_url": {
            "type": "string",
            "format": "uri",
            "description": "Where /developers/ explains this code.",
            "example": "https://carstandings.com/developers/#errors"
          }
        }
      },
      "Country": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "example": "us"
          },
          "name": {
            "type": "string",
            "example": "United States"
          },
          "region": {
            "type": "string",
            "example": "North America"
          },
          "years": {
            "type": "string",
            "example": "1984-2025"
          },
          "cars": {
            "type": "integer",
            "example": 20285
          },
          "sellable": {
            "type": "boolean",
            "description": "Whether a paid plan can include this country. Always true: only launched countries are listed, and every launched country can be sold."
          }
        }
      },
      "Factor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "fuel_efficiency"
          },
          "label": {
            "type": "string",
            "example": "Fuel Efficiency"
          },
          "description": {
            "type": "string"
          },
          "weight": {
            "type": "number",
            "format": "float",
            "example": 0.14
          },
          "invert": {
            "type": "boolean",
            "description": "True if lower values are better."
          },
          "unit": {
            "type": "string",
            "example": "mpg"
          }
        }
      },
      "Car": {
        "type": "object",
        "properties": {
          "rank": {
            "type": "integer",
            "nullable": true,
            "description": "A composite market only. Absent entirely from a row in a market-year whose score_scale declares product = \"figures\": nothing there is ranked against anything else."
          },
          "make": {
            "type": "string",
            "example": "Toyota"
          },
          "model": {
            "type": "string",
            "example": "Camry"
          },
          "powertrain": {
            "type": "string",
            "example": "hybrid"
          },
          "teaser": {
            "type": "boolean",
            "description": "product = \"figures\" only. True on the ten most-registered nameplates of the market-year, by NY DMV registration counts (Canada carries the same ten as the US); every powertrain row of a teaser nameplate carries it. A FREE account reads the four open specs on ANY car, but the 25-different-cars-a-day limit does not count a teaser car: these ten are always readable and never use up a slot. The static website shows a signed-out visitor only these ten. A Data or API plan reads every car in full. There is no crawler exception."
          },
          "composite_score": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "A composite market only. Absent from a product = \"figures\" row, as are composite_exact, tie_break_key, factor_scores and key_factor."
          },
          "published_figures": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "product = \"figures\" only. The RAW MEASURED value of each spec this market-year publishes, in that spec's own unit, present only where a publisher measured that quantity FOR THIS CAR. Keys are spec ids (fuel_economy, fuel_economy_mpge, co2, co2_charge_weighted, horsepower, horsepower_engine, weight, test_weight, warranty, recalls, crash_rating, complaint_rate, price). Six of those are three pairs of DIFFERENT quantities, never ordered together: co2 is the publisher's tailpipe rating for the car driven as rated — which an electric or hydrogen car's measured zero is — and co2_charge_weighted a plug-in hybrid's composite, in which the publisher credits a share of the miles to grid electricity at zero tailpipe, so it is smaller than what the car emits with the battery empty; horsepower is the car's rated output and horsepower_engine a hybrid's combustion engine alone; weight is the kerb weight and test_weight EPA's loaded test class, about 300 lb above it. Read a CO2 spec with co2_source, which names its basis. A spec missing here is explained in figures_withheld; it is never defaulted, and never 0."
          },
          "figure_provenance": {
            "type": "object",
            "description": "product = \"figures\" only. Per spec id: field (the row field the value is published in), unit, label, sources (the loader tokens that published it), publishers (the organisations behind them), basis (the row fields stating the spec's basis and their values — e.g. horsepower_basis, curb_weight_source and curb_weight_basis, the complaint rate's complaints_rate_basis and production_us, a price's price_source, price_basis and price_as_of) and also (the same quantity in another published unit), plus unit_basis on CO2 rows, which says which unit the publisher stated (g/mi in the US, g/km in Canada) and how the other was derived. Quote a spec with its basis."
          },
          "figures_withheld": {
            "type": "object",
            "description": "product = \"figures\" only. Per spec id this row does NOT carry: reason (source_lacks_field, provenance, licence_blocked, impossible, suppressed, not_this_quantity, or requires_account) and detail, a sentence saying why. \"not_this_quantity\" means the spec is not the same measurement for this car — an electric car's MPGe is not a petrol car's mpg. \"requires_account\" is the one reason that is not a fact about the DATA: it is added by the account gate to every spec outside the public set, for a caller on the free plan — the row's own figures_measured and figure_provenance are unaffected, and the same car returns every spec with no requires_account reason at all on the Data or API plan."
          },
          "figures_measured": {
            "type": "integer",
            "description": "product = \"figures\" only. How many of this market-year's specs this row carries."
          },
          "figures_in_market": {
            "type": "integer",
            "description": "product = \"figures\" only. How many of this market-year's published specs are that quantity for THIS car. Not a denominator for a score — there is none."
          },
          "fuel_efficiency": {
            "type": "number",
            "description": "MPG or L/100km."
          },
          "co2_g_per_km": {
            "type": "number"
          },
          "co2_source": {
            "type": "string",
            "enum": [
              "tailpipe_rated",
              "charge_weighted",
              "derived_from_mpg"
            ],
            "description": "What the CO2 spec IS, not who published it — the publisher is in `sources` and in the spec's own `publishers`. \"tailpipe_rated\" is what the car emits at the tailpipe driven as rated on the test cycle, which an electric or hydrogen car's measured zero also is. \"charge_weighted\" is a plug-in hybrid's composite, weighted by the share of the miles the publisher credits to grid electricity at zero tailpipe, and so is SMALLER than what the car emits with the battery empty — it is not comparable with a tailpipe rating, which is why the two are published as separate specs (co2 and co2_charge_weighted) and never ordered in one list. The basis is decided PER ROW, never by which file the row came from: EPA states a utility factor (combinedUF) per vehicle, and a row from NRCan's plug-in file is checked against its own stated fuel consumption, so a car listed as a plug-in whose CO2 is what its petrol consumption burns is published as tailpipe_rated. A 2024-2025 Lamborghini Revuelto is that car: 8 km of electric range, and a spec 1.3% ABOVE its own petrol-only arithmetic, so it carries no electric credit to state. \"derived_from_mpg\" is computed from the row's own fuel economy and is not a measurement: no CO2 spec is published for such a row. Quote the spec with its basis."
          },
          "safety_crash": {
            "type": "number",
            "description": "NHTSA stars (1-5)."
          },
          "safety_recalls": {
            "type": "integer"
          },
          "price_value": {
            "type": "number"
          },
          "reliability": {
            "type": "number"
          },
          "owner_satisfaction": {
            "type": "number"
          },
          "horsepower": {
            "type": "number"
          },
          "horsepower_basis": {
            "type": "string",
            "enum": [
              "rated",
              "engine_only",
              "system",
              "motor_rated"
            ],
            "description": "What the horsepower is a rating of, and it is never the same number on two of these: \"rated\" an engine's rating (a petrol or diesel car), \"engine_only\" a hybrid's or plug-in's engine alone, \"system\" the whole car's peak output, \"motor_rated\" the motors'. Quote the spec with its basis."
          },
          "curb_weight_kg": {
            "type": "number",
            "description": "Kerb weight in kilograms, where the row carries one. It is ONE named publisher's spec, confirmed by a second source — never an average or a blend of several publishers, which would rank cars partly by which agency weighed which trim. Read it with curb_weight_source, curb_weight_basis and curb_weight_corroborated_by."
          },
          "curb_weight_source": {
            "type": "string",
            "enum": [
              "nhtsa_cafe_market_data",
              "epa_certification",
              "nhtsa_ncap_bulk"
            ],
            "description": "The publisher of curb_weight_kg for this car, for US and Canadian rows. Other markets name their own national publisher."
          },
          "curb_weight_basis": {
            "type": "string",
            "enum": [
              "trim_average",
              "tested_vehicle",
              "crash_tested"
            ],
            "description": "What KIND of spec that kerb weight is, and the three are not interchangeable. \"trim_average\" is NHTSA's CAFE file: one row per compliance configuration, the curb weights of the trims matching it averaged by DOT staff — no single car weighs this. \"tested_vehicle\" is EPA's certification test results: the vehicle a manufacturer certified, one real car. \"crash_tested\" is NHTSA's NCAP file: the vehicle NHTSA weighed before crashing it, one arbitrary trim of the nameplate. Show the basis wherever you show the spec."
          },
          "curb_weight_corroborated_by": {
            "type": "string",
            "description": "The second source or sources that confirmed curb_weight_kg, as \"; \"-joined phrases each starting with a publisher name. A spec no second source confirms is not published. \"epa_testcar test weight N lb\" is the weakest of them: EPA's loaded test class must sit 0-1000 lb above the kerb weight, which bounds the spec rather than measuring it, and on most rows it is the only confirmation there is."
          },
          "price_usd": {
            "type": "number",
            "description": "The car's base price, in US DOLLARS. On United States rows of model years 2020, 2022 and 2024 it is the MSRP column of NHTSA's CAFE Model Market Data Input File, and it is an AVERAGE ACROSS TRIMS: NHTSA publishes one row per compliance configuration, and DOT staff \"averaged the MSRP and curb weights of the multiple trims that would match the row presented in the compliance record\". No single car is sold for this spec. It is published only where every configuration matching the car states the same price, so a car whose trims are priced differently carries none (about 31-64% of US rows have one, lowest in 2020). A spec the file rounds to a whole thousand and states for three or more different nameplates is a class placeholder rather than a price — the 2020 file gives 40,000 to fifteen nameplates, and the 4Runner's price was 36,120 — and no row carries one. THERE IS NO PRICE ON A CANADIAN ROW: this is a US-market price in US dollars, Canada is a different market with its own prices, and nothing here converts one into the other — a Canadian row's price_usd is absent, never a converted US spec. Other markets' rows carry their own national price register's spec; read price_source to see whose. It is a PUBLISHED FACT and not a ranking input: no score, depreciation estimate or five-year cost is derived from it."
          },
          "price_source": {
            "type": "string",
            "description": "The dataset the price came from, by the name the licence register uses. \"nhtsa_cafe_market_data\" is the US CAFE compliance file described under price_usd."
          },
          "price_basis": {
            "type": "string",
            "enum": [
              "trim_average",
              "sii_fiscal_valuation",
              "njkb_official_sale_value"
            ],
            "description": "What KIND of spec the price is, and they are not interchangeable. \"trim_average\" is the US CAFE file's: the prices of the trims matching one compliance configuration, averaged by DOT staff — not any trim's list price and not a transaction price. The file does not say whether freight and delivery are included, and a hand check against the manufacturers' own prices found both (the 2024 VW Jetta and Cadillac XT6 include it to the dollar; the 2020 Honda Clarity does not), so do not present it as a delivered price or as a price before freight. The other values are national tax registers' own valuations. Show the basis wherever you show the spec."
          },
          "price_as_of": {
            "type": "string",
            "description": "The date the priced source file was fetched."
          },
          "price_cross_check": {
            "type": "string",
            "description": "What a second source says about this price, always reported and never resolved for you. For the US CAFE spec the second source is NHTSA's VIN decoder (vPIC) BasePrice, which is TRIM-LEVEL where this spec is a trim average, and which vPIC states for model years 2020 and 2021 only. A string beginning \"DISAGREES\" means the two are far apart and BOTH are reported: neither replaces the other, and the published spec is still the CAFE file's. \"not checked\" means vPIC states no base price for the car, or was never asked for one — an absence of knowledge, not a statement that there is no price."
          },
          "test_weight_kg": {
            "type": "number",
            "description": "EPA test weight in kilograms, a loaded test class about 300 lb above kerb weight. A different quantity from curb_weight_kg; never compare or relabel one as the other. Which one a ranking used is score_scale.weight_field."
          },
          "weight_efficiency": {
            "type": "number"
          },
          "warranty": {
            "type": "number",
            "description": "Years."
          },
          "depreciation": {
            "type": "number",
            "description": "Percent."
          },
          "insurance": {
            "type": "number",
            "description": "USD/year."
          },
          "tco_5yr_total": {
            "type": "number",
            "description": "USD."
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every dataset cited by this row, by the name the licence register uses: each one that put a published value on it, including datasets named only in a field's own provenance key rather than in the merge (the NCAP crash-test file, the VIN decoder, EPA's Test Car List). Reproduce any acknowledgement the response's `attribution` field carries wherever you publish these specs."
          },
          "contributing_sources": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The narrower list the confidence label is counted from: the datasets that measured something PUBLISHED ON THIS ROW. A table we compile ourselves (the warranty terms) is not in it, and neither is a dataset that was read and produced nothing about this car. A subset of `sources`."
          },
          "contributing_publishers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "EPA",
              "NHTSA",
              "NRCan"
            ],
            "description": "`contributing_sources` folded onto the organisations that published them, named once each: NHTSA's recall API, its bulk NCAP file, vPIC, its production reports and its CAFE files are one publisher, and EPA's label files, Test Car List and Automotive Trends are one. This is what the confidence label counts, and what to quote when you say how much independent evidence stands behind a car."
          },
          "source_count": {
            "type": "integer",
            "description": "The length of `contributing_publishers` — publishers, not datasets."
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ],
            "description": "How much is known about the car, not how good it is. HIGH: measured on at least 75% of the market-year's factor weight, by at least 3 independent publishers. MEDIUM: at least 60% of that weight, by at least 2. LOW: anything less. GET /api/v1/factors returns the rule with its thresholds."
          },
          "confidence_basis": {
            "type": "object",
            "description": "What produced that label: publishers (the names counted), datasets (`contributing_sources`), weight_coverage (the row's share of the market-year's factor weight) and rule (the rule in words)."
          }
        }
      },
      "Rankings": {
        "type": "object",
        "properties": {
          "country": {
            "type": "string"
          },
          "year": {
            "type": "integer"
          },
          "sort": {
            "type": "string"
          },
          "cars": {
            "type": "array",
            "description": "product = \"figures\": a spec sort returns the cars carrying that spec, ordered by it and then by the documented tie-break, followed by the cars that do not carry it (each row's figures_withheld says why). sort=catalogue returns every car in tie-break order and claims nothing. No row carries rank, composite_score or factor_scores.\nA composite market, sort=overall: ranked cars in rank order, then the cars with too little measured data to rank (rank and composite_score null, unranked_reason and unranked_detail set). A factor sort: the cars with a score for that factor, best first, then the rest in overall rank order. rank is always the overall rank.",
            "items": {
              "$ref": "#/components/schemas/Car"
            }
          },
          "total_cars": {
            "type": "integer",
            "description": "Every car of the market-year, listed or not."
          },
          "listed_cars": {
            "type": "integer",
            "description": "product = \"figures\" only. The cars IN this list — those carrying a value for the chosen spec. Equal to total_cars for sort=catalogue."
          },
          "unlisted_cars": {
            "type": "integer",
            "description": "product = \"figures\" only. The cars that carry no value for the chosen spec. They follow the list and are not ordered by it."
          },
          "claim": {
            "type": "string",
            "nullable": true,
            "description": "product = \"figures\" only. The ONE claim this list makes, e.g. \"Most horsepower\". null for sort=catalogue, which makes none."
          },
          "figure": {
            "type": "string",
            "nullable": true,
            "description": "product = \"figures\" only. The spec id this list is ordered by, or null for the catalogue."
          },
          "unit": {
            "type": "string",
            "nullable": true,
            "description": "product = \"figures\" only. The unit that spec is published in."
          },
          "tie_break": {
            "type": "string",
            "nullable": true,
            "description": "product = \"figures\" only. The documented order inside a tie, so any list is reproducible from the published rows."
          },
          "ranked_cars": {
            "type": "integer",
            "description": "A composite market only. The cars that carry a rank."
          },
          "scored_cars": {
            "type": "integer",
            "description": "A composite market only. The cars that carry a score for the sort: the factor's own score, or for overall a composite (the ranked cars). The rest follow them."
          },
          "score_scale": {
            "type": "object",
            "nullable": true,
            "description": "What the market-year's ranking is made of: factors_used {factor: renormalised weight}, factors_absent {factor: {reason, detail}}, n, ceiling, ranked, unranked, sellable, failures. null when none is published with these rows."
          },
          "attribution": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "Contains information licensed under the Open Government Licence – Canada."
            ],
            "description": "The acknowledgements the licences behind these rows require, verbatim and in full, wherever you publish the data or anything derived from it. Absent when the rows carry no such obligation — which is not permission to claim one. The same obligations are named by dataset in the X-Data-Attribution response header. Every endpoint that answers with rows carries this field on the same terms."
          },
          "attribution_sources": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "nrcan"
            ],
            "description": "The datasets those acknowledgements are owed for, by the register's own name — the body's copy of the X-Data-Attribution header, so a caller can tell which specs carry which obligation."
          },
          "attribution_unavailable": {
            "type": "boolean",
            "description": "Present and true when the obligations for an answer computed in SQL (the aggregates) could not be read. The data is still returned; treat it as attribution owed but unnamed rather than as none owed."
          },
          "error": {
            "type": "string",
            "description": "Present when data is unavailable for the country/year/sort."
          }
        }
      },
      "MarketNotAvailable": {
        "type": "object",
        "description": "The answer for a country that has not launched, on every endpoint and for every caller.",
        "properties": {
          "error": {
            "type": "string",
            "example": "This market is not available."
          },
          "market_not_available": {
            "type": "boolean",
            "example": true
          },
          "available_markets": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The launched market codes."
          }
        }
      },
      "Stats": {
        "type": "object",
        "properties": {
          "country": {
            "type": "string"
          },
          "year": {
            "type": "integer"
          },
          "total_cars": {
            "type": "integer"
          },
          "score_range": {
            "type": "object",
            "properties": {
              "min": {
                "type": "number",
                "nullable": true
              },
              "max": {
                "type": "number",
                "nullable": true
              },
              "avg": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "co2_g_per_km": {
            "type": "object",
            "description": "Tailpipe-rated rows only — the quantity the co2 spec and the \"Lowest tailpipe CO2\" list carry, including an electric or hydrogen car's measured 0. A plug-in's charge-weighted composite is NOT in here; it is in co2_g_per_km_charge_weighted.",
            "properties": {
              "min": {
                "type": "number",
                "nullable": true
              },
              "max": {
                "type": "number",
                "nullable": true
              },
              "avg": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "co2_g_per_km_charge_weighted": {
            "type": "object",
            "description": "The plug-in rows whose CO2 credits part of the miles to grid electricity (the co2_charge_weighted spec). PRESENT ONLY where the market-year has such a row: an absent key means no row carries that basis, never that the market has no plug-in hybrids.",
            "properties": {
              "min": {
                "type": "number",
                "nullable": true
              },
              "max": {
                "type": "number",
                "nullable": true
              },
              "avg": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "tco_5yr": {
            "type": "object",
            "description": "Present only for a market-year that publishes a composite. One publishing measured specs derives no cost of ownership, and the key is absent rather than three nulls.",
            "properties": {
              "min": {
                "type": "number",
                "nullable": true
              },
              "max": {
                "type": "number",
                "nullable": true
              },
              "avg": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "horsepower": {
            "type": "object",
            "description": "The whole car's rated output only — a petrol or diesel engine's rating, an electric or fuel-cell car's motor rating, or a hybrid's system output. A hybrid published with its combustion engine ALONE is not in here; it is in horsepower_engine_only, a smaller number measuring a different thing.",
            "properties": {
              "min": {
                "type": "number",
                "nullable": true
              },
              "max": {
                "type": "number",
                "nullable": true
              },
              "avg": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "horsepower_engine_only": {
            "type": "object",
            "description": "The rows whose only published power spec is the combustion engine's (the horsepower_engine spec). PRESENT ONLY where the market-year has such a row.",
            "properties": {
              "min": {
                "type": "number",
                "nullable": true
              },
              "max": {
                "type": "number",
                "nullable": true
              },
              "avg": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "no_composite": {
            "type": "string",
            "nullable": true,
            "description": "Why this market-year publishes no overall score, in full. Returned by every market-year whose score_scale declares product = \"figures\", in place of score_range, top_5 and bottom_5."
          },
          "attribution": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "Contains information licensed under the Open Government Licence – Canada."
            ],
            "description": "The acknowledgements the licences behind these rows require, verbatim and in full, wherever you publish the data or anything derived from it. AN AGGREGATE IS STILL THE LICENSED INFORMATION: a CO2 range computed from NRCan's specs is derived from them and the acknowledgement is owed. Absent when the rows carry no such obligation — which is not permission to claim one."
          },
          "attribution_sources": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "nrcan"
            ],
            "description": "The datasets those acknowledgements are owed for, by the register's own name — the body's copy of the X-Data-Attribution header."
          },
          "attribution_unavailable": {
            "type": "boolean",
            "description": "Present and true when the obligations for an answer computed in SQL could not be read. The data is still returned; treat it as attribution owed but unnamed rather than as none owed."
          },
          "powertrain_breakdown": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "top_5": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "rank": {
                  "type": "integer"
                },
                "make": {
                  "type": "string"
                },
                "model": {
                  "type": "string"
                },
                "score": {
                  "type": "number"
                }
              }
            }
          },
          "bottom_5": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "rank": {
                  "type": "integer"
                },
                "make": {
                  "type": "string"
                },
                "model": {
                  "type": "string"
                },
                "score": {
                  "type": "number"
                }
              }
            }
          }
        }
      },
      "ManufacturerSummary": {
        "type": "object",
        "properties": {
          "manufacturer": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "year": {
            "type": "integer"
          },
          "total_models": {
            "type": "integer"
          },
          "avg_composite_score": {
            "type": "number",
            "nullable": true
          },
          "best_model": {
            "$ref": "#/components/schemas/Car"
          },
          "worst_model": {
            "$ref": "#/components/schemas/Car"
          },
          "powertrain_breakdown": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "models": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "rank": {
                  "type": "integer"
                },
                "model": {
                  "type": "string"
                },
                "score": {
                  "type": "number"
                },
                "powertrain": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "VinDecoded": {
        "type": "object",
        "properties": {
          "make": {
            "type": "string",
            "nullable": true
          },
          "model": {
            "type": "string",
            "nullable": true
          },
          "year": {
            "type": "integer",
            "nullable": true
          },
          "fuel_type": {
            "type": "string",
            "nullable": true
          },
          "body_class": {
            "type": "string",
            "nullable": true
          },
          "engine": {
            "type": "string",
            "nullable": true
          },
          "displacement": {
            "type": "string",
            "nullable": true
          },
          "cylinders": {
            "type": "string",
            "nullable": true
          },
          "transmission": {
            "type": "string",
            "nullable": true
          },
          "drive_type": {
            "type": "string",
            "nullable": true
          },
          "plant": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ApiKeyRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "key_0123456789abcdef"
          },
          "name": {
            "type": "string",
            "example": "Production"
          },
          "prefix": {
            "type": "string",
            "example": "cs_live_ab12"
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "expiring",
              "expired",
              "revoked",
              "suspended"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "null = never expires."
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Updated at most once an hour."
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "KeyList": {
        "type": "object",
        "properties": {
          "api_access": {
            "type": "boolean"
          },
          "tier": {
            "type": "string",
            "example": "api"
          },
          "tier_label": {
            "type": "string",
            "example": "API"
          },
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeyRecord"
            }
          },
          "max_active_keys": {
            "type": "integer",
            "example": 10
          },
          "expiry_choices_days": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "example": [
              30,
              90,
              180,
              365
            ]
          },
          "default_expiry_days": {
            "type": "integer",
            "example": 90
          },
          "usage": {
            "type": "object",
            "nullable": true,
            "description": "Today's account-wide count; null when the plan has no API access.",
            "properties": {
              "limit": {
                "type": "integer",
                "example": 1000
              },
              "used": {
                "type": "integer"
              },
              "remaining": {
                "type": "integer"
              },
              "resets_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "message": {
            "type": "string",
            "description": "Present when the plan has no API access."
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "tier": {
            "type": "string"
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rate_limit": {
            "type": "integer",
            "description": "Daily request cap for the whole account",
            "shared by all its keys; 0 = unlimited.": null
          },
          "api_access": {
            "type": "boolean"
          },
          "requests_today": {
            "type": "integer",
            "description": "Requests the account's keys have made today",
            "this one included.": null
          },
          "limit": {
            "type": "integer",
            "nullable": true,
            "description": "The account's daily cap",
            "or null when the tier has none.": null
          },
          "used": {
            "type": "integer",
            "description": "Same value as requests_today",
            "under the name the quota fields share.": null
          },
          "remaining": {
            "type": "integer",
            "nullable": true,
            "description": "limit minus used",
            "or null when the tier has no cap.": null
          },
          "resets_at": {
            "type": "string",
            "format": "date-time",
            "description": "The next UTC midnight",
            "when today's count reads zero again.": null
          },
          "note": {
            "type": "string"
          }
        }
      },
      "AuthMe": {
        "type": "object",
        "properties": {
          "authenticated": {
            "type": "boolean"
          },
          "clerk_user_id": {
            "type": "string",
            "nullable": true
          },
          "tier": {
            "type": "string",
            "example": "free"
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "billing_cycle": {
            "type": "string",
            "nullable": true
          },
          "checkout_enabled": {
            "type": "boolean",
            "description": "Signed in only. Whether this account may buy in the worker's Paddle environment: always in live; in sandbox, only an account named in PADDLE_SANDBOX_TESTERS."
          },
          "checkout_closed_reason": {
            "type": "string",
            "nullable": true
          },
          "sellable_tiers": {
            "type": "object",
            "additionalProperties": true,
            "description": "Signed in only. The same shape as /api/v1/countries' sellable_tiers, answered for THIS account — which differs from the public answer only for a sandbox tester."
          },
          "error": {
            "type": "string",
            "nullable": true
          }
        }
      }
    }
  }
}