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

  1. Replace GET https://specifications.vinaudit.com/v3/specifications?vin=...&key=... with GET https://api.cardog.app/v2/vin/{vin} and send your key in the x-api-key header.
  2. Follow links.specsBuild (or links.specs when the build link is missing) from that response to get the spec sheet.
  3. If you used include=recalls, call GET /v2/vin/{vin}/recalls as well.
  4. Drop the adapter below in front of your existing code so nothing downstream changes yet.
  5. 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 fieldCardog fieldNote
successHTTP status, then card.validRenamed. A malformed VIN is a 400 with code invalid_vin. A well-formed VIN we cannot name is a 200 with valid: false.
errorerror 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.vinThe VIN.
attributes.yearcard.yearSame. A number, not a string.
attributes.makecard.make (card.refs.make for joins)Same.
attributes.modelcard.model (card.refs.model)Same.
attributes.trimcard.trimSame. Catalogue trim read from the VIN. Can be null.
attributes.stylesheet.markets.{US,CA}.pricing.msrp[].stylePartial. One style per published price, and there can be several.
attributes.typecard.refs.vehicleTypeRenamed. A ref string, different vocabulary.
attributes.sizenot mapped
attributes.categorycard.refs.bodyStyleClosest field only. Body style, not a size class.
attributes.made_incard.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_citycard.attributes.plantCity, or sheet.sections.build.plantCityRenamed.
attributes.doorscard.doorsSame. A number.
attributes.fuel_typecard.refs.fuelTypeRenamed. A ref. Plug-in hybrids also get refs.fuelTypeSecondary.
attributes.fuel_capacitysheet.sections.efficiency.fuelTankCapacityUnit change: litres. stated keeps gallons when published that way.
attributes.city_mileagesheet.sections.efficiency.fuelEconomyCityUnit change: L/100 km. stated keeps MPG. One number, not a range.
attributes.highway_mileagesheet.sections.efficiency.fuelEconomyHighwayUnit change, as above.
attributes.enginethe partsBuild a description from the parts: card.attributes.displacementL, cylinders, engineConfiguration, camType, valves on the sheet.
attributes.engine_sizecard.attributes.displacementL, or sheet.sections.engine.displacementLSame unit. A number, not "1.8-L".
attributes.engine_cylinderscylinders and engineConfiguration, on the card's attributes or sheet.sections.engineSplit in two. engineConfiguration is an entity, not text. The "L-4" format is not served.
attributes.transmissioncard.refs.transmissionA ref, not a text list of every offered gearbox.
attributes.transmission_typecard.refs.transmissionRenamed. A ref.
attributes.transmission_speedscard.attributes.transmissionSpeeds, or sheet.sections.drivetrain.transmissionSpeedsRenamed. A number.
attributes.drivetraincard.refs.driveTypeRenamed. A ref.
attributes.anti_brake_systemsheet.sections.safety.abs, sheet.sections.chassis.brakeSystemType; also card.attributes.abs, card.attributes.brakeSystemTypeDifferent shape. On the sheet abs is an availability value (standard or optional) and brakeSystemType is an entity. Neither is a description string.
attributes.steering_typesheet.sections.chassis.steeringTypeRenamed.
attributes.curb_weightsheet.sections.dimensions.curbWeight, card.attributes.curbWeightUnit change: kg on the sheet, stated keeps pounds. Check the attribute's unit. The card also has shippingWeight in pounds.
attributes.gross_vehicle_weight_ratingcard.grossWeight (pounds), card.refs.gvwrSame quantity. Cardog adds the GVWR class.
attributes.gross_weight (XML sample)card.grossWeightSame.
attributes.overall_heightsheet.sections.dimensions.heightUnit change: mm.
attributes.overall_lengthsheet.sections.dimensions.lengthUnit change: mm.
attributes.overall_widthsheet.sections.dimensions.widthUnit change: mm.
attributes.wheelbase_lengthsheet.sections.dimensions.wheelbase, card.attributes.wheelbaseUnit change: mm on the sheet. Check the attribute's unit.
attributes.standard_seatingsheet.sections.identity.seatingCapacity, or card.attributes.seatingCapacityRenamed. A number.
attributes.invoice_pricenot mapped
attributes.delivery_chargesnot mapped
attributes.manufacturer_suggested_retail_pricesheet.markets.US.pricing.msrp[].msrp, sheet.markets.CA.pricing.msrp[].msrp; also card.attributes.msrpOn 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[].sourcerecalls[].authorityLabel (from /v2/vin/{vin}/recalls)Separate call. Renamed.
recalls[].campaignrecalls[].campaignNumberRenamed.
recalls[].daterecalls[].recallDateRenamed. YYYY-MM-DD.
recalls[].componentsrecalls[].componentRenamed.
recalls[].summaryrecalls[].defectSummaryRenamed. 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

  1. 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.
  2. Run each through your current call and through the adapter.
  3. 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.
  4. Check failures on both sides. On Cardog, read reasons for any valid: false before you count it as a miss.
  5. Measure how often each field is filled on both sides. A field in our catalog is not filled for every build.
  6. For bulk jobs, POST /v2/vin/batch takes 1 to 1000 VINs and one bad VIN only fails its own row.

What Cardog adds

  • Canadian recalls. GET /v2/vin/{vin}/recalls returns 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 stated keeps the original when the source used another one (units).
  • A reason for every miss. A VIN we cannot name comes back with reasons, such as CHECK_DIGIT_MISMATCH or UNSEEN_BUILD.
  • Stable join keys. refs, nano (build code) and squish (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.

Sources

Checked on 2026-10-26:

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