Decode a Canadian VIN by API

One VIN in, one identity card out — entity refs, VIN grains, and links, for 99.77% of Canadian VINs.

bash
curl "https://api.cardog.app/v2/vin/2T3R1RFV7MW180266" \
  -H "x-api-key: $CARDOG_API_KEY"

One VIN in, one identity card out: the display names to render, the entity refs to store, the two VIN grains, and links to this vehicle's market instrument, recalls, and live listings. Two credits per VIN, batch or single.

The reason this is a separate product from the free American decoder is coverage. NHTSA's vPIC is built from US regulatory submissions, so a Canadian-market vehicle decodes to a make and a model and then stops — no trim, sometimes no engine, sometimes nothing at all for a model line that never went on sale in the United States. We decode 99.77% of Canadian VINs to trim level, and where a pattern isn't in any public registry we carry our own. Every fact comes back keyed to a ref, so the decode output joins directly against specs, listings, market quotes, and recalls without a name-matching step.

The response:

Example

10 keys
"response": {
"vin": "2T3R1RFV7MW180266",
"valid": true,
"year": 2021,
"make": "Toyota",
"model": "RAV4",
"trim": "XLE HV",
"refs": {
"make": "make:toyota",
"model": "model:toyota/rav4",
"modelYear": "model-year:toyota/rav4/2021",
"bodyStyle": "body-style:sport-utility-vehicle-suv-multi-purpose-vehicle-mpv",
"fuelType": "fuel-type:gasoline",
"driveType": "drive-type:4wd-4-wheel-drive-4x4",
"transmission": null,
"electrificationLevel": null,
"vehicleType": null,
"country": null
},
"nano": "nano:2T3R1RFVMW",
"squish": "squish:2T3R1RFVM",
"links": {
"instrument": "/v2/vin/2T3R1RFV7MW180266/instrument",
"recalls": "/v2/vin/2T3R1RFV7MW180266/recalls",
"listings": "/v2/vin/2T3R1RFV7MW180266/listings"
}
}

refs are the point. The display names are for your UI; the refs are for your database. Write make:toyota into the column, not "Toyota" — every other endpoint takes the ref, and it will still mean Toyota in five years. null in any ref field means not derivable for this VIN — never "we don't know the ref."

What comes back

  • valid — the decoder's verdict, and only the decoder's. false means no identity: every field below it is null and every ref is null. The API never fills an unresolved VIN from adjacent data.
  • year, make, model, trim — display names. Render these; join on the refs. trim is the catalogue trim this VIN's own pattern resolves to — never a seller's listing text, which ships separately in observedTrim, tagged and dated.
  • refs — the join keys, one per domain. Every other endpoint takes them. The grammar they obey is the ref grammar.
  • nano and squish — the two VIN grains, covered below.
  • links — server-relative paths to this VIN's adjacent resources. Follow them instead of building URLs by hand.

The capture above is trimmed. The live card also carries provenance — authorityTier for the identity facts, trimAuthorityTier on the trim — and links grows as the decode resolves: per-ref entity links, plus specs, quote, and safety once the model year is known. The full spec sheet is one link away at links.specs; on the MCP surface, the identify_vehicle tool skims it into a specHighlights block. The complete response shape is in the VIN reference.

null means not-derivable, never unknown

A null in any identity field is a fact about the VIN, not a gap in our lookup: it means this VIN's pattern does not derive that fact. It is never "we don't know the ref", never a fuzzy match withheld, and it will not turn into a guess on retry. The refs also cohere as a set — a model ref only ships under the make ref beside it, and a pairing that cannot hold together goes null rather than travel as a plausible-looking join key. That is what makes a decode trustworthy in a pipeline: every non-null ref can be written straight into a column with no validation pass.

Batch

Same contract, up to 1,000 VINs per call:

bash
curl -X POST "https://api.cardog.app/v2/vin/batch" \
  -H "x-api-key: $CARDOG_API_KEY" \
  -H "content-type: application/json" \
  -d '{"vins":["2T3R1RFV7MW180266","1HGCM82633A123456"]}'

The batch is all-accepted, per-item-resolved: one malformed VIN fails its own row with the standard error envelope and never the batch. Results come back in request order, with a meta block counting requested, decoded, and failed. Metered per VIN — N VINs is N decode units.

The TypeScript SDK is the same surface, typed:

typescript
import { CardogClient } from "@cardog/api";

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });
const identity = await client.v2.vin.getByVin("2T3R1RFV7MW180266");

// Store the refs, render the names.
await db.vehicles.update(id, {
  makeRef: identity.refs.make,          // "make:toyota"
  modelYearRef: identity.refs.modelYear, // "model-year:toyota/rav4/2021"
  squish: identity.squish,               // "squish:2T3R1RFVM"
});

Why Canadian VINs are hard, specifically

vPIC is the decoder everyone starts with, and inside its boundary it is good: free, public, and correct for US-market vehicles, because it is built from the submissions US regulation compels. The boundary is the border. A trim configuration sold only in Canada has no US submission behind it, so the pattern is simply absent — the decode returns a make, usually a model, and stops. A model line that never went on sale in the United States can return nothing at all.

Our coverage comes from treating VIN patterns as a registry to maintain, not a dataset to download: where a Canadian pattern exists in a public registry we use it, and where it doesn't we derive and carry our own pattern extensions. That is what carries decode coverage to 99.77% of Canadian VINs at trim level.

VIN structure is the same on both sides of the border. For WMI, check digits, and year codes — the half of decoding that is arithmetic, not data — see Understanding VINs.

The grains: squish and nano

Two keys in the identity card are pure projections of the VIN itself:

  • squish — WMI + VDS + year, plant-agnostic. The market grain: two VINs with the same squish are the same configuration, which is why a squish is a valid instrument for market quotes.
  • nano — squish plus the plant code. The build grain: the dedup and comparables key.

Because both are projections, they are derivable offline from any VIN with @cardog/entities — zero API calls, zero network:

typescript
import { squishFromVin, nanoFromVin } from "@cardog/entities";

const vin = "2T3R1RFV7MW180266";

squishFromVin(vin); // "2T3R1RFVM"  — the market grain (WMI + VDS + year)
nanoFromVin(vin);   // "2T3R1RFVMW" — the build grain (+ plant)

The key you compute offline is the key the API returns — the identity card ships it in ref form (squish:2T3R1RFVM), from the same grammar. You can index a table on the squish grain before you ever call us.

From identity to everything else

The identity card is the entry point to the rest of the graph. Every card carries a links block — rel → server-relative path — so the adjacent resources are one traversal away:

  • instrument — the market instrument this VIN trades as, and from it the live quote
  • recalls — Transport Canada and NHTSA campaigns for this VIN; see the recall lookup
  • listings — live listings of this vehicle

Follow the links instead of building URLs by hand.

Errors

Every non-2xx response is one envelope. A malformed VIN:

Example

4 keys
"response": {
"code": "invalid_vin",
"message": "Not a valid VIN: 2T3R1RFV7MW18026",
"hint": "A VIN is 17 characters, A–Z (no I/O/Q) and 0–9",
"docs_url": "https://cardog.app/docs/errors/invalid_vin.md"
}

code is machine-dispatchable, hint says what to do next, and docs_url is fetchable markdown — the full correction recipe, no HTML parsing. In a batch, the same envelope appears on the failing row's error field while the rest of the batch decodes. The complete code list is in Errors.