Migration
How to migrate from NHTSA vPIC to Cardog
Last updated:
Move a vPIC DecodeVinValues call to GET /v2/vin/{vin}, with every field mapped.
Swap DecodeVinValues (or DecodeVinValuesExtended) for GET /v2/vin/{vin} and read the identity card it returns. What you gain: Canadian VINs decoded to trim, refs that join each VIN to spec sheets, Transport Canada and NHTSA recalls and market prices, a reason code for every VIN we cannot name, and batches of 1,000.
The tables below map all 154 of vPIC's flat keys: 112 to a Cardog field and 42 not mapped. Most of the 112 live in the card's attributes map, which is filled per build.
This guide is written by Cardog. Every claim about vPIC links to NHTSA's own public page, and we checked those pages on 2026-10-26. vPIC is NHTSA's public decoder; Cardog's is a separate decoder with its own response shape. The adapter below builds a vPIC-shaped object in your code.
The short version
- Get a Cardog API key and send it as the
x-api-keyheader (orAuthorization: Bearer crdg_…). Unauthenticated calls to/v2get a 401. - Replace
GET https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValues/{vin}?format=jsonwithGET https://api.cardog.app/v2/vin/{vin}. For batches, replaceDecodeVINValuesBatch(up to 50 VINs) withPOST /v2/vin/batchand{"vins": [...]}(up to 1,000). - Read
validfirst. When it isfalse,reasonssays why, and the identity fields, refs,nano,squishandattributesare null. - Change your reads: vPIC's
Results[0].Makebecomesmake,ModelYearbecomesyear(a number), the category fields become refs underrefs, and engine, battery, plant and equipment keys move toattributes.<id>. The tables below have every key. - Diff a sample of your own VINs before you switch traffic.
How their fields map to ours
vPIC's flat decode returns Count, Message, SearchCriteria and a Results array holding one object per VIN, with every value a string and an empty string when vPIC has no data (NHTSA, sample response). The API page documents the call and its parameters but does not list the fields on the decode itself (it offers a separate GetVehicleVariableList call for that), so the 154 keys below come from NHTSA's own sample URL on that page, fetched on 2026-10-26.
Cardog returns one JSON object, not a Results array. Integers are integers, absent values are null, and category values (body, fuel, drive and so on) are ref strings such as "fuel-type:electric" that join to the rest of the API. They are not vPIC's display text, so a straight string compare against vPIC will fail on those rows.
The attributes map
attributes is the decoder's attribute map for the build, keyed by the attribute ids in our spec catalogue (GET /v2/specs/catalog). The catalogue tags 101 attributes with the matching vPIC element, which is how the keys below line up. Each entry is an object rather than a bare string: usually { value }, a range as { from, to }, and a measured value can carry a unit. Our contract types the map loosely, so check unit before you use a number; we never convert a number into vPIC's unit for you.
The map is sparse. An entry is present only where the decoder has that fact for that build. A key in the first table below means Cardog has a place for it, not that every VIN fills it.
The envelope
| vPIC field | Cardog field | Note |
|---|---|---|
Count | (none) | One VIN, one object. Batch returns meta.requested, meta.decoded, meta.failed. |
Message | (none) | vPIC uses it to say that a missing value means NHTSA has no data, not that the feature is absent. A null or a missing attributes entry on Cardog means the same: we do not have the fact for this build. |
SearchCriteria | vin | Closest. We echo the VIN only. |
Results | the response body | One object, not an array. Batch returns results[], each { ok, vin, identity } or { ok: false, vin, error }. |
vPIC keys with a Cardog field (112)
Every attributes.* row is "where the decoder has it".
| vPIC key | Cardog field on /v2/vin | Note |
|---|---|---|
ABS | attributes.abs | |
AdaptiveCruiseControl | attributes.adaptiveCruiseControl | |
AdaptiveDrivingBeam | attributes.adaptiveDrivingBeam | |
AdditionalErrorText | reasons | Codes, not prose. |
AirBagLocCurtain | attributes.curtainAirbagLocations | |
AirBagLocFront | attributes.frontAirbagLocations | |
AirBagLocKnee | attributes.kneeAirbagLocations | |
AirBagLocSeatCushion | attributes.seatCushionAirbagLocations | |
AirBagLocSide | attributes.sideAirbagLocations | |
AutomaticPedestrianAlertingSound | attributes.pedestrianAlertSound | |
AxleConfiguration | attributes.axleConfiguration | |
Axles | numberOfAxles | Integer. |
BasePrice | attributes.msrp | USD, where the decoder has it. |
BatteryA | attributes.batteryCurrent | |
BatteryA_to | attributes.batteryCurrent | The upper end of a range. |
BatteryCells | attributes.batteryCellsPerModule | |
BatteryKWh | attributes.batteryCapacity | |
BatteryKWh_to | attributes.batteryCapacity | The upper end of a range. |
BatteryModules | attributes.batteryModulesPerPack | |
BatteryPacks | attributes.batteryPacks | |
BatteryType | attributes.batteryChemistry | |
BatteryV | attributes.batteryVoltage | |
BatteryV_to | attributes.batteryVoltage | The upper end of a range. |
BedLengthIN | attributes.bedLength | Check the attribute's unit. |
BedType | refs.bedType, else attributes.bedType | A ref such as bed-type:short. |
BlindSpotMon | attributes.blindSpotMonitoring | |
BodyCabType | refs.bodyCab, else attributes.cabType | A ref such as body-cab:crew-super-crew-crew-max. |
BodyClass | refs.bodyStyle, else attributes.bodyStyle | A ref string, not vPIC text. |
BrakeSystemType | attributes.brakeSystemType | |
BusFloorConfigType | attributes.busFloorConfig | |
BusLength | attributes.busLength | Check the attribute's unit. |
BusType | attributes.busType | |
ChargerLevel | attributes.chargerLevel | |
ChargerPowerKW | attributes.chargerPower | |
CIB | attributes.automaticEmergencyBraking | |
CoolingType | attributes.coolingType | |
CurbWeightLB | attributes.curbWeight; also shippingWeight | shippingWeight is an integer in lb. Check the attribute's unit before you treat it as pounds. |
CustomMotorcycleType | attributes.customMotorcycleType | |
DestinationMarket | attributes.destinationMarket | |
DisplacementCC | attributes.displacementCC | |
DisplacementCI | attributes.displacementCI | |
DisplacementL | attributes.displacementL | |
Doors | doors | Integer. |
DriveType | refs.driveType, else attributes.driveType | A ref string. |
ElectrificationLevel | refs.electrificationLevel, else attributes.electrificationLevel | A ref string. |
EngineConfiguration | attributes.engineConfiguration | |
EngineCycles | attributes.engineStrokeCycles | |
EngineCylinders | attributes.cylinders | |
EngineHP | attributes.horsepowerFrom | |
EngineHP_to | attributes.horsepowerFrom | The upper end of a range. |
EngineKW | attributes.powerKW | |
EngineManufacturer | attributes.engineManufacturer | |
EngineModel | attributes.engineModel | |
EntertainmentSystem | attributes.entertainmentSystem | |
ErrorCode | valid + reasons | No numeric codes. valid: false always comes with reasons codes such as WMI_UNKNOWN or UNSEEN_BUILD. The check digit is never a gate, so a VIN vPIC flags for its check digit can still come back valid: true. |
ErrorText | reasons | Codes, not prose. |
ESC | attributes.stabilityControl | |
EVDriveUnit | attributes.evDriveUnit | |
ForwardCollisionWarning | attributes.forwardCollisionWarning | |
FuelInjectionType | attributes.fuelSystem | |
FuelTypePrimary | refs.fuelType, else attributes.fuelTypePrimary | A ref string. |
FuelTypeSecondary | refs.fuelTypeSecondary, else attributes.fuelTypeSecondary | A ref such as fuel-type:electric. |
GVWR | refs.gvwr and grossWeight | refs.gvwr is the class as a ref; grossWeight is the rating as an integer in lb. attributes.gvwr holds the class range where the decoder has it. |
GVWR_to | attributes.gvwr | The upper end of the class range. |
KeylessIgnition | attributes.pushButtonStart | |
LaneDepartureWarning | attributes.laneDepartureWarning | |
LaneKeepSystem | attributes.laneKeepAssist | |
LowerBeamHeadlampLightSource | attributes.headlightType | |
Make | make (display), refs.make (key) | Our display name; casing can differ from vPIC (vPIC returns BMW). Join on refs.make. |
MakeID | refs.make | vPIC's numeric make id; refs.make is the key that joins. |
Manufacturer | attributes.manufacturer | |
Model | model (display), refs.model (key) | Join on refs.model. |
ModelID | refs.model | vPIC's numeric model id; refs.model is the key that joins. |
ModelYear | year | A number, not a string. |
MotorcycleChassisType | attributes.motorcycleChassisType | |
MotorcycleSuspensionType | attributes.motorcycleSuspensionType | |
NonLandUse | attributes.nonLandUse | |
PlantCity | attributes.plantCity | |
PlantCompanyName | attributes.plantCompany | |
PlantCountry | refs.country | Format change: a country ref, such as country:canada or country:united-states-usa, not vPIC's uppercase name. null when the decoder does not state a plant country. |
PlantState | attributes.plantState | |
Pretensioner | attributes.seatBeltPretensioner | |
RearAutomaticEmergencyBraking | attributes.rearAutomaticEmergencyBraking | |
RearVisibilitySystem | attributes.backupCamera | |
SeatBeltsAll | attributes.seatBeltType | |
SeatRows | attributes.seatRows | |
Seats | attributes.seatingCapacity | |
SemiautomaticHeadlampBeamSwitching | attributes.automaticHighBeams | |
Series | attributes.series (text), refs.series (key) | refs.series is a ref such as series:1500. |
Series2 | attributes.seriesSecondary | |
SteeringLocation | attributes.steeringPosition | |
TopSpeedMPH | attributes.topSpeed | Check the attribute's unit; our catalogue holds top speed in km/h. |
TractionControl | attributes.tractionControl | |
TrailerBodyType | refs.trailerBodyType, else attributes.trailerBodyType | A ref string. |
TrailerLength | attributes.trailerLength | Check the attribute's unit. |
TrailerType | refs.trailerType, else attributes.trailerType | A ref string. |
TransmissionSpeeds | attributes.transmissionSpeeds | |
TransmissionStyle | refs.transmission, else attributes.transmission | A ref string. |
Trim | trim | The catalogue trim decoded from the VIN pattern. |
Trim2 | attributes.trimSecondary | |
Turbo | attributes.turbo | |
ValveTrainDesign | attributes.camType | |
VehicleDescriptor | nano | Same positions as vPIC's descriptor (1 to 8, then 10 and 11) as a ref with the masked position left out, e.g. nano:5TDGSKFCRS. Makers whose WMI ends in 9 get a longer key. |
VehicleType | refs.vehicleType, else attributes.vehicleType | A ref string. |
VIN | vin | Always the 17 characters you sent, upper-cased. |
WheelBaseLong | attributes.wheelbase | The upper end of a range. Check the unit. |
WheelBaseShort | attributes.wheelbase | The low end of a range. Check the unit. |
WheelBaseType | attributes.wheelbaseType | |
Wheels | numberOfWheels | Integer. |
WheelSizeFront | attributes.wheelSizeFront | |
WheelSizeRear | attributes.wheelSizeRear | |
Windows | attributes.windows |
vPIC keys not mapped (42)
| vPIC keys | Note |
|---|---|
ActiveSafetySysNote, AdaptiveHeadlights, AutoReverseSystem, BatteryInfo, BlindSpotIntervention, BrakeSystemDesc, CAN_AACN, CashForClunkers, CombinedBrakingSystem, DaytimeRunningLight, DriverAssist, DynamicBrakeSupport, EDR, FuelTankMaterial, FuelTankType, GCWR, GCWR_to, LaneCenteringAssistance, OtherBusInfo, OtherEngineInfo, OtherMotorcycleInfo, OtherRestraintSystemInfo, OtherTrailerInfo, ParkAssist, PedestrianAutomaticEmergencyBraking, RearCrossTrafficAlert, SAEAutomationLevel, SAEAutomationLevel_to, TPMS, TrackWidth, WheelieMitigation | Not mapped. |
ManufacturerId | vPIC's internal manufacturer id. attributes.manufacturer has the name. |
NCSABodyType, NCSAMake, NCSAMapExcApprovedBy, NCSAMapExcApprovedOn, NCSAMappingException, NCSAModel, NCSANote | NHTSA's crash-database coding of the vehicle. |
Note | vPIC's free-text pattern note. |
PossibleValues, SuggestedVIN | An unreadable VIN is reported in reasons, never repaired. |
Fields that exist only on Cardog: valid, reasons, nano (build key), squish (configuration key), refs.modelYear, authorityTier, corgiBuild (the decoder build that answered) and links. When we know the build, links.specsBuild and links.safetyBuild point to /v2/specs/{nano} and /v2/safety/{nano}, where the spec sheet carries values with units for many of the same facts.
A drop-in adapter
This calls Cardog and hands back an object in vPIC's flat shape for the keys that map. It covers the typed fields and the plain attributes entries; everything else stays undefined. This adapter is code you run on your side.
type VpicFlatResult = Record<string, string | undefined>;
type VpicFlatResponse = {
Count: number;
Message: string;
SearchCriteria: string;
Results: VpicFlatResult[];
};
type Attr = { value?: unknown; from?: unknown; to?: unknown; unit?: string };
const str = (v: unknown) => (v === null || v === undefined ? "" : String(v));
// vPIC key -> Cardog attribute id, for plain-text and count keys only:
// no unit to check and no ref to translate.
const FROM_ATTRIBUTES: Record<string, string> = {
DisplacementL: "displacementL",
EngineCylinders: "cylinders",
EngineModel: "engineModel",
EngineManufacturer: "engineManufacturer",
EngineHP: "horsepowerFrom",
ValveTrainDesign: "camType",
FuelInjectionType: "fuelSystem",
Turbo: "turbo",
TransmissionSpeeds: "transmissionSpeeds",
Series: "series",
Trim2: "trimSecondary",
Seats: "seatingCapacity",
SeatRows: "seatRows",
Manufacturer: "manufacturer",
PlantCity: "plantCity",
PlantState: "plantState",
PlantCompanyName: "plantCompany",
};
export async function decodeVinValuesViaCardog(
vin: string,
apiKey: string
): Promise<VpicFlatResponse> {
const res = await fetch(`https://api.cardog.app/v2/vin/${encodeURIComponent(vin)}`, {
headers: { "x-api-key": apiKey },
});
// 400 = not a 17-character VIN.
if (!res.ok) throw new Error(`Cardog ${res.status}: ${await res.text()}`);
const id = await res.json();
const attrs: Record<string, Attr> = id.attributes ?? {};
// A plain value, or the low end of a range.
const attr = (key: string) => {
const a = attrs[key];
return a ? str(a.value ?? a.from) : undefined;
};
const result: VpicFlatResult = {
VIN: id.vin,
ModelYear: str(id.year),
Make: str(id.make), // casing may differ from vPIC
Model: str(id.model),
Trim: str(id.trim),
Doors: str(id.doors),
Wheels: str(id.numberOfWheels),
Axles: str(id.numberOfAxles),
// vPIC's ErrorCode is numeric; Cardog's reasons are named codes.
ErrorCode: id.valid ? "0" : undefined,
ErrorText: id.valid ? "" : (id.reasons ?? []).join("; "),
};
for (const [key, attribute] of Object.entries(FROM_ATTRIBUTES)) {
result[key] = attr(attribute); // undefined when the decoder does not have it
}
// Left undefined on purpose: Cardog returns these as refs
// (id.refs.bodyStyle, id.refs.fuelType, id.refs.driveType,
// id.refs.transmission, id.refs.gvwr, id.refs.country ...), not vPIC's display text.
// Translate them with your own lookup if you need the vPIC strings;
// do not paste a ref into a vPIC field. The same goes for entity
// attributes such as attributes.engineConfiguration. Unit-bearing keys
// (CurbWeightLB, TopSpeedMPH, BedLengthIN ...) need a check of
// attrs[id].unit first, so they are not filled here either.
return {
Count: 1,
Message: "Adapted from Cardog GET /v2/vin",
SearchCriteria: `VIN(s): ${id.vin}`,
Results: [result],
};
}Verify before you switch
Pick 20 to 50 VINs you already decode with vPIC, ideally including a few recent model years, a truck, a trailer and one you know is bad. For each, call both and compare:
const vpic = await fetch(
`https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValues/${vin}?format=json`
).then((r) => r.json());
const ours = await decodeVinValuesViaCardog(vin, process.env.CARDOG_API_KEY!);
for (const key of ["ModelYear", "Make", "Model", "Trim", "Doors", "DisplacementL", "EngineCylinders", "PlantCity"]) {
const a = vpic.Results[0][key] ?? "";
const b = ours.Results[0][key] ?? "";
if (b !== "" && a.toLowerCase() !== b.toLowerCase()) console.log(vin, key, { vpic: a, cardog: b });
}Look first at year, make and model, then closely at trim, and at any VIN where one side says it is invalid and the other does not. A VIN with a wrong check digit is the usual case: vPIC can flag it, while Cardog decodes the build and may still answer valid: true. Some numbers differ only in format (3.0 against 3). For the ref fields, check that the ref reads as the same thing as vPIC's text rather than comparing strings.
What Cardog adds
- Canadian VINs to trim. We decode 99.77% of Canadian VINs to trim level (Decode a Canadian VIN).
- Builds from every market. Our own VIN patterns name builds such as Tesla China and Berlin builds, BYD and NIO.
- Join keys.
refs,nanoandsquishtie a VIN to model-year specs, safety, market quotes and listings. - Canadian recalls on the same VIN.
GET /v2/vin/{vin}/recallsreturns campaigns with anauthorityof"tc"(Transport Canada) or"nhtsa". - A clear yes or no.
validplus a closed list ofreasonscodes; avalid: falsealways arrives with at least one reason. - Window stickers and listings for the VIN.
GET /v2/vin/{vin}/window-stickerandGET /v2/vin/{vin}/listings. - Bigger batches.
POST /v2/vin/batchtakes up to 1,000 VINs. A malformed VIN fails its own row (ok: false), not the batch; a well-formed VIN we cannot name comes backok: truewithidentity.valid: false. - Provenance.
corgiBuildnames the decoder build that answered, so two different answers can be traced to two builds.
FAQ
Does Cardog check the check digit?
/v2/vin decodes the build from the VIN with position 9 masked, so a mistyped check digit does not stop a decode. If you need it enforced, check it before you call.
Why do I get "fuel-type:electric" instead of "Electric"?
Category fields are refs, the keys the rest of the API joins on. They are not vPIC display text.
Where are engine size, horsepower and airbags?
In attributes, keyed by catalogue id (attributes.displacementL, attributes.horsepowerFrom, attributes.frontAirbagLocations and so on), where the decoder has them for the build. The spec sheet at links.specsBuild carries many of the same facts with units.
What happens to a VIN Cardog cannot name?
You get valid: false, null identity fields, refs, nano, squish and attributes, and reasons with at least one code, such as UNSEEN_BUILD or WMI_UNKNOWN. vin and links still arrive.
How is a decode billed?
The identity route is priced at two credits per VIN. Today a single GET /v2/vin/{vin} bills those two credits for any 200 answer, including valid: false. A batch bills two credits for each row that comes back valid: true, so malformed rows and valid: false rows cost nothing. A 400 costs nothing. See /pricing.
Related
- Cardog vs NHTSA vPIC
- NHTSA vPIC alternatives
- Best VIN decoder APIs (2026)
- API docs
- Other migration guides
- Pricing
Sources
Checked on 2026-10-26:
- NHTSA vPIC API: https://vpic.nhtsa.dot.gov/api/
- NHTSA vPIC sample flat decode (the example URL on that page): https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValues/5UXWX7C5*BA?format=json&modelyear=2011
If something here is wrong, tell us and we will fix it.
Was this helpful?