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

  1. Get a Cardog API key and send it as the x-api-key header (or Authorization: Bearer crdg_…). Unauthenticated calls to /v2 get a 401.
  2. Replace GET https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValues/{vin}?format=json with GET https://api.cardog.app/v2/vin/{vin}. For batches, replace DecodeVINValuesBatch (up to 50 VINs) with POST /v2/vin/batch and {"vins": [...]} (up to 1,000).
  3. Read valid first. When it is false, reasons says why, and the identity fields, refs, nano, squish and attributes are null.
  4. Change your reads: vPIC's Results[0].Make becomes make, ModelYear becomes year (a number), the category fields become refs under refs, and engine, battery, plant and equipment keys move to attributes.<id>. The tables below have every key.
  5. 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 fieldCardog fieldNote
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.
SearchCriteriavinClosest. We echo the VIN only.
Resultsthe response bodyOne 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 keyCardog field on /v2/vinNote
ABSattributes.abs
AdaptiveCruiseControlattributes.adaptiveCruiseControl
AdaptiveDrivingBeamattributes.adaptiveDrivingBeam
AdditionalErrorTextreasonsCodes, not prose.
AirBagLocCurtainattributes.curtainAirbagLocations
AirBagLocFrontattributes.frontAirbagLocations
AirBagLocKneeattributes.kneeAirbagLocations
AirBagLocSeatCushionattributes.seatCushionAirbagLocations
AirBagLocSideattributes.sideAirbagLocations
AutomaticPedestrianAlertingSoundattributes.pedestrianAlertSound
AxleConfigurationattributes.axleConfiguration
AxlesnumberOfAxlesInteger.
BasePriceattributes.msrpUSD, where the decoder has it.
BatteryAattributes.batteryCurrent
BatteryA_toattributes.batteryCurrentThe upper end of a range.
BatteryCellsattributes.batteryCellsPerModule
BatteryKWhattributes.batteryCapacity
BatteryKWh_toattributes.batteryCapacityThe upper end of a range.
BatteryModulesattributes.batteryModulesPerPack
BatteryPacksattributes.batteryPacks
BatteryTypeattributes.batteryChemistry
BatteryVattributes.batteryVoltage
BatteryV_toattributes.batteryVoltageThe upper end of a range.
BedLengthINattributes.bedLengthCheck the attribute's unit.
BedTyperefs.bedType, else attributes.bedTypeA ref such as bed-type:short.
BlindSpotMonattributes.blindSpotMonitoring
BodyCabTyperefs.bodyCab, else attributes.cabTypeA ref such as body-cab:crew-super-crew-crew-max.
BodyClassrefs.bodyStyle, else attributes.bodyStyleA ref string, not vPIC text.
BrakeSystemTypeattributes.brakeSystemType
BusFloorConfigTypeattributes.busFloorConfig
BusLengthattributes.busLengthCheck the attribute's unit.
BusTypeattributes.busType
ChargerLevelattributes.chargerLevel
ChargerPowerKWattributes.chargerPower
CIBattributes.automaticEmergencyBraking
CoolingTypeattributes.coolingType
CurbWeightLBattributes.curbWeight; also shippingWeightshippingWeight is an integer in lb. Check the attribute's unit before you treat it as pounds.
CustomMotorcycleTypeattributes.customMotorcycleType
DestinationMarketattributes.destinationMarket
DisplacementCCattributes.displacementCC
DisplacementCIattributes.displacementCI
DisplacementLattributes.displacementL
DoorsdoorsInteger.
DriveTyperefs.driveType, else attributes.driveTypeA ref string.
ElectrificationLevelrefs.electrificationLevel, else attributes.electrificationLevelA ref string.
EngineConfigurationattributes.engineConfiguration
EngineCyclesattributes.engineStrokeCycles
EngineCylindersattributes.cylinders
EngineHPattributes.horsepowerFrom
EngineHP_toattributes.horsepowerFromThe upper end of a range.
EngineKWattributes.powerKW
EngineManufacturerattributes.engineManufacturer
EngineModelattributes.engineModel
EntertainmentSystemattributes.entertainmentSystem
ErrorCodevalid + reasonsNo 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.
ErrorTextreasonsCodes, not prose.
ESCattributes.stabilityControl
EVDriveUnitattributes.evDriveUnit
ForwardCollisionWarningattributes.forwardCollisionWarning
FuelInjectionTypeattributes.fuelSystem
FuelTypePrimaryrefs.fuelType, else attributes.fuelTypePrimaryA ref string.
FuelTypeSecondaryrefs.fuelTypeSecondary, else attributes.fuelTypeSecondaryA ref such as fuel-type:electric.
GVWRrefs.gvwr and grossWeightrefs.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_toattributes.gvwrThe upper end of the class range.
KeylessIgnitionattributes.pushButtonStart
LaneDepartureWarningattributes.laneDepartureWarning
LaneKeepSystemattributes.laneKeepAssist
LowerBeamHeadlampLightSourceattributes.headlightType
Makemake (display), refs.make (key)Our display name; casing can differ from vPIC (vPIC returns BMW). Join on refs.make.
MakeIDrefs.makevPIC's numeric make id; refs.make is the key that joins.
Manufacturerattributes.manufacturer
Modelmodel (display), refs.model (key)Join on refs.model.
ModelIDrefs.modelvPIC's numeric model id; refs.model is the key that joins.
ModelYearyearA number, not a string.
MotorcycleChassisTypeattributes.motorcycleChassisType
MotorcycleSuspensionTypeattributes.motorcycleSuspensionType
NonLandUseattributes.nonLandUse
PlantCityattributes.plantCity
PlantCompanyNameattributes.plantCompany
PlantCountryrefs.countryFormat 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.
PlantStateattributes.plantState
Pretensionerattributes.seatBeltPretensioner
RearAutomaticEmergencyBrakingattributes.rearAutomaticEmergencyBraking
RearVisibilitySystemattributes.backupCamera
SeatBeltsAllattributes.seatBeltType
SeatRowsattributes.seatRows
Seatsattributes.seatingCapacity
SemiautomaticHeadlampBeamSwitchingattributes.automaticHighBeams
Seriesattributes.series (text), refs.series (key)refs.series is a ref such as series:1500.
Series2attributes.seriesSecondary
SteeringLocationattributes.steeringPosition
TopSpeedMPHattributes.topSpeedCheck the attribute's unit; our catalogue holds top speed in km/h.
TractionControlattributes.tractionControl
TrailerBodyTyperefs.trailerBodyType, else attributes.trailerBodyTypeA ref string.
TrailerLengthattributes.trailerLengthCheck the attribute's unit.
TrailerTyperefs.trailerType, else attributes.trailerTypeA ref string.
TransmissionSpeedsattributes.transmissionSpeeds
TransmissionStylerefs.transmission, else attributes.transmissionA ref string.
TrimtrimThe catalogue trim decoded from the VIN pattern.
Trim2attributes.trimSecondary
Turboattributes.turbo
ValveTrainDesignattributes.camType
VehicleDescriptornanoSame 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.
VehicleTyperefs.vehicleType, else attributes.vehicleTypeA ref string.
VINvinAlways the 17 characters you sent, upper-cased.
WheelBaseLongattributes.wheelbaseThe upper end of a range. Check the unit.
WheelBaseShortattributes.wheelbaseThe low end of a range. Check the unit.
WheelBaseTypeattributes.wheelbaseType
WheelsnumberOfWheelsInteger.
WheelSizeFrontattributes.wheelSizeFront
WheelSizeRearattributes.wheelSizeRear
Windowsattributes.windows

vPIC keys not mapped (42)

vPIC keysNote
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, WheelieMitigationNot mapped.
ManufacturerIdvPIC's internal manufacturer id. attributes.manufacturer has the name.
NCSABodyType, NCSAMake, NCSAMapExcApprovedBy, NCSAMapExcApprovedOn, NCSAMappingException, NCSAModel, NCSANoteNHTSA's crash-database coding of the vehicle.
NotevPIC's free-text pattern note.
PossibleValues, SuggestedVINAn 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, nano and squish tie a VIN to model-year specs, safety, market quotes and listings.
  • Canadian recalls on the same VIN. GET /v2/vin/{vin}/recalls returns campaigns with an authority of "tc" (Transport Canada) or "nhtsa".
  • A clear yes or no. valid plus a closed list of reasons codes; a valid: false always arrives with at least one reason.
  • Window stickers and listings for the VIN. GET /v2/vin/{vin}/window-sticker and GET /v2/vin/{vin}/listings.
  • Bigger batches. POST /v2/vin/batch takes 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 back ok: true with identity.valid: false.
  • Provenance. corgiBuild names 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.

Sources

Checked on 2026-10-26:

If something here is wrong, tell us and we will fix it.