Migration
How to migrate from VinAudit to Cardog
Last updated:
Swap VinAudit's specifications call for Cardog's VIN and spec sheet calls, with a field map and an adapter.
VinAudit's specifications call returns one flat block of strings for a VIN. On Cardog the same answer comes from two calls: the VIN identity card, which carries the decoder's attribute map and refs that join to recalls and prices, then the spec sheet it links to, with every number in metric, labelled with its unit, and Canadian and US prices kept apart.
This guide was written by Cardog. Every claim about VinAudit links to VinAudit's own public documentation, and we checked those pages on 2026-10-26. Every Cardog field named here is in our published contract or spec catalog.
The short version
- Replace
GET https://specifications.vinaudit.com/v3/specifications?vin=...&key=...withGET https://api.cardog.app/v2/vin/{vin}and send your key in thex-api-keyheader. - Follow
links.specsBuild(orlinks.specswhen the build link is missing) from that response to get the spec sheet. - If you used
include=recalls, callGET /v2/vin/{vin}/recallsas well. - Drop the adapter below in front of your existing code so nothing downstream changes yet.
- Diff a sample of VINs between the two before you switch traffic.
How their fields map to ours
VinAudit's fields come from the JSON sample on its VIN Decoder API docs. "Card" is the response of GET /v2/vin/{vin}. "Sheet" is the response of GET /v2/specs/nano:{code}. Sheet values are objects of the form { value, unit, stated }, where stated keeps the number in the unit the source published it in. The sheet also carries the card's refs; a value served there is not repeated in sections.
card.attributes is the decoder's attribute map for the build, keyed by the ids in our spec catalog (GET /v2/specs/catalog). Each entry is an object, usually { value }, and a measured value can carry a unit, so check it before you use a number. The map is sparse: an entry is there only where the decoder has that fact for the build. Where a row below names both, the sheet and the card's attributes are two places to look, not two different facts.
| VinAudit field | Cardog field | Note |
|---|---|---|
success | HTTP status, then card.valid | Renamed. A malformed VIN is a 400 with code invalid_vin. A well-formed VIN we cannot name is a 200 with valid: false. |
error | error code, or card.reasons[] | Renamed. Cardog's codes are its own list, not VinAudit's. reasons says why a VIN did not resolve, from a fixed list. |
input.* | card.vin | The VIN. |
attributes.year | card.year | Same. A number, not a string. |
attributes.make | card.make (card.refs.make for joins) | Same. |
attributes.model | card.model (card.refs.model) | Same. |
attributes.trim | card.trim | Same. Catalogue trim read from the VIN. Can be null. |
attributes.style | sheet.markets.{US,CA}.pricing.msrp[].style | Partial. One style per published price, and there can be several. |
attributes.type | card.refs.vehicleType | Renamed. A ref string, different vocabulary. |
attributes.size | not mapped | |
attributes.category | card.refs.bodyStyle | Closest field only. Body style, not a size class. |
attributes.made_in | card.refs.country (also sheet.refs.country) | Format change: the plant country as a country ref, such as country:canada, not a region like "America". null when the decoder does not state one. |
attributes.made_in_city | card.attributes.plantCity, or sheet.sections.build.plantCity | Renamed. |
attributes.doors | card.doors | Same. A number. |
attributes.fuel_type | card.refs.fuelType | Renamed. A ref. Plug-in hybrids also get refs.fuelTypeSecondary. |
attributes.fuel_capacity | sheet.sections.efficiency.fuelTankCapacity | Unit change: litres. stated keeps gallons when published that way. |
attributes.city_mileage | sheet.sections.efficiency.fuelEconomyCity | Unit change: L/100 km. stated keeps MPG. One number, not a range. |
attributes.highway_mileage | sheet.sections.efficiency.fuelEconomyHighway | Unit change, as above. |
attributes.engine | the parts | Build a description from the parts: card.attributes.displacementL, cylinders, engineConfiguration, camType, valves on the sheet. |
attributes.engine_size | card.attributes.displacementL, or sheet.sections.engine.displacementL | Same unit. A number, not "1.8-L". |
attributes.engine_cylinders | cylinders and engineConfiguration, on the card's attributes or sheet.sections.engine | Split in two. engineConfiguration is an entity, not text. The "L-4" format is not served. |
attributes.transmission | card.refs.transmission | A ref, not a text list of every offered gearbox. |
attributes.transmission_type | card.refs.transmission | Renamed. A ref. |
attributes.transmission_speeds | card.attributes.transmissionSpeeds, or sheet.sections.drivetrain.transmissionSpeeds | Renamed. A number. |
attributes.drivetrain | card.refs.driveType | Renamed. A ref. |
attributes.anti_brake_system | sheet.sections.safety.abs, sheet.sections.chassis.brakeSystemType; also card.attributes.abs, card.attributes.brakeSystemType | Different shape. On the sheet abs is an availability value (standard or optional) and brakeSystemType is an entity. Neither is a description string. |
attributes.steering_type | sheet.sections.chassis.steeringType | Renamed. |
attributes.curb_weight | sheet.sections.dimensions.curbWeight, card.attributes.curbWeight | Unit change: kg on the sheet, stated keeps pounds. Check the attribute's unit. The card also has shippingWeight in pounds. |
attributes.gross_vehicle_weight_rating | card.grossWeight (pounds), card.refs.gvwr | Same quantity. Cardog adds the GVWR class. |
attributes.gross_weight (XML sample) | card.grossWeight | Same. |
attributes.overall_height | sheet.sections.dimensions.height | Unit change: mm. |
attributes.overall_length | sheet.sections.dimensions.length | Unit change: mm. |
attributes.overall_width | sheet.sections.dimensions.width | Unit change: mm. |
attributes.wheelbase_length | sheet.sections.dimensions.wheelbase, card.attributes.wheelbase | Unit change: mm on the sheet. Check the attribute's unit. |
attributes.standard_seating | sheet.sections.identity.seatingCapacity, or card.attributes.seatingCapacity | Renamed. A number. |
attributes.invoice_price | not mapped | |
attributes.delivery_charges | not mapped | |
attributes.manufacturer_suggested_retail_price | sheet.markets.US.pricing.msrp[].msrp, sheet.markets.CA.pricing.msrp[].msrp; also card.attributes.msrp | On the sheet, a list of published prices per market, cheapest first, a number in whole units of that market's currency (USD or CAD). The two markets are never merged. card.attributes.msrp is the decoder's US base price, where it has one. |
selections.trims[] | GET /v2/specs/{model-year ref}?trim= | Different model. A ref plus a trim, not a selection tree. |
colors[] | sheet.markets.{US,CA}.colors.colorsExterior[] / colorsInterior[] | category becomes which list. Cardog adds rgb and hex where published. |
equipment[] | sheet.sections.{safety,comfort,driverAssistance,infotainment,lighting,connectivity}.*; some on card.attributes (for example adaptiveCruiseControl, backupCamera, stabilityControl) | Not available as a list. Keyed fields instead. |
recalls[].source | recalls[].authorityLabel (from /v2/vin/{vin}/recalls) | Separate call. Renamed. |
recalls[].campaign | recalls[].campaignNumber | Renamed. |
recalls[].date | recalls[].recallDate | Renamed. YYYY-MM-DD. |
recalls[].components | recalls[].component | Renamed. |
recalls[].summary | recalls[].defectSummary | Renamed. Cardog also gives consequenceSummary and correctiveAction. |
warranties[] | sheet.sections.warranty.warrantyBasicMonths, warrantyBasicMiles, warrantyPowertrainMonths, ... | Keyed fields, not a list. Distance in km, stated keeps miles. |
photos[] | not mapped |
Card-only fields with no VinAudit match include nano, squish, refs, reasons, links and, where the decoder has them, more card.attributes entries such as horsepowerFrom, engineModel, fuelSystem and battery fields for EVs.
Where a value is a "ref" (for example drive-type:...), it is a stable key you can join on, not display text. See ref grammar.
A drop-in adapter
This returns an object in the shape VinAudit documents for attributes, filled from Cardog where a field maps, and undefined where it does not. It reads the sheet first and falls back to the card's attributes for plain values. It converts our metric values back to VinAudit's US units, preferring the source's own stated number when we have it. Cardog does not serve a VinAudit-shaped response itself.
type Val = { value: unknown; unit?: string; stated?: { value: number; unit: string } };
type Sheet = { sections: Record<string, Record<string, Val>>; markets: any };
const BASE = "https://api.cardog.app";
async function get(path: string, key: string) {
const res = await fetch(BASE + path, { headers: { "x-api-key": key } });
return { status: res.status, body: await res.json() };
}
// Prefer the number the source published in the wanted unit; else convert.
function us(v: Val | undefined, wantUnit: string, fromMetric: (n: number) => number) {
if (!v || typeof v.value !== "number") return undefined;
if (v.stated?.unit === wantUnit) return v.stated.value;
return Math.round(fromMetric(v.value) * 100) / 100;
}
export async function vinauditSpecifications(vin: string, key: string) {
const card = await get(`/v2/vin/${encodeURIComponent(vin)}`, key);
// Cardog's own error code (e.g. invalid_vin, unauthorized, rate_limited), not VinAudit's list.
if (card.status !== 200) return { success: false, error: card.body?.code ?? "failed_request" };
const c = card.body;
if (!c.valid) return { success: false, error: "invalid_vin" }; // c.reasons says why
const link = c.links.specsBuild ?? c.links.specs;
const sheet: Sheet | undefined = link ? (await get(link, key)).body : undefined;
const s = sheet?.sections ?? {};
const v = (sec: string, id: string) => s[sec]?.[id];
const str = (x: unknown) => (x == null ? undefined : String(x));
const a: Record<string, { value?: unknown; unit?: string }> = c.attributes ?? {};
// A decoder attribute with no unit attached.
const plain = (id: string) => (a[id]?.value != null && !a[id].unit ? String(a[id].value) : undefined);
const usPrice = sheet?.markets?.US?.pricing?.msrp?.[0]; // cheapest of possibly several
return {
success: true,
error: "",
attributes: {
year: str(c.year),
make: c.make ?? undefined,
model: c.model ?? undefined,
trim: c.trim ?? undefined,
style: usPrice?.style ?? undefined,
type: c.refs.vehicleType ?? undefined, // a ref, not VinAudit's wording
size: undefined, // not mapped
category: undefined, // not mapped (refs.bodyStyle is not a size class)
made_in: c.refs.country ?? undefined, // a ref, e.g. "country:canada"
made_in_city: str(v("build", "plantCity")?.value) ?? plain("plantCity"),
doors: str(c.doors),
fuel_type: c.refs.fuelType ?? undefined, // a ref
fuel_capacity: str(us(v("efficiency", "fuelTankCapacity"), "GAL", (l) => l / 3.78541)),
city_mileage: str(us(v("efficiency", "fuelEconomyCity"), "MPG", (l100) => 235.215 / l100)),
highway_mileage: str(us(v("efficiency", "fuelEconomyHighway"), "MPG", (l100) => 235.215 / l100)),
engine: undefined, // no single description string
engine_size: str(v("engine", "displacementL")?.value) ?? plain("displacementL"),
engine_cylinders: str(v("engine", "cylinders")?.value) ?? plain("cylinders"),
transmission: c.refs.transmission ?? undefined, // a ref
transmission_type: c.refs.transmission ?? undefined,
transmission_speeds: str(v("drivetrain", "transmissionSpeeds")?.value) ?? plain("transmissionSpeeds"),
drivetrain: c.refs.driveType ?? undefined, // a ref
anti_brake_system: str(v("safety", "abs")?.value), // availability, not a description
steering_type: str(v("chassis", "steeringType")?.value),
curb_weight: str(us(v("dimensions", "curbWeight"), "LB", (kg) => kg / 0.453592)),
gross_vehicle_weight_rating: str(c.grossWeight), // pounds
overall_height: str(us(v("dimensions", "height"), "IN", (mm) => mm / 25.4)),
overall_length: str(us(v("dimensions", "length"), "IN", (mm) => mm / 25.4)),
overall_width: str(us(v("dimensions", "width"), "IN", (mm) => mm / 25.4)),
wheelbase_length: str(us(v("dimensions", "wheelbase"), "IN", (mm) => mm / 25.4)),
standard_seating: str(v("identity", "seatingCapacity")?.value) ?? plain("seatingCapacity"),
invoice_price: undefined, // not mapped
delivery_charges: undefined, // not mapped
manufacturer_suggested_retail_price: usPrice ? `$${usPrice.msrp.toLocaleString("en-US")}` : undefined,
},
};
}Three things to know. VinAudit puts the unit inside the string ("2590 pounds"), and this adapter returns bare numbers as strings, so if your code parses the unit off, add it back. When several trims share a VIN pattern, Cardog lists every price and never picks one; the adapter takes the cheapest, which you may want to change. And type, fuel_type, transmission and drivetrain come back as refs, so map them to your own display words if you show them.
Verify before you switch
- Pick 20 to 50 VINs you already have VinAudit answers for. Include a few that VinAudit rejected and a few Canadian-built and Canadian-market vehicles.
- Run each through your current call and through the adapter.
- Compare field by field. Expect year, make, model, doors and engine size to match. Expect small differences in converted numbers (rounding) and in wording for refs.
- Check failures on both sides. On Cardog, read
reasonsfor anyvalid: falsebefore you count it as a miss. - Measure how often each field is filled on both sides. A field in our catalog is not filled for every build.
- For bulk jobs,
POST /v2/vin/batchtakes 1 to 1000 VINs and one bad VIN only fails its own row.
What Cardog adds
- Canadian recalls.
GET /v2/vin/{vin}/recallsreturns Transport Canada campaigns (authority: "tc") as well as NHTSA, with consequence and remedy text. - Prices kept per market. The spec sheet keeps US and CA prices apart, in USD and CAD, never merged.
- Stated units. Every number states its unit, and
statedkeeps the original when the source used another one (units). - A reason for every miss. A VIN we cannot name comes back with
reasons, such asCHECK_DIGIT_MISMATCHorUNSEEN_BUILD. - Stable join keys.
refs,nano(build code) andsquish(the configuration market prices are quoted at) let you join VINs to each other and to market data. - Batch decode of up to 1,000 VINs per request, where one bad VIN fails only its own row.
FAQ
Why two calls instead of one? The card answers "what is this VIN". The sheet answers "what was this build specified with". You can cache the card for a day when it is valid.
How is a decode billed?
The identity route is priced at two credits per VIN and a spec sheet at one credit. Today a single GET /v2/vin/{vin} bills its two credits for any 200 answer, including valid: false, while a batch bills only the rows that come back valid: true. A 400 costs nothing. See /pricing.
Why are my numbers metric now?
Cardog serves metric (except horsepower and inch sizes) and keeps the source number in stated. The adapter converts back.
What about VinAudit's market value call?
Cardog's card links to a quote for the model year in links.quote. The inputs and outputs differ from VinAudit's, so it needs its own comparison. See market data.
What happens with an invalid VIN?
A malformed VIN returns 400 with code invalid_vin. A well-formed VIN we cannot name returns 200, valid: false and reasons. See errors.
How do I authenticate?
Send x-api-key on every request. See authentication.
Related
- Cardog vs VinAudit
- VinAudit alternatives
- Best VIN decoder APIs (2026)
- Docs home
- Decode a Canadian VIN
- Recalls
- Units
- Pricing
Sources
Checked on 2026-10-26:
- VinAudit VIN Decoder API documentation: https://data.vinaudit.com/docs/api/vin/decoder
- VinAudit VIN Decoder API product page: https://data.vinaudit.com/vin-decoder-api
- VinAudit Market Values API documentation: https://data.vinaudit.com/docs/api/market/values
- VinAudit documentation index: https://data.vinaudit.com/docs
If something here is wrong, tell us and we will fix it.
Was this helpful?