Dashboard
Plan and API usage for your account.
Current plan
100 monthly requests · All API endpoints · Basic usage analytics
- All API endpoints
- Basic usage analytics
Weekly usage
Requests made with your API key over the last 7 days.
MotorAPI v2.3.0
Introduction
MotorAPI decodes a 17-character VIN against government vehicle registries and returns the result as JSON. One endpoint, one header, and you get make, model, year, engine, fuel economy, safety equipment, recalls, owner complaints and defect investigations — each field tagged with how it was obtained.
Your first request
Every vehicle endpoint lives under /api/v1 and needs
your API key. This is the whole integration:
curl -H "X-API-Key: mot_live_YOUR_KEY" \
"https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352?units=metric"
import requests
resp = requests.get(
"https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352",
headers={"X-API-Key": "mot_live_YOUR_KEY"},
params={"units": "metric"},
timeout=10,
)
resp.raise_for_status()
vehicle = resp.json()["vehicle"]["identity"]
# {'Honda Accord 2003'}
print(vehicle["make"], vehicle["model"], vehicle["year"])
const res = await fetch(
"https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352?units=metric",
{ headers: { "X-API-Key": process.env.MOTORAPI_KEY } },
);
if (!res.ok) throw new Error(await res.text());
const { vehicle } = await res.json();
console.log(vehicle.identity.make, vehicle.identity.model, vehicle.identity.year);
Mileage is never estimated. Odometer figures are only returned when a
source published a dated reading for that exact VIN. When nothing exists,
estimatedCurrent is null by design and the response says so.
What you get back
A decode of a 2003 Honda Accord Coupe returns all of these sections. Any of them can be absent when the government source has no record — sections are omitted rather than returned empty.
| Section | Source | Match level |
|---|---|---|
| vinStructure | Decoded locally from the VIN (ISO 3779) | VIN_DERIVED |
| identity | NHTSA vPIC | VIN_DERIVED |
| engine | NHTSA vPIC | VEHICLE_CONFIGURATION |
| fuel, transmission, drivetrain | NHTSA vPIC | VEHICLE_CONFIGURATION |
| fuelEconomy | EPA fueleconomy.gov | VEHICLE_CONFIGURATION |
| weights, safetyEquipment | NHTSA vPIC | VEHICLE_CONFIGURATION |
| manufacturing | NHTSA vPIC (plant, city, country) | VEHICLE_CONFIGURATION |
| recalls | NHTSA Recalls | VEHICLE_CONFIGURATION |
| complaints | NHTSA ODI owner complaints | VEHICLE_CONFIGURATION |
| investigations | NHTSA ODI flat-file downloads | VEHICLE_CONFIGURATION |
| wmiRegistry | NHTSA WMI registry | VIN_DERIVED |
| canadaFuelConsumption | Natural Resources Canada | VEHICLE_CONFIGURATION |
| canadianVehicleSpecs | Transport Canada | VEHICLE_CONFIGURATION |
pricing, suspension, tires and
colors are always null. They are reserved for
licensed providers and are not filled with guesses.
Endpoints
| Method | Path | Auth | Counts against quota |
|---|---|---|---|
| GET | /api/v1/vin/{vin} | API key | Yes |
| GET | /api/v1/vehicles/{vin}/summary | API key | Yes |
| GET | /api/v1/vehicles/{vin}/odometer | API key | Yes |
| GET | /api/v1/health | None | No |
| GET | /api/v1/providers | None | No |
| GET | /api/v1/plans | None | No |
Conventions
- All requests and responses are
application/json. Responses are gzipped when the client sendsAccept-Encoding: gzip. - Successful responses set
success: true. Every failure setssuccess: falseand anerrorobject with a stablecode. - VINs are uppercased and trimmed before validation. Lowercase input works.
- CORS is open, so you can call the API directly from a browser. Keep your key out of client-side code anyway — proxy it through your own backend.
- VINs are redacted from server logs. They are never written to the request log.
Getting started
Authentication
Every vehicle endpoint requires an API key. Send it in the
X-API-Key header, or as a bearer token if the library
you use only speaks Authorization.
Key format
| Environment | Prefix | Example |
|---|---|---|
| Production | mot_live_ | mot_live_5hhq9t31_g4tV_6i5Ts2QHqEsO… |
| Test / development | mot_test_ | mot_test_5hhq9t31_g4tV_6i5Ts2QHqEsO… |
The public part of a key is its prefix — the first
segment — and it is safe to log. The secret part after it is stored
only as an HMAC-SHA256 digest salted with a server-side pepper, so
neither the database nor a log leak can be used to reconstruct a key.
Sending the key
# Preferred: dedicated header
curl -H "X-API-Key: mot_live_YOUR_KEY" \
"https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352"
# Equivalent, for tooling that only speaks bearer tokens
curl -H "Authorization: Bearer mot_live_YOUR_KEY" \
"https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352"
API_KEY = "mot_live_YOUR_KEY" # keep this in an env var
headers = {"X-API-Key": API_KEY}
resp = requests.get(url, headers=headers, timeout=10)
# Bearer works too
resp = requests.get(url, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=10)
const headers = { "X-API-Key": process.env.MOTORAPI_KEY };
$ch = curl_init("https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: mot_live_YOUR_KEY"]);
$data = json_decode(curl_exec($ch), true);
req, _ := http.NewRequest("GET", "https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352", nil)
req.Header.Set("X-API-Key", os.Getenv("MOTORAPI_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
var body struct {
Success bool `json:"success"`
Vehicle struct {
Identity struct {
Make string `json:"make"`
Model string `json:"model"`
Year int `json:"year"`
} `json:"identity"`
} `json:"vehicle"`
}
json.NewDecoder(res.Body).Decode(&body)
Failed authentication
A missing key, an unknown key, a revoked key and an expired key all return the identical body and status. That is deliberate — a caller cannot use the error to discover which keys exist.
401 Unauthorized
Cache-Control: no-store
{
"success": false,
"error": {
"code": "invalid_api_key",
"message": "A valid API key is required."
}
}
# No header — note the identical error for a wrong or revoked key
curl -i "https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352"
Keep keys server-side. CORS is open, so a key pasted into browser JavaScript is readable by anyone who opens the page. Proxy requests through your own endpoint.
Getting started
Rate limits & plans
Two independent limits apply to every authenticated request: a short-term burst limit that protects the service from bursts, and a monthly quota that is your billing allowance. They are separate, and one does not stand in for the other.
Plans
Free
$0 / month
100 requests / month
- 10 requests / minute burst
- All endpoints
- Basic analytics
Starter
$19 / month
5,000 requests / month
- 30 requests / minute burst
- All endpoints
- Standard analytics
Pro
$49 / month
25,000 requests / month
- 60 requests / minute burst
- All endpoints
- Advanced analytics
Business
$149 / month
100,000 requests / month
- 120 requests / minute burst
- All endpoints
- Advanced analytics
These are the live values from GET /api/v1/plans, which is
public and unmetered — read it instead of hardcoding prices in your app.
The two limits
| Limit | Window | Scope | Status |
|---|---|---|---|
| Burst | 60 seconds | Per API key, per plan | 429 rate_limit_exceeded |
| Monthly quota | Calendar month from your billing anchor | Per customer | 429 monthly_quota_exceeded |
| Anonymous flood guard | 15 minutes | Per IP — only for requests with no API key | 429 RATE_LIMITED |
The anonymous flood guard deliberately skips any request that presents an API key. A flat per-IP ceiling on authenticated traffic would make the monthly quotas unreachable for anyone behind a shared NAT egress.
Reading your remaining quota
Every authenticated response carries your current standing. A real Starter key on its fifth request returns:
200 OK
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 26
X-RateLimit-Reset: 1791209640
X-Quota-Limit: 5000
X-Quota-Remaining: 4992
X-Quota-Reset: 1793491200
X-Quota-Reset-After: 2281563
X-Quota-Period: monthly
Content-Type: application/json; charset=utf-8
remaining = int(resp.headers["X-Quota-Remaining"])
limit = int(resp.headers["X-Quota-Limit"])
reset_at = int(resp.headers["X-Quota-Reset"])
if remaining / limit < 0.1:
notify_ops(f"Only {remaining} of {limit} requests left")
# reset_at is an absolute Unix timestamp, like X-RateLimit-Reset
print(datetime.utcfromtimestamp(reset_at))
| Header | Meaning |
|---|---|
| X-Quota-Limit | Total requests in your current billing period |
| X-Quota-Remaining | Requests left in this period |
| X-Quota-Reset | Absolute Unix timestamp (seconds) when the period resets |
| X-Quota-Reset-After | Seconds until the reset — easier to use directly |
| X-Quota-Period | Always monthly |
| X-RateLimit-Limit | Burst ceiling for your plan in the current window |
| X-RateLimit-Remaining | Burst requests left in the current window |
| X-RateLimit-Reset | Absolute Unix timestamp when the burst window resets |
When you hit a limit
429 Too Many Requests
X-RateLimit-Remaining: 0
{
"success": false,
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests in a short period. Please slow down."
}
}
429 Too Many Requests
X-Quota-Limit: 5000
X-Quota-Remaining: 0
X-Quota-Reset: 1793491200
{
"success": false,
"error": {
"code": "monthly_quota_exceeded",
"message": "Your monthly API request limit has been reached."
}
}
Retry rate_limit_exceeded after X-RateLimit-Reset and
monthly_quota_exceeded only after X-Quota-Reset. Retrying
either immediately just burns more of the same budget.
Failed requests are refunded
Quota is reserved before your VIN is decoded, and the reservation is
returned if the server fails to produce data — any response with status
5xx. Your own bad input stays counted: a 400 for a
malformed VIN was still a request you made, so do not retry it unchanged.
Caching
Decoded VINs are cached server-side for
4 hours (up to 500 VINs). A cache hit returns the
identical payload plus "cached": true and a
Cache-Control: public, max-age=14400 header, so you can
cache on your side too without a second request.
Endpoint
Decode a VIN
The main endpoint. Takes a 17-character VIN and returns every section MotorAPI could source for it.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| units | string | metric | Unit system for converted values. metric or imperial. Anything else is treated as metric. |
The server echoes back what it actually applied in the top-level
units field, so you never have to guess. A full decode of
this VIN is around 47 KB of JSON; the payload shrinks when fewer
sources match.
Request
curl -H "X-API-Key: mot_live_YOUR_KEY" \
"https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352?units=metric"
import requests
BASE = "https://api.motorapi.dev/api/v1"
KEY = "mot_live_YOUR_KEY"
def decode_vin(vin, units="metric"):
resp = requests.get(
f"{BASE}/vin/{vin}",
headers={"X-API-Key": KEY},
params={"units": units},
timeout=15,
)
if resp.status_code != 200:
err = resp.json().get("error", {})
raise RuntimeError(f"{resp.status_code} {err.get('code')}: {err.get('message')}")
return resp.json()
data = decode_vin("1HGCM82633A004352")
identity = data["vehicle"]["identity"]
print(f"{identity['year']} {identity['make']} {identity['model']} {identity.get('trim') or ''}")
# 2003 HONDA Accord EX-V6
const BASE = "https://api.motorapi.dev/api/v1";
const KEY = process.env.MOTORAPI_KEY;
async function decodeVin(vin, units = "metric") {
const url = `${BASE}/vin/${vin}?units=${units}`;
const res = await fetch(url, { headers: { "X-API-Key": KEY } });
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
return res.json();
}
const { vehicle, warnings } = await decodeVin("1HGCM82633A004352");
if (warnings?.length) console.warn(warnings);
$url = "https://api.motorapi.dev/api/v1/vin/1HGCM82633A004352?units=metric";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-API-Key: mot_live_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
if (!empty($data["error"])) {
throw new RuntimeException($data["error"]["code"]);
}
echo $data["vehicle"]["identity"]["model"];
func DecodeVin(vin, units string) (*Vehicle, error) {
req, _ := http.NewRequest("GET",
"https://api.motorapi.dev/api/v1/vin/"+vin+"?units="+units, nil)
req.Header.Set("X-API-Key", os.Getenv("MOTORAPI_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
var e ErrorResponse
json.NewDecoder(res.Body).Decode(&e)
return nil, fmt.Errorf("%d %s: %s", res.StatusCode, e.Error.Code, e.Error.Message)
}
var out Response
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
return nil, err
}
return &out, nil
}
Successful response
200 OK for 1HGCM82633A004352. Arrays are
truncated here for readability — the real payload carries all 24
recalls, 2,013 complaints and the full complaint statistics.
{
"success": true,
"vin": "1HGCM82633A004352",
"units": "metric",
"vehicle": {
"vinStructure": {
"wmi": {
"code": "1HG",
"region": "North America",
"country": "United States",
"manufacturer": "Honda (US)",
"manufacturerKnown": true
},
"segments": {
"wmi": "1HG", "vds": "CM826",
"checkDigit": "3", "vis": "3A004352"
},
"checkDigit": {
"char": "3", "applicable": true,
"valid": true, "expected": "3"
},
"modelYearCode": {
"char": "3", "decodedYear": 2003,
"matchesDecodedYear": true
}
},
"identity": {
"matchLevel": "VIN_DERIVED",
"vinSpecific": false,
"make": "HONDA",
"makeId": "474",
"model": "Accord",
"modelId": "1861",
"year": 2003,
"vehicleType": "PASSENGER CAR",
"bodyClass": "Coupe",
"trim": "EX-V6",
"doors": 2,
"vehicleDescriptor": "1HGCM826*3A"
},
"engine": {
"model": "J30A4",
"configuration": "V-Shaped",
"cylinders": 6,
"displacement": { "liters": 2.999, "cc": 2998.8, "ci": 183 },
"horsepower": { "value": 240 },
"valveTrain": "Single Overhead Cam (SOHC)"
},
"fuel": { "typePrimary": "Gasoline", "grade": "Regular" },
"fuelEconomy": {
"source": "EPA fueleconomy.gov",
"matchLevel": "VEHICLE_CONFIGURATION",
"epa": { "cityMpg": 19, "highwayMpg": 27, "combinedMpg": 22 },
"annualFuelCostUsd": 3050,
"co2TailpipeGramsPerMile": 403.95,
"barrelsPrimaryPerYear": 13.52,
"epaVehicleClass": "Midsize Cars"
},
"transmission": { "style": "Automatic", "speeds": "5", "description": "Automatic 5-spd" },
"drivetrain": { "type": "Front-Wheel Drive" },
"weights": {
"gvwrClass": "Class 1C: 4,001 - 5,000 lb (1,814 - 2,268 kg)",
"gvwrClassTo": "Class 1: 6,000 lb or less (2,722 kg or less)"
},
"safetyEquipment": {
"airbags": {
"front": "1st Row (Driver and Passenger)",
"side": "1st Row (Driver and Passenger)",
"curtain": "1st and 2nd Rows"
},
"restraints": { "seatBeltsAll": "Manual", "otherInfo": "Seat Belt (Rr center position)" }
},
"manufacturing": {
"manufacturerName": "AMERICAN HONDA MOTOR CO., INC.",
"manufacturerId": "988",
"plant": { "city": "MARYSVILLE", "state": "OHIO", "country": "UNITED STATES (USA)" }
},
"recalls": {
"source": "NHTSA Recalls",
"matchLevel": "VEHICLE_CONFIGURATION",
"totalCount": 24,
"recallCount": 24,
"items": [
{
"campaignNumber": "17V220000",
"reportReceivedDate": "30/03/2017",
"component": "AIR BAGS:FRONTAL:PASSENGER SIDE:INFLATOR MODULE",
"summary": "Honda is recalling certain 2003 Honda Accord Coupe vehicles…",
"consequence": "An inflator rupture may result in metal fragments striking the driver…",
"remedy": "Honda will notify owners, and dealers will inspect the vehicle and replace any Takata inflator…"
}
]
},
"complaints": {
"matchLevel": "VEHICLE_CONFIGURATION",
"totalComplaints": 2013,
"analyzedCount": 2013,
"statistics": {
"withCrash": 158,
"withFire": 19,
"reportedInjuries": 157,
"reportedDeaths": 1
}
},
"investigations": {
"matchLevel": "VEHICLE_CONFIGURATION",
"investigationCount": 2,
"datasetLicense": "US Government work — public domain (NHTSA ODI).",
"sourceUrl": "https://static.nhtsa.gov/odi/ffdd/inv/FLAT_INV.zip"
},
"wmiRegistry": {
"source": "NHTSA vPIC WMI Registry",
"wmi": "1HG",
"commonName": "Honda",
"make": "HONDA",
"manufacturerName": "AMERICAN HONDA MOTOR CO., INC.",
"vehicleType": "Passenger Car",
"dateAvailableToPublic": "2015-01-01",
"matchLevel": "VIN_DERIVED"
},
"canadaFuelConsumption": {
"source": "NRCan Fuel Consumption Ratings",
"matchLevel": "VEHICLE_CONFIGURATION",
"combinedMpg": 26,
"fuelConsumption": {
"city": 12.7, "highway": 8.7,
"combined": 10.9, "unit": "L/100km"
}
},
"pricing": null,
"suspension": null,
"tires": null,
"colors": null
},
"dataSources": [
{ "provider": "NHTSA vPIC", "providerId": "nhtsa_vpic", "status": "ok" },
{ "provider": "EPA fueleconomy.gov", "providerId": "epa_fueleconomy", "status": "ok", "epaVehicleId": "18657" }
],
"dataClassification": {
"matchLevelPolicy": {
"VIN_EXACT": "Observed for this exact 17-character VIN.",
"VEHICLE_CONFIGURATION": "Selected by model year/make/model from the decoded identity. NOT observed for this VIN, and not a statement that this specific vehicle is affected.",
"VIN_DERIVED": "Derived from the characters of this VIN. True of this VIN, but not an observation recorded about it.",
"CALCULATED": "Computed by this API from other values; carries no independent provenance.",
"warning": "Sections tagged VEHICLE_CONFIGURATION describe the model line, not this vehicle."
},
"vinDecoded": {
"description": "Fields derivable directly from the VIN structure (WMI, VDS, check digit)",
"fields": ["identity.make", "identity.model", "identity.year", "identity.vehicleType", "vinStructure.wmi", "vinStructure.segments", "vinStructure.checkDigit", "vinStructure.modelYearCode", "wmiRegistry"]
},
"catalogData": { "description": "Fields from year/make/model/trim catalog lookups; not encoded in the VIN itself", "fields": ["identity.bodyClass", "engine", "fuel.typePrimary", "transmission.style", "drivetrain.type"] },
"epaData": { "description": "EPA values included only when the EPA record passes configuration matching", "fields": ["fuel.grade", "fuelEconomy", "transmission.description"] },
"governmentProgramData": { "description": "NHTSA recall, owner-complaint and defect-investigation program data, matched by year/make/model", "fields": ["recalls", "complaints", "investigations"] },
"canadianOpenData": { "description": "Transport Canada and Natural Resources Canada open data under the Open Government Licence – Canada", "fields": ["canadaFuelConsumption", "canadianVehicleSpecs"] },
"calculatedConverted": { "description": "Values calculated or converted from a provider-returned value", "fields": ["canadianVehicleSpecs.curbWeight.converted"] }
}
}
// This VIN's real "warnings" array
[
"Transport Canada CVS has no model year 2003 entry for this model; dimensions shown are from model year 2018."
]
// A warning means a source was matched less precisely than usual.
// Log them — they are the difference between "we looked" and
// "we looked and this is as close as the data allows".
Always read warnings. A section can be present but
matched loosely. On this VIN, Transport Canada has no 2003 entry, so Canadian
dimensions came from 2018 — and the response says so rather than presenting
2018 numbers as 2003 facts.
Errors
400 Bad Request
{
"success": false,
"error": {
"code": "INVALID_VIN",
"message": "VIN must be 17 characters (received 12)."
}
}
400 Bad Request
{
"success": false,
"error": {
"code": "INVALID_VIN",
"message": "Invalid VIN: check digit is \"4\", expected \"3\"."
}
}
404 Not Found
{
"success": false,
"error": {
"code": "VIN_NOT_FOUND",
"message": "VIN passed validation but could not be decoded — vehicle not found in NHTSA database.",
"vin": "ZZZZZZZZZZZZZZZZZ"
}
}
How VIN validation works
- Trimmed and uppercased, so
1hgcm82633a004352is accepted. - Must be exactly 17 characters.
- Rejected if it contains
I,OorQ— those are never used in a VIN. - Every character must be in the ISO 3779 alphabet.
- The ISO 3779 check digit at position 9 is enforced for North American VINs only (WMI starting
1–5). European and Asian VINs frequently use a different convention there, so a mismatch is reported incheckDigitbut not rejected.
A rejected VIN never reaches a data provider and never costs you anything beyond the request itself.
Endpoint
Vehicle summary
A compact roll-up of a decode: about 1.7 KB instead of 47 KB. Built for list views and validation-at-checkout, where you need to know what a VIN is and whether it is safe, not every spec.
Accepts the same units parameter as the decode endpoint.
curl -H "X-API-Key: mot_live_YOUR_KEY" \
"https://api.motorapi.dev/api/v1/vehicles/1HGCM82633A004352/summary"
res = requests.get(
f"{BASE}/vehicles/{vin}/summary",
headers={"X-API-Key": KEY},
timeout=10,
)
summary = res.json()
if not summary["safety"]["recallsAvailable"]:
# Source unavailable, not "no recalls" — do not treat as safe
raise RuntimeError("Recall data unavailable")
print(summary["recalls"]["totalCount"], "open recalls on this model line")
const res = await fetch(`${BASE}/vehicles/${vin}/summary`, {
headers: { "X-API-Key": KEY },
});
const summary = await res.json();
if (summary.recalls.available && summary.recalls.totalCount > 0) {
console.warn(`${summary.recalls.totalCount} recalls for this model line`);
}
Response
{
"success": true,
"vin": "1HGCM82633A004352",
"units": "metric",
"vehicle": { "year": 2003, "make": "HONDA", "model": "Accord" },
"dataAvailability": {
"specifications": true,
"fuelEconomy": true,
"recalls": true,
"complaints": true,
"investigations": true,
"canadianSpecs": true,
"canadianFuel": true
},
"odometer": {
"available": false,
"matchLevel": "VIN_EXACT",
"vinSpecific": true,
"recordCount": 0,
"note": "No odometer history available for this VIN. Free mileage history is regional (NY State inspections, UK MOT tests)."
},
"recalls": { "available": true, "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false, "totalCount": 24 },
"complaints": { "available": true, "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false, "totalComplaints": 2013 },
"investigations": { "available": true, "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false, "investigationCount": 2 },
"fuelEconomy": { "available": true, "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false },
"safety": {
"recallsAvailable": true,
"complaintsAvailable": true,
"investigationsAvailable": true
},
"sources": [
{ "providerId": "nhtsa_vpic", "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false },
{ "providerId": "epa_fueleconomy", "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false },
{ "providerId": "nhtsa_recalls", "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false },
{ "providerId": "nhtsa_complaints", "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false },
{ "providerId": "nhtsa_investigations", "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false },
{ "providerId": "nrcan_fuel_consumption", "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false },
{ "providerId": "tc_vehicle_specs", "matchLevel": "VEHICLE_CONFIGURATION", "vinSpecific": false },
{ "providerId": "nhtsa_wmi", "matchLevel": "VIN_DERIVED", "vinSpecific": false }
]
}
available: false means unknown, not safe. It says the
source had nothing for this VIN — which is not the same as the vehicle having no
recalls. Check available before you let a false count
stand as a clean bill of health.
Endpoint
Odometer history
Dated, source-attributed mileage readings for a VIN, from government inspection records. Every reading is an observation published by an authority — nothing is interpolated, averaged or inferred.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| units | string | metric | Unit for returned mileage. |
| refresh | boolean | false | true, 1, false or 0. Bypasses the 15-minute provider cache. Anything else returns 400 INVALID_REFRESH. |
Coverage is regional, and that is a data limit rather than a bug. Free source-backed mileage exists for vehicles inspected in New York State (annual DMV inspections) and vehicles tested in the United Kingdom (annual MOT). Nationwide US and Canada VIN-level history is a paid commercial product — Carfax, AutoCheck, NMVTIS — and is not redistributed here.
Request
curl -H "X-API-Key: mot_live_YOUR_KEY" \
"https://api.motorapi.dev/api/v1/vehicles/1HGCM82633A004352/odometer"
# Bypass the provider cache
curl -H "X-API-Key: mot_live_YOUR_KEY" \
"https://api.motorapi.dev/api/v1/vehicles/1HGCM82633A004352/odometer?refresh=true"
res = requests.get(
f"{BASE}/vehicles/{vin}/odometer",
headers={"X-API-Key": KEY},
params={"units": "imperial"},
timeout=30, # provider calls are slower here
)
odo = res.json()["odometer"]
if odo["status"] != "ok":
raise RuntimeError(odo["reason"])
latest = odo["latestVerified"]
print(latest["mileage"], latest["unit"], "on", latest["date"])
# estimatedCurrent is ALWAYS null. Never substitute it.
assert odo["estimatedCurrent"] is None
const res = await fetch(
`${BASE}/vehicles/${vin}/odometer?units=imperial`,
{ headers: { "X-API-Key": KEY } },
);
const { odometer } = await res.json();
if (odometer.status === "ok") {
const { mileage, unit, date } = odometer.latestVerified;
console.log(`${mileage} ${unit} on ${date}`);
}
No history available
This is a normal, healthy answer for a vehicle outside the covered
regions. Note the status is 200 OK while
success is false — the request succeeded,
the VIN simply has no published readings.
200 OK
{
"success": false,
"vin": "1HGCM82633A004352",
"units": "metric",
"odometer": {
"status": "unavailable",
"sourceMatchLevel": "VIN_EXACT",
"vinSpecific": true,
"estimationPolicy": {
"currentMileage": "never estimated",
"method": null,
"reason": "Mileage is never interpolated, averaged, or inferred from model year, trim or typical usage. estimatedCurrent is always null by design."
},
"latest": null,
"latestVerified": null,
"latestObserved": null,
"estimatedCurrent": null,
"records": [],
"readingExclusions": [],
"anomalies": [],
"conflicts": [],
"providers": [
{
"providerId": "ny_dmv_inspections",
"providerName": "NY DMV Vehicle Inspections",
"matchLevel": "VIN_EXACT",
"status": "ok",
"recordCount": 0,
"cacheHit": false,
"retrievedAt": "2026-10-05T14:10:33.055Z",
"expiresAt": "2026-10-05T14:25:33.055Z"
}
],
"reason": "no source-backed odometer records were returned by available providers"
},
"warnings": [
"No NY DMV inspection records found for this VIN (coverage: vehicles inspected in New York State).",
"no source-backed odometer records were returned by available providers"
],
"cache": { "hit": false, "refreshed": false },
"providerFailure": false
}
400 Bad Request
{
"success": false,
"error": {
"code": "INVALID_REFRESH",
"message": "refresh must be true or false."
}
}
Provider failures
When a provider errors and no records come back, the status is derived from the failure so you can retry intelligently.
| Provider error | Status | What to do |
|---|---|---|
| provider_rate_limited | 429 | The upstream source is throttling. Retry later. |
| provider_timeout | 504 | Retry — the upstream source did not answer in time. |
| provider_auth_failed, provider_vin_mismatch, provider_report_mismatch, malformed_provider_response | 502 | Upstream source is misbehaving. Retrying rarely helps immediately. |
| anything else | 503 | Service unavailable. Retry with backoff. |
| unhandled | 500 | ODOMETER_UNAVAILABLE — odometer history is temporarily unavailable. |
Field guide
| Field | Type | Description |
|---|---|---|
| status | string | ok when at least one reading exists, otherwise unavailable. |
| sourceMatchLevel | string | Always VIN_EXACT. Only VIN-keyed records may contribute readings. |
| records[] | array | The readings. Each has date, mileage, unit, sourceType and vin. |
| latestVerified | object|null | The most recent reading that passed verification, or null. |
| latestObserved | object|null | The most recent reading of any kind, including ones excluded from verification. |
| estimatedCurrent | null | Always null. Present as a permanent, explicit statement that no mileage is guessed. |
| readingExclusions[] | array | Readings dropped, with the reason each was rejected. |
| anomalies[] | array | Implausible readings detected — for example a mileage rollback. |
| conflicts[] | array | Places where two sources disagree about the same date. |
| providers[] | array | Per-provider outcome: status, record count, cache hit, retrieval and expiry timestamps. |
| cache | object | hit and refreshed, so you can tell a cached answer from a fresh lookup. |
Endpoints
Public endpoints
Three endpoints need no API key and never consume quota. Use them for health checks, plan pickers and source transparency.
Health
curl "https://api.motorapi.dev/api/v1/health"
{
"status": "ok",
"version": "2.3.0",
"startedAt": "2026-10-05T14:08:57.147Z",
"uptime": "9s",
"cache": { "size": 0, "maxSize": 500, "ttlMs": 14400000 },
"rateLimit": {
"windowMs": 900000,
"max": 60,
"note": "Anonymous requests only. Authenticated keys use per-plan burst limits."
}
}
Plans
The live plan catalogue. Prices are integer priceCents —
use that for arithmetic, and the formatted price string
only for display.
{
"success": true,
"plans": [
{ "id": "free", "name": "Free", "priceCents": 0, "price": "$0.00", "currency": "usd", "monthlyRequestLimit": 100, "analyticsLevel": "basic", "features": { "allEndpoints": true, "analytics": "basic" } },
{ "id": "starter", "name": "Starter", "priceCents": 1900, "price": "$19.00", "currency": "usd", "monthlyRequestLimit": 5000, "analyticsLevel": "standard", "features": { "allEndpoints": true, "analytics": "standard" } },
{ "id": "pro", "name": "Pro", "priceCents": 4900, "price": "$49.00", "currency": "usd", "monthlyRequestLimit": 25000, "analyticsLevel": "advanced", "features": { "allEndpoints": true, "analytics": "advanced" } },
{ "id": "business", "name": "Business", "priceCents": 14900, "price": "$149.00", "currency": "usd", "monthlyRequestLimit": 100000, "analyticsLevel": "advanced", "features": { "allEndpoints": true, "analytics": "advanced" } }
],
"quota": {
"period": "monthly",
"description": "Each plan includes a monthly request quota. Quotas reset at the start of each billing period.",
"headers": ["X-Quota-Limit", "X-Quota-Remaining", "X-Quota-Reset"]
}
}
Providers
What is wired up right now, with each source's licence and match level. This is the endpoint to check before promising a customer data from a given country.
| Provider ID | Availability | Covers |
|---|---|---|
| nhtsa_vpic | active | Core specs, identity, engine, safety equipment |
| nhtsa_wmi | active | Manufacturer registry (VIN positions 1–3) |
| local_wmi | active | Local WMI directory fallback (ISO 3779) |
| epa_fueleconomy | active | US fuel economy and CO₂ |
| nhtsa_recalls | active | US safety recalls |
| nhtsa_complaints | active | US owner complaints |
| nhtsa_investigations | active | US defect investigations |
| nrcan_fuel_consumption | active | Canadian fuel consumption |
| tc_vehicle_specs | active | Canadian vehicle specifications |
| ny_dmv_inspections | active | Odometer — New York State inspections |
| dvsa_mot_history | unavailable | Odometer — UK MOT. Needs DVSA credentials. |
Each provider entry also carries source, access,
legal and matching metadata — including the licence each
dataset is published under and its declared match level.
Profile
Account details
Loading your details…
- First name
- —
- Last name
- —
- Business
- —
- —
- User ID
- —
API Key
The key used to authenticate your API calls.
Your API key
Your key is never displayed here — only a masked public id is shown. The full key exists once, when the account is created.
•••• •••• •••• ••••
- Status
- Inactive
- Data generated
- not_created
- Requests this month
- 0 / 100
Free plan
$0/month
Need more requests? Upgrade from the pricing page and the new quota applies straight away, including to this key.