Migration
How to migrate from CarsXE to Cardog
Last updated:
Replace CarsXE's /specs call with two Cardog calls, and use a small adapter you own to keep CarsXE-shaped code working while you compare results.
Most teams start with CarsXE's Specifications endpoint, GET /specs. On Cardog the same data comes from two calls: GET /v2/vin/{vin} for identity and the decoder's attribute map, then GET /v2/specs/nano:{code} for the spec sheet, with every number labelled with its unit and Canadian and US prices kept apart. Most fields have a match, some come back as entity refs that join to the rest of the API, and the table below lists every one.
Cardog wrote this guide. Each claim about CarsXE links to CarsXE's own public documentation, and every page was checked on 2026-10-26. CarsXE can change its docs at any time, so where their pages and this summary differ, go by their pages.
The short version
- Get a Cardog API key. Send it in the
x-api-keyheader, not as akeyquery parameter. PinCardog-Version: 2026-10-21. - Replace
GET https://api.carsxe.com/specs?key=...&vin=...withGET https://api.cardog.app/v2/vin/{vin}. Ifvalidistrue, takenanofrom that response (it looks likenano:5TDGSKFCRS) and callGET https://api.cardog.app/v2/specs/{nano}. - Add the adapter below to your code. It turns those two responses into the CarsXE shape for the fields that map.
- Run your own VINs through both APIs and diff the results before you switch traffic.
- Over time, read the Cardog fields directly and use the refs (
refs.make,nano,squish) as join keys instead of display strings.
How their fields map to ours
"identity" means the GET /v2/vin/{vin} response, defined in packages/contracts/src/schemas/vin.v2.schemas.ts. identity.attributes is the decoder's attribute map for the build, keyed by the ids in our spec catalogue (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. "sheet" means the GET /v2/specs/nano:{code} response, defined in packages/contracts/src/schemas/specs.v2.schemas.ts. Every sheet value is an object { value, unit?, stated? }. stated holds the number in the unit the source published, when that differs from the unit we serve. Attribute ids and units come from packages/entities/generated/spec.json.
CarsXE's field list is from their Specifications page. CarsXE returns every attribute as a string.
| CarsXE field | Cardog field | Note |
|---|---|---|
success | HTTP status + identity.valid | Different. Both APIs return 400 for input that is not a 17-character VIN. CarsXE returns 404 when it finds no data for the VIN. Cardog returns 200 with valid: false and a non-empty reasons array. |
input | identity.vin | Only the VIN is echoed back. |
timestamp | not mapped | identity.corgiBuild names the decoder build that produced the answer. |
attributes.year | identity.year | Same. Integer, not string. |
attributes.make | identity.make | Same. identity.refs.make is the join key. |
attributes.model | identity.model | Same. identity.refs.model is the join key. |
attributes.trim | identity.trim | Same: the catalogue trim decoded from the VIN. Optional. |
attributes.style | identity.refs.bodyStyle | Renamed. A ref such as body-style:sedan-saloon, not the manufacturer's uppercase label. |
attributes.type | identity.refs.vehicleType | Renamed. Ref value. |
attributes.made_in | identity.refs.country | Format change: the plant country as a country ref, such as country:canada, not a display name. null when the decoder does not state one. |
attributes.made_in_city | identity.attributes.plantCity, or sheet.sections.build.plantCity | Renamed. Where the decoder or the sheet has it. |
attributes.engine | not mapped as one string | We serve the parts: identity.attributes.displacementL, cylinders, engineConfiguration, camType, horsepowerFrom, engineModel, and on the sheet sections.engine.*. |
attributes.engine_size | identity.attributes.displacementL, or sheet.sections.engine.displacementL | A number in litres, not a string. |
attributes.engine_cylinders | identity.attributes.cylinders, or sheet.sections.engine.cylinders | Same. Integer. |
attributes.fuel_type | identity.refs.fuelType | Ref value. A dual-fuel vehicle also has refs.fuelTypeSecondary. |
attributes.fuel_capacity | sheet.sections.efficiency.fuelTankCapacity | Unit change: L. stated holds gallons when the source published gallons. |
attributes.city_mileage | sheet.sections.efficiency.fuelEconomyCity | Unit change: L100K. stated holds MPG when published. |
attributes.highway_mileage | sheet.sections.efficiency.fuelEconomyHighway | Unit change: L100K. stated holds MPG when published. |
attributes.transmission | identity.refs.transmission + sheet.sections.drivetrain.transmissionSpeeds | Split into a ref and a count. |
attributes.transmission_short | not mapped | |
attributes.transmission_type | identity.refs.transmission | Ref value. |
attributes.transmission_speeds | identity.attributes.transmissionSpeeds, or sheet.sections.drivetrain.transmissionSpeeds | Same. Integer. |
attributes.drivetrain | identity.refs.driveType | Ref value such as drive-type:4wd-4-wheel-drive-4x4. |
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, or identity.attributes.wheelbase | Unit change: MM on the sheet. Check the attribute's unit. |
attributes.curb_weight | sheet.sections.dimensions.curbWeight, identity.attributes.curbWeight | Unit change: KG on the sheet, with stated in LB. Check the attribute's unit. identity.shippingWeight is an integer in pounds. |
attributes.gross_vehicle_weight_rating | identity.refs.gvwr + identity.grossWeight | The class is a ref. grossWeight is in pounds. |
attributes.doors | identity.doors | Same. Integer. |
attributes.standard_seating | sheet.sections.identity.seatingCapacity, or identity.attributes.seatingCapacity | Renamed. Integer. |
attributes.invoice_price | not mapped | |
attributes.manufacturer_suggested_retail_price | sheet.markets.US.pricing.msrp[], sheet.markets.CA.pricing.msrp[]; also identity.attributes.msrp | On the sheet, every published price as its own row, cheapest first. USD and CAD are kept separate, and we never pick one row. identity.attributes.msrp is the decoder's US base price, where it has one. |
equipment.abs_brakes | sheet.sections.safety.abs, or identity.attributes.abs | Value change: standard, optional or unavailable on the sheet, instead of Std. or empty. |
equipment.driver_airbag, passenger_airbag | sheet.sections.safety.frontAirbags; identity.attributes.frontAirbagLocations | One sheet attribute covers both. The decoder's entry names the locations, where it has them. |
equipment.side_airbag | sheet.sections.safety.sideAirbags; identity.attributes.sideAirbagLocations | Three states on the sheet. |
equipment.traction_control | sheet.sections.safety.tractionControl, or identity.attributes.tractionControl | Three states on the sheet. |
equipment.stability_control | sheet.sections.safety.stabilityControl, or identity.attributes.stabilityControl | Three states on the sheet. |
equipment.navigation_aid | sheet.sections.infotainment.navigation | Renamed. Three states. |
equipment.power_sunroof | sheet.sections.comfort.sunroof | Renamed. Does not say whether it is powered. |
equipment.backup_camera | sheet.sections.driverAssistance.backupCamera, or identity.attributes.backupCamera | Three states on the sheet. |
equipment.bluetooth | sheet.sections.infotainment.bluetooth | Same, three states. |
equipment.keyless_entry | sheet.sections.comfort.keylessEntry | Same, three states. |
equipment.remote_start | sheet.sections.comfort.remoteStart | Same, three states. |
equipment.leather_seat | not mapped | |
equipment.alloy_wheels | not mapped | |
other equipment.* flags | partial | Check each one against GET /v2/specs/catalog. |
colors[] { category, name } | sheet.markets.{US,CA}.colors.colorsExterior[], colorsInterior[] | The category is the array. Each colour has rgb and hex, which are null when not published. Lists every colour offered across the trims behind the build. |
warranties[] Basic | sheet.sections.warranty.warrantyBasicMiles, warrantyBasicMonths | Numbers, not strings like "50,000 mile". Distance in KM, with stated in MI. |
warranties[] Powertrain | sheet.sections.warranty.warrantyPowertrainMiles, warrantyPowertrainMonths | Unit change, as above. |
warranties[] Rust | sheet.sections.warranty.warrantyCorrosionMiles, warrantyCorrosionMonths | Renamed to corrosion. Unit change, as above. |
deepdata | identity.attributes (closest) | Not the same thing. CarsXE's deepdata is raw upstream variables behind an opt-in flag. identity.attributes is always included when the decoder has entries, keyed by our catalogue ids, and it is not a second source. |
format=xml | not mapped | JSON only. |
disableIntVINDecoding | not mapped | Nothing to switch off. A VIN we cannot name returns valid: false with reasons. |
| (none) | identity.nano, identity.squish, identity.refs, identity.links, identity.reasons | Cardog only: build and configuration keys, entity refs, links to recalls and listings, and a fixed set of failure codes. |
| (none) | identity.attributes.* beyond the rows above | Cardog only, where the decoder has them: for example horsepowerFrom, engineModel, fuelSystem, plantCompany, battery fields for EVs, and ADAS entries such as adaptiveCruiseControl. |
A field can be missing from the identity's attributes because the decoder does not have it for that build. A field can be missing from a Cardog sheet for three reasons. No source stated it. The catalogue trims behind the build disagreed, or only some of them stated it. Or the stated value did not fit the catalog's type. Where the decoder states a value for the build, that value is served. We never average catalogue values. CarsXE also documents that every field is optional, so your code should already handle gaps.
An adapter you can paste
This function calls Cardog and returns an object in CarsXE's documented /specs shape. It reads the sheet first and falls back to the identity's attributes. Where we have no matching value, the field is undefined and a comment explains why. It runs in your code. Cardog itself only returns its own v2 shape.
const BASE = "https://api.cardog.app";
const HEADERS = { "x-api-key": process.env.CARDOG_API_KEY!, "Cardog-Version": "2026-10-21" };
type V = { value: unknown; unit?: string; stated?: { value: number; unit: string } };
type A = { value?: unknown; unit?: string };
async function get(path: string) {
const res = await fetch(`${BASE}${path}`, { headers: HEADERS });
return { status: res.status, body: res.ok ? await res.json() : null };
}
// The source's own number, only when it was published in the unit CarsXE uses.
const asStated = (v: V | undefined, unit: string) =>
v?.stated?.unit === unit ? String(v.stated.value) : undefined;
const str = (v: V | undefined) => (v == null ? undefined : String(v.value));
const std = (v: V | undefined) =>
v == null ? undefined : v.value === "standard" ? "Std." : ""; // "optional" has no CarsXE equivalent
export async function carsxeSpecs(vin: string) {
const { status, body: id } = await get(`/v2/vin/${encodeURIComponent(vin)}`);
if (status !== 200 || !id?.valid) {
// CarsXE answers 400 or 404 here; Cardog explains a miss in id.reasons.
return { success: false, input: { vin }, error: id?.reasons ?? status };
}
const sheet = (await get(`/v2/specs/${encodeURIComponent(id.nano)}`)).body; // 404 leaves sheet null
const s = sheet?.sections ?? {};
const a: Record<string, A> = id.attributes ?? {};
// A decoder attribute with no unit attached, as a string.
const plain = (key: string) =>
a[key]?.value != null && !a[key].unit ? String(a[key].value) : undefined;
const us = sheet?.markets?.US;
const usd = us?.pricing?.msrp ?? [];
return {
success: true,
input: { vin },
timestamp: undefined, // not returned by Cardog
attributes: {
year: id.year != null ? String(id.year) : undefined,
make: id.make ?? undefined,
model: id.model ?? undefined,
trim: id.trim ?? undefined,
style: undefined, // Cardog returns a ref: id.refs.bodyStyle
type: undefined, // ref: id.refs.vehicleType
made_in: undefined, // ref: id.refs.country, e.g. "country:canada"
made_in_city: str(s.build?.plantCity) ?? plain("plantCity"),
engine: undefined, // no combined descriptor; parts are in s.engine and a.*
engine_size: str(s.engine?.displacementL) ?? plain("displacementL"),
engine_cylinders: str(s.engine?.cylinders) ?? plain("cylinders"),
fuel_type: undefined, // ref: id.refs.fuelType
fuel_capacity: asStated(s.efficiency?.fuelTankCapacity, "GAL"),
city_mileage: asStated(s.efficiency?.fuelEconomyCity, "MPG"),
highway_mileage: asStated(s.efficiency?.fuelEconomyHighway, "MPG"),
transmission: undefined, // ref id.refs.transmission + s.drivetrain.transmissionSpeeds
transmission_short: undefined, // not mapped
transmission_type: undefined, // ref: id.refs.transmission
transmission_speeds: str(s.drivetrain?.transmissionSpeeds) ?? plain("transmissionSpeeds"),
drivetrain: undefined, // ref: id.refs.driveType
// Dimensions are held in MM; these fill only when a source published inches.
overall_height: asStated(s.dimensions?.height, "IN"),
overall_length: asStated(s.dimensions?.length, "IN"),
overall_width: asStated(s.dimensions?.width, "IN"),
wheelbase_length: asStated(s.dimensions?.wheelbase, "IN"),
curb_weight: asStated(s.dimensions?.curbWeight, "LB"),
gross_vehicle_weight_rating: undefined, // ref id.refs.gvwr; id.grossWeight is lbs
doors: id.doors != null ? String(id.doors) : undefined,
standard_seating: str(s.identity?.seatingCapacity) ?? plain("seatingCapacity"),
invoice_price: undefined, // not mapped
// Only when one US price stands; several trims can share a build.
manufacturer_suggested_retail_price:
usd.length === 1 ? `$${usd[0].msrp.toLocaleString("en-US")} USD` : undefined,
},
equipment: {
abs_brakes: std(s.safety?.abs),
driver_airbag: std(s.safety?.frontAirbags),
passenger_airbag: std(s.safety?.frontAirbags),
side_airbag: std(s.safety?.sideAirbags),
traction_control: std(s.safety?.tractionControl),
stability_control: std(s.safety?.stabilityControl),
navigation_aid: std(s.infotainment?.navigation),
power_sunroof: std(s.comfort?.sunroof),
backup_camera: std(s.driverAssistance?.backupCamera),
bluetooth: std(s.infotainment?.bluetooth),
keyless_entry: std(s.comfort?.keylessEntry),
remote_start: std(s.comfort?.remoteStart),
leather_seat: undefined, // not mapped
alloy_wheels: undefined, // not mapped
},
colors: [
...(us?.colors?.colorsExterior ?? []).map((c: { name: string }) => ({ category: "Exterior", name: c.name })),
...(us?.colors?.colorsInterior ?? []).map((c: { name: string }) => ({ category: "Interior", name: c.name })),
],
warranties: undefined, // available in s.warranty as numbers (KM / MONTHS); not reformatted here
};
}asStated returns a value only when the source published the number in the unit CarsXE uses. We hold dimensions in millimetres, so the four inch fields will often be undefined. Curb weight, fuel and economy usually carry the US number in stated. If you want every value, convert from our metric value yourself. Either way you know which numbers were published and which you calculated.
Verify before you switch
- Pick 20 to 50 VINs from your own traffic. Include old and new model years, a few EVs and hybrids, and any Canadian-market vehicles you see.
- Call CarsXE
/specsandcarsxeSpecs()for each one. Save both responses. - Diff the responses field by field. Expect three kinds of difference: units (inches against millimetres), equipment values (
optionalbecomes empty), and fields we leave undefined. Anything else needs a closer look. - List the VINs where CarsXE returned 404 and Cardog returned
valid: true, and the reverse. On the Cardog side,reasonssays why a VIN missed. - Switch over only when you have accepted every difference.
What Cardog adds
- Canadian VINs to trim. We decode 99.77% of Canadian VINs to trim level.
- Canadian and US prices kept separate.
markets.CA(CAD) andmarkets.US(USD) are separate and never merged. - Metric units, with the published number kept. Every number states its unit, and
statedholds the source's original when it was published in another unit. - Recalls from Transport Canada and NHTSA. Each recall names its issuing authority:
tcornhtsa. The VIN check isGET /v2/vin/{vin}/recalls. CarsXE's recalls page names NHTSA as its source and does not mention Transport Canada (Recalls). - A reason for every miss. When
validisfalse,reasonsgives a code from a fixed list, such asCHECK_DIGIT_MISMATCHorUNSEEN_BUILD. - Join keys.
nanoidentifies the build,squishthe configuration, andrefs.*the entities. You can join records on these instead of on name strings. - Three-state equipment.
optionalis a separate value fromunavailable. - Batch decode.
POST /v2/vin/batchtakes 1 to 1,000 VINs, and a bad VIN fails only its own row.
FAQ
Does Cardog accept the API key as a query parameter like CarsXE?
No. Send it in the x-api-key header or as Authorization: Bearer.
Why does a field CarsXE returned come back empty? Either no source states it for that build, or the catalogue trims behind the build disagree and we will not pick one. The mapping table shows which fields are not mapped.
Why two calls instead of one?
Identity and spec sheets are separate resources. nano from the first call is the key for the second.
How much does the two-call flow cost?
The identity route is priced at two credits per VIN and each spec sheet request at one credit, so one CarsXE /specs call becomes three Cardog credits. 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.
Do I have to use refs?
No. Display names (make, model) are in the identity response. Refs are there for joining records.
Related
- Cardog vs CarsXE
- CarsXE alternatives
- Best VIN decoder APIs (2026)
- API docs
- Decode a Canadian VIN
- Pricing
Sources
Every CarsXE page below was checked on 2026-10-26:
- CarsXE Specifications
- CarsXE Market Value
- CarsXE Recalls
- CarsXE Supported Countries
- CarsXE documentation index
If something here is wrong, tell us and we will fix it.
Was this helpful?