100 monthly requests · All API endpoints · Basic usage analytics
0 / 100
0% used100 requests remaining
All API endpoints
Basic usage analytics
Total calls0Successful calls0Error calls0Blocked calls0
Weekly usage
Requests made with your API key over the last 7 days.
Profile
Your account details, as Auth0 holds them.
Account details
Loading your details…
First name
—
Last name
—
Business
—
Email
—
User ID
—
Email and User ID are owned by Auth0 and cannot be changed here. First name,
last name and Business are editable and save straight back to your Auth0
account.
API Key
The key used to authenticate your API calls.
Your API key
The full key is shown once, when the key is created. It is never shown again — only the masked public id is kept.
•••• •••• •••• ••••
No API key created yet. Click Create API Key to get started.
Status
Inactive
Data generated
not_created
Requests this month
0 / 100
Free plan
$0/month
Monthly quota100 requests
Used this month0 requests
Remaining100 requests
Usage analyticsBasic
EndpointsAll API endpoints
Resets—
Need more requests? Upgrade from the pricing page and the new quota applies
straight away, including to this key.
MotorAPI v1.0.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.
https://api.motorapi.devBase URL
Your first request
Every vehicle endpoint lives under /api/v1 and needs
your API key. This is the whole integration:
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 sends Accept-Encoding: gzip.
Successful responses set success: true. Every failure sets success: false and an error object with a stable code.
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)
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:
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-Resetprint(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: 5000X-Quota-Remaining: 0X-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.
GET/api/v1/vin/{vin}Counts against quota
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.
funcDecodeVin(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 { returnnil, err }
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
var e ErrorResponse
json.NewDecoder(res.Body).Decode(&e)
returnnil, 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 {
returnnil, 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)."
}
}
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 1hgcm82633a004352 is accepted.
Must be exactly 17 characters.
Rejected if it contains I, O or Q — 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 in checkDigit but 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.
GET/api/v1/vehicles/{vin}/summaryCounts against quota
Accepts the same units parameter as the decode endpoint.
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 saferaise 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`);
}
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.
GET/api/v1/vehicles/{vin}/odometerCounts against quota
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).
More may be included in the future.
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.
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
active
Odometer — UK MOT
Each provider entry also carries source, access,
legal and matching metadata — including the licence each
dataset is published under and its declared match level.
Reference
Response fields
Every authenticated response shares one envelope. Learn the envelope
once and the individual endpoints are just sections underneath it.
The envelope
Field
Type
Description
success
boolean
true on a successful call, false with an error object.
vin
string
The uppercased 17-character VIN that was looked up.
units
string
The unit system the server actually applied — metric or imperial.
vehicle
object
The decoded vehicle. Sections with no data are null, never absent.
dataSources[]
array
Which provider answered for each section, with its status.
dataClassification
object
Field-by-field provenance: which values came from the VIN, a catalog lookup, a government program, or were calculated.
warnings[]
array
Sources that matched less precisely than usual. Log them.
error
object
Present only on failure. Carries a stable code and a human message.
A section being null means “this source had nothing for this
VIN”. It is never an error, and it never costs you extra quota.
vehicle sections
A full decode is around 47 KB. vehicle.identity and
vehicle.vinStructure are always present; everything else depends
on which providers matched.
dataClassification groups every field by where it came from, so a
UI can label values without hardcoding a list of paths. The groups are
vinDecoded, catalogData, epaData,
governmentProgramData, canadianOpenData and
calculatedConverted.
const { dataClassification, vehicle } = await get(`/vin/${vin}`);
// Every value in these groups was computed or converted by// this API. Mark them as derived in your own UI.const derived = new Set(dataClassification.calculatedConverted.fields);
functionprovenanceOf(path) {
for (const [group, meta] of Object.entries(dataClassification)) {
if (meta?.fields?.includes(path)) return group;
}
returnnull;
}
data = requests.get(url, headers=headers, timeout=15).json()
classification = data["dataClassification"]
# group -> [field, ...] for every provenance group
groups = {
group: meta["fields"]
for group, meta in classification.items()
if isinstance(meta, dict) and"fields"in meta
}
defprovenance(path):
for group, fields in groups.items():
if path in fields:
return group
return None
Values with units
Anything measured arrives as an object, not a bare number:
{ value, unit }, sometimes with a converted member
holding the same measurement in the other system. That is what lets a single
response serve both units=metric and units=imperial
callers without a second request.
Read value with its unit rather than assuming a
unit. If you only need one system, ask for it with
?units= and use the top-level numbers.
Reference
Match levels
Not every value in a response was recorded about your vehicle. Each
section carries a matchLevel so you can tell the difference, and
every response ships the policy that defines it.
The four levels
Level
What it means
VIN_EXACT
Observed for this exact 17-character VIN.
VEHICLE_CONFIGURATION
Selected by model year, make and 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 it is not an observation recorded about it.
CALCULATED
Computed by this API from other values; carries no independent provenance.
Sections tagged VEHICLE_CONFIGURATION describe the model line,
not this vehicle. Recalls, owner complaints and defect investigations are
matched this way, so never present them as “this car is affected”.
Where to read it
matchLevel appears on a section, on a provider entry in
dataSources[], and per dataset in the summary endpoint's
availability block. Odometer readings are always
VIN_EXACT — only VIN-keyed records may contribute a reading.
const data = await get(`/vin/${vin}`);
// Only ship a value to the customer if it is about their car.functionisAboutThisVehicle(section) {
return section?.matchLevel === "VIN_EXACT";
}
const recalls = data.vehicle.recalls;
if (recalls && !isAboutThisVehicle(recalls)) {
console.warn("Recall data is model-line level, not VIN specific");
}
data = requests.get(url, headers=headers, timeout=15).json()
# Only ship a value to the customer if it is about their car.defis_about_this_vehicle(section):
return (section or {}).get("matchLevel") == "VIN_EXACT"
recalls = data["vehicle"].get("recalls")
if recalls andnot is_about_this_vehicle(recalls):
print("Recall data is model-line level, not VIN specific")
The policy travels with the response
You never have to hardcode the table above.
dataClassification.matchLevelPolicy returns every level with its
definition plus a warning string, so a UI can render the caveat
without shipping its own copy of the wording.
identity.vinSpecific is the boolean shortcut for the same
question. It is false when identity came from a model-line catalog
match rather than a VIN-keyed record.
Reference
Errors & status codes
Every failure has the same shape: success: false and an
error object with a stable code. Branch on the code,
show the message to a human.
The error object
const { error } = await get(url);
switch (error.code) {
case"INVALID_VIN": // 400 - never reached a providercase"VIN_NOT_FOUND": // 404 - valid VIN, no vehiclecase"INVALID_API_KEY": // 401 - missing, unknown, revoked or expiredcase"KEY_INACTIVE": // 403 - key exists but is switched offcase"rate_limit_exceeded": // 429 - burst limit; read X-RateLimit-Resetcase"monthly_quota_exceeded": // 429 - quota spent; read X-Quota-Resetdefault: throw new Error(error.message);
}
body = requests.get(url, headers=headers, timeout=15).json()
error = body.get("error") or {}
if error.get("code") == "INVALID_VIN":
raise ValueError("That VIN is not valid.") # 400if error.get("code") == "VIN_NOT_FOUND":
raise LookupError("No vehicle for that VIN.") # 404if error.get("code") == "monthly_quota_exceeded":
raise QuotaExhausted("Quota spent.") # 429
Codes
Status
Code
Cause
Counts against quota
400
INVALID_VIN
Wrong length, an illegal character, or a bad check digit.
No
400
INVALID_REQUEST
A parameter was unusable, such as a malformed VIN path segment.
No
401
INVALID_API_KEY
The key is missing, unknown, revoked or expired. All four are deliberately indistinguishable.
No
403
KEY_INACTIVE
The key is real but switched off. Activate it from the dashboard.
No
404
VIN_NOT_FOUND
The VIN passed validation but no vehicle matched. The body echoes the vin.
No
429
rate_limit_exceeded
Burst limit hit. Retry after X-RateLimit-Reset.
No
429
monthly_quota_exceeded
The monthly allowance is spent. Retry only after X-Quota-Reset.
No
500
internal_error
Something failed on our side. Safe to retry with backoff.
No
503
service_unavailable
A dependency is down. Retry with backoff.
No
Failed requests are refunded
Only a request that produced usable data counts against your quota. Every 4xx
and 5xx is refunded in full, so a bad VIN or a rejected key never costs you
anything. You do not need to track this yourself — just read
X-Quota-Remaining from the response and trust it.
The odometer endpoint is the one partial success. It answers
200 with success: false when a VIN has no history at
all, because the lookup worked and “no records” is a real answer. Treat that
body as complete, not as a failure.