# Cardog API v2 documentation VIN identity, canonical specs, live market data, and recalls — one key. **The ref is the API.** Every parameter is an entity ref (`make:tesla`, `model-year:honda/cr-v/2026`) or a VIN; every response carries refs plus `links` to adjacent resources. Free text is accepted in exactly one place — `GET /v2/entities/resolve` — and returns refs with confidence, never silently fuzzed results. **Errors are instructions.** Every non-2xx body is the ErrorEnvelope: `code`, `message`, `hint`, `docs_url`, the offending `refs` named, and nearest-ref `suggestions` where computable. **Price is machine-readable.** `GET /v2/pricing` is the live rate card; every metered response carries `X-Credits-*` headers. - Base URL: `https://api.cardog.app` - Contract version: 2026-10-26 - Auth: API key via `x-api-key` or `Authorization: Bearer`. Create one at https://cardog.app/login?next=/account/api - Machine-readable spec: `GET https://api.cardog.app/v2/openapi.json` - Live route groups today: `vin`, `entities`, `specs`, `listings`, `instruments`, `quotes`, `tape`, `recalls`, `safety`, `charging`, `fuel`, `locations`, `feedback`, `parts`, `history`, `carriers`, `platform` (the rest are frozen contract, not yet mounted) - Every docs page has a markdown twin: append `.md`, or send `Accept: text/markdown` --- # Quickstart Get a key, save it in your shell, and decode a Canadian VIN. Base URL: `https://api.cardog.app`. 1. **Get an API key** [Create a key](https://cardog.app/login?next=/account/api). Sign in with your phone or a passkey, then create a key. It is shown once, so copy it before you close the dialog. 2. **Save it in your shell** ```bash export CARDOG_API_KEY="crdg_..." ``` 3. **Decode a VIN** ```bash curl "https://api.cardog.app/v2/vin/2T3R1RFV7MW180266" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` ```typescript // npm install @cardog/api import { CardogClient } from "@cardog/api"; const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY }); const result = await client.v2.vin.getByVin("2T3R1RFV7MW180266"); ``` ```python # pip install cardog import os from cardog import Cardog client = Cardog(api_key=os.environ["CARDOG_API_KEY"]) result = client.v2.vin.get_by_vin("2T3R1RFV7MW180266") ``` You get one identity card back. A decode costs 2 credits; the response headers say what is left: ```http X-Credits-Rate: 2 X-Credits-Remaining: 48 ``` ```json { "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" }, "links": { "instrument": "/v2/vin/2T3R1RFV7MW180266/instrument", "recalls": "/v2/vin/2T3R1RFV7MW180266/recalls", "listings": "/v2/vin/2T3R1RFV7MW180266/listings" } } ``` ## Next steps - [Resolve free text to refs](https://cardog.app/docs/entity-resolution): turn "2021 civic" into refs every endpoint takes. - [Recalls by VIN](https://cardog.app/docs/recalls): open campaigns for the car you just decoded. - [MCP server](https://cardog.app/docs/mcp): give an agent the same calls as tools. - [Authentication](https://cardog.app/docs/authentication): both headers, plans and key hygiene. --- # Authentication Every `/v2/*` request needs a credential. For developers that credential is an **API key**. ## Creating a key Create a key from [Account → API](https://cardog.app/login?next=/account/api); the [Quickstart](https://cardog.app/docs/quickstart) walks through it. ## Sending the key Two headers are accepted, and they are equivalent: ```bash # x-api-key header curl "https://api.cardog.app/v2/vin/2T3R1RFV7MW180266" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" # Authorization bearer curl "https://api.cardog.app/v2/vin/2T3R1RFV7MW180266" \ -H "Authorization: Bearer $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` ## Plans Monthly request allowances per plan, rendered from the platform's own plan definitions. See [/pricing](https://cardog.app/pricing) for current prices. | Plan | Credits / month | | --- | --- | | Free | 50 | | Starter | 1,000 | | Pro | 5,000 | | Business | 25,000 | | Enterprise | 100,000 | A VIN decode is 2 credits; a recall check by VIN is 5. See [Credits](https://cardog.app/docs/credits). Metered responses carry `X-Credits-*` headers, and `GET /v2/pricing` returns the machine-readable rate card without auth. ## When auth fails Failures use the standard [error envelope](https://cardog.app/docs/errors): - `401 unauthorized`: no valid key, session, or internal credential - `403 forbidden`: the credential lacks the scope for this route group - `402 insufficient_credits`: free-tier hard stop (paid tiers bill overage instead) - `429 rate_limited`: per-minute rate limit exceeded ## Key hygiene - Never commit keys to version control. Use environment variables. - Use separate keys for development and production. - Rotate keys periodically and revoke unused keys in the dashboard. - Monitor usage under [Account → API](https://cardog.app/login?next=/account/api). --- # The Vehicle Graph Every Cardog endpoint is a view onto one structure: a graph of automotive entities, each with a permanent identifier, each linked to what the platform knows about it. Understand the graph and the API stops being a list of routes — every response tells you where you can go next. ## Nodes: entities and their refs An entity is a node in the graph — a make, a model, a model year, a body style, a fuel type. Every entity has exactly one **ref**: a permanent, human-readable identifier of the form `{domain}:{key}`. ``` make:honda model:honda/civic model-year:honda/civic/2021 fuel-type:electric recall:tc/2024-123 ``` Refs are all-lowercase, with `/` separating the segments of composite keys. They are identifiers, not display strings — `make:mini` is the ref; "MINI" is the display name carried in the entity's `name` field. This distinction is deliberate: display casing varies across data sources, and case-splits are how catalogs silently fracture. The ref never varies. Two domains break the lowercase rule, for a reason: `nano:` and `squish:` keys are uppercase VIN-charset strings, because they are machine-derived from VINs — no human ever types one from memory, and keeping them byte-recognizable against VINs is worth the exception. **Refs are stable join keys.** Store them in your own database columns, your config, your agent's memory. `make:honda` will mean Honda for as long as the platform exists. This permanence is the contract that makes the graph usable as infrastructure. The grammar itself is open source: `npm install @cardog/entities` and your code validates, builds, and types refs with zero API calls — see [The ref grammar](https://cardog.app/docs/ref-grammar) for holding the language offline. ## Edges: hierarchy and links Entities form a hierarchy: a model year belongs to a model, a model to a make. Dereference any entity and you get its ancestors, its children, and counts of what hangs off it: ```bash curl "https://api.cardog.app/v2/entities/model:honda/civic" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` The response carries `parents` (the chain up to `make:honda`), `children` (the model years), `counts` (how many live listings, recall campaigns, and children the graph holds for this node), and `links` — ready URLs into every other group for this entity. Responses are self-describing: follow the links instead of constructing paths. ## Entering the graph There are exactly two doors, by design. **Free text enters through resolve — and nowhere else.** Your user says "2021 Civic", not `model-year:honda/civic/2021`: ```bash curl "https://api.cardog.app/v2/entities/resolve?q=civic&domain=model" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` Resolve returns candidates with confidence scores, best first. `best` is non-null only when a candidate clears the confidence floor — the API never guesses on your behalf. Everywhere else, a parameter that is not a well-formed, known ref is a `400` that names the offending value and points you back to resolve. Never a fuzzy match, never a silently empty result. An unknown ref is a fact worth telling you about, not a thing to paper over. **VINs enter through decode.** A VIN is the physical world's pointer into the graph: ```bash curl "https://api.cardog.app/v2/vin/1HGCM82633A123456" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` The decode returns the vehicle's identity *as refs* — its make, model, model year, fuel type, body style — each one verified against the registry before it is claimed. A null ref in the response means "not derivable for this VIN," never "we guessed." ## The grains: nano and squish Between "one specific vehicle" (a VIN) and "a model year" (millions of vehicles) sit two machine-derived grains, and most market questions live there: - **`squish:`** — the first 9 meaningful VIN characters (WMI + VDS + model year). Every VIN sharing a squish is the same *configuration* of the same model year: same plant-agnostic build. This is the exact-config market grain. - **`nano:`** — the squish plus plant (VIN positions 1–8, 10 and 11), and positions 12–14 when position 3 is `9`, where they complete a small-volume manufacturer's identifier. Vehicles sharing a nano are fungible: same build, same origin. This is the deduplication and comparables grain. Both are pure functions of the VIN — derivable offline, forever, from the VIN alone. A quote at the squish grain answers "what is *this exact car* worth on the live market," not "what do Civics go for." ## What hangs off the graph Every dataset the platform serves is keyed to graph nodes, which is what makes the API composable: | Data | Keyed by | Group | | --- | --- | --- | | Live listings, facets, counts | entity refs + nano | [Listings](https://cardog.app/docs/reference/listings) | | Canonical specs (173 attributes, 17 sections) | model-year ref | [Specs](https://cardog.app/docs/reference/specs) | | Recall campaigns (TC + NHTSA, fused) | recall ref → model-year refs | [Recalls](https://cardog.app/docs/reference/recalls) | | Live quotes and the tape | model-year ref, squish | [Instruments](https://cardog.app/docs/reference/instruments), [Quotes](https://cardog.app/docs/reference/quotes), [Tape](https://cardog.app/docs/reference/tape) | | Safety ratings and complaints | model-year ref | [Safety](https://cardog.app/docs/reference/safety) | One ref, held once, dereferences into all of it. That is the shape of a typical integration: **resolve once (or decode a VIN), hold the refs, query everything else with them.** The refs in your database columns are the same refs in our registry — your data and ours join on identical keys. ## Provenance Facts in the graph carry authority. A recall record is Transport Canada's, not ours — responses carry the issuing authority and data freshness (`asOf`) so what you cite is citable. Where a fact's tier matters (authoritative, commercial, derived, observed), the response says so rather than flattening everything into unattributed data. ## Why a graph, and not a database dump The registry's vocabulary is open — the ref grammar is documented, refs are free to hold, and validating one requires no API call. What the platform meters is *answers*: resolution, enumeration, and the data keyed to the nodes. Browse endpoints are paginated for evaluation, not extraction. The graph is infrastructure you build on, with the vocabulary as the public interface and the facts as the product. --- # Entity resolution Resolution turns something from the world (a phrase a user typed, a VIN off a windshield, a build code) into a node in the [Vehicle Graph](https://cardog.app/docs/vehicle-graph). It either succeeds or it reports how far it got. It never guesses. Most identity bugs in automotive data come from a system that picked one answer when it had three, and every layer downstream inherited the guess without knowing it. This API is built so that cannot happen. **When resolution cannot narrow to one, it says so in a typed field and returns everything that survived.** ## No fuzzy matching at read time Any parameter that is not a well-formed, known ref returns a `400` naming the value. There is no fuzzy match and no empty result for a bad ref. Normalization happens once, in the pipeline, against closed vocabularies. It never happens per request, and it never happens against display text. This is why free text is only accepted at one endpoint. ## Resolve, for text ```bash curl "https://api.cardog.app/v2/entities/resolve?q=2021%20civic&domain=model-year" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` Candidates come back scored, best first. `best` is non-null **only** when a candidate clears the confidence floor. A null `best` with three candidates is a real answer: the input was ambiguous, so ask your user rather than taking element zero. ## Decode, for VINs ```bash curl "https://api.cardog.app/v2/vin/1FTEX1CB0NKD00000" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` The decode returns identity **as refs**, each verified against the registry before it is returned. A null ref means the fact is not derivable from this VIN. It never means a guess was withheld. The decode also returns the **build code**, `nano:`, which is where resolution gets more involved. ## The build code is the resolution key `nano:` (VIN positions 1–8, 10 and 11, plus 12–14 when position 3 is `9`) identifies a configuration: cab, drivetrain, engine, weight class. It is pure string math over the VIN, derivable offline and stable forever, and it is the key the data endpoints answer to: ``` /v2/specs/nano:{code} → the spec sheet for that build /v2/safety/nano:{code} → the crash rating for that configuration /v2/quotes/nano:{code} → the market price for that build /v2/entities/nano:{code} → the build itself, with links to all of the above ``` Resolving to a build code is exact. Resolving from a build code to a third-party dataset, such as NHTSA's rated configurations, is a join, and a join has to report its grain. ## Resolution grain Endpoints that join a build against an external catalogue return **`matchGrain`**: how precisely your vehicle was located in that catalogue. | `matchGrain` | Meaning | What to render | | --- | --- | --- | | `configuration` | Exactly one configuration survived, nothing blocking | Your vehicle's result. Safe to state as fact. | | `partial` | Narrowed, but more than one survived | The set. Do not pick from it. | | `model-year` | Nothing was eliminated | Everything for the model year, labelled as such. | | `exempt` | The programme does not cover this vehicle | The reason, carried in `note`. | | `unrated` | The vehicle exists; the programme never tested it | "Not rated". This is a fact, not a gap. | Alongside it, `candidates` reports how many configurations existed before narrowing, so `13 → 3` and `3 → 3` are distinguishable. The first did some work. The second did none. ### The pin rule ``` pinned = matchGrain === "configuration" && !response.unresolved?.length ``` Both halves are required. A single survivor is not automatically your vehicle. **`unresolved`** names the reasons a unique match still must not be claimed: an axis the catalogue declares that the platform does not yet model (`variant-residue:super-crew`), or a decode too coarse to prove which axle variant was rated (`drive-axle-unresolved`). An unmodeled axis must never be read as satisfied. If `unresolved` is present, treat the result as a shortlist of one. ### Ties are reported, not broken Where a result set carries a headline value, `best` is populated only when the maximum is **unique**. Three configurations tied at five stars produce `best: null`. Breaking that tie would mean picking alphabetically and presenting it as a finding. ## Trim is not an identity The most common integration mistake is inferring resolution from trim text. Do not do this, even where it appears to work. `trim` is display text. It is not a join key, and nothing in the platform joins on it. `"Work Truck"` spans four weight classes, and `"LE"` spans gas and hybrid. Where trim is carried it comes tagged with its authority tier (`derived` from the VIN's own pattern, or `observed` from a seller's listing). Those are different facts about different things, not two sources for one field. The relationship is **many-to-many**: - One catalogue variant is encoded by **many VINs**. A King Ranch SuperCrew is thousands of vehicles. - One build code carries **many variants**. Catalogue granularity runs down to interior and cosmetic packages that share a VIN pattern exactly. Fifteen to thirty per model year is ordinary on a full-size truck. So a build code neither implies one trim nor is implied by one. If a shortlist contains a slug that resembles your `trim` string, that resemblance is a naming coincidence, not a join. When a make names things differently, code built on it fails without warning. Wait for the grain to say `configuration`. ## Worked example: a 2022 F-150 ```bash curl "https://api.cardog.app/v2/safety/nano:1FTEX1CBNK" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` ```jsonc { "matchGrain": "partial", "candidates": 13, "matched": [ { "variant": "super-crew", "overallStars": 5, "frontalDriverStars": 5 }, { "variant": "super-cab", "overallStars": 5, "frontalDriverStars": 4 }, { "variant": "regular-cab", "overallStars": 5, "frontalDriverStars": 4 } ], "best": null } ``` Thirteen rated configurations narrowed to three on drivetrain and fuel family. The three that remain differ by cab, an axis not yet modeled as a join column, so all three survive and the grain says `partial`. `best` is null because all three tie at five stars overall. The correct rendering is all three, labelled as configurations for this model year rather than as this vehicle's rating. The incorrect rendering is `matched[1]`, chosen because `super-cab` resembles the decode's `"SuperCab"`. As modeling deepens, results like this collapse toward `configuration` without any change on your side. That is why you read the grain instead of the array length. ## Integration checklist 1. Resolve or decode once. Store the **refs**, not the display strings. 2. Query everything else by ref. Never reconstruct one by string concatenation. Build it with `@cardog/entities` ([the ref grammar](https://cardog.app/docs/ref-grammar)). 3. Branch on `matchGrain` before rendering. Never on array length. 4. Check `unresolved` before treating a single match as a pin. 5. Never join on `trim`, `styleName`, or any display text. 6. Treat `null` as "not known", never as "not applicable". The platform distinguishes those, and `unrated` versus `exempt` is that distinction made visible. --- # Conventions Every `/v2` group follows the same conventions. Learn them once and the reference pages only have to tell you what an operation returns. ## Request encoding **Repeatable filters repeat the parameter.** No commas and no custom delimiters. A filter that takes many values takes the parameter many times. This example, like every query string on this page, is generated by the contract's own URL codec: ```http GET /v2/listings/search?make=make:tesla&make=make:rivian ``` **Ranges are dotted.** `{field}.min` and `{field}.max`, either side optional: ```http GET /v2/listings/search?year.min=2022&price.min=20000&price.max=60000 ``` **Spec filters address the catalog directly.** `spec.{attributeId}` filters on any canonical spec attribute. Numeric attributes take `.min`/`.max`, and availability attributes take repeated values: ```http GET /v2/listings/search?spec.fuelEconomyCombined.min=35&spec.heatedSeatsFront=standard ``` Unknown spec keys return `400 unknown_spec_attributes` naming the bad keys, the same way unknown refs do. ## Refs in paths vs queries A ref's slashes are part of the key (`model-year:honda/cr-v/2026`), so position matters: - **In a URL path**, encode slashes as `%2F`: `https://api.cardog.app/v2/entities/model-year:honda%2Fcr-v%2F2026` - **In a query string**, pass the ref raw: `?make=make:tesla` Every `links` block the API emits is already encoded correctly, which is one more reason to follow links instead of assembling paths. A ref is `domain:slug`: `make:tesla`, `model:mini/hardtop`, `model-year:honda/cr-v/2026`. In a URL path, encode the slashes as `%2F`; in a query string, pass the ref raw. Ref filters repeat the parameter (`?make=make:tesla&make=make:rivian`). Browse domains and members with `GET /v2/entities`, and look up any ref with `GET /v2/entities/{ref}`. The full grammar, and the npm package that validates it offline, is in [Ref grammar](https://cardog.app/docs/ref-grammar). ## Query with refs Refs work across every group: listings, quotes, specs, recalls. Not every parameter is a ref, though, and the distinction matters: - **Ref parameters** name a node in the graph: `make`, `model`, and the path segment on quotes, specs, and recalls. They take a full `{domain}:{key}` string and reject anything else with a `400` that names the bad value. - **Scalar parameters** are ordinary filters: `year.min`, `year.max`, `price.max`, `limit`. They take numbers. On listings, a make is a ref and a year is a range, so `make=make:tesla` and `year.min=2022` in the same query is normal. Listings filters accept **make and model refs only**. There is no `model-year=` filter, because a model year on this surface is a year range. To query a single model year as a node, use the groups that take one in the path (quotes, specs, recalls, safety), as below. ```bash # Listings: make is a ref, year is a range curl "https://api.cardog.app/v2/listings/search?make=make:tesla&year.min=2022&limit=5" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" # Live market quote for an instrument curl "https://api.cardog.app/v2/quotes/model-year:honda%2Fcr-v%2F2026" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" # Recalls scoped to an entity. make:, model:, and model-year: all work curl "https://api.cardog.app/v2/recalls/entity/model-year:honda%2Fcr-v%2F2026" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` ```typescript // Listings: ref-native filters const page = await client.v2.listings.search({ filters: { makes: ["make:tesla"], year: { min: 2022 } }, pagination: { limit: 5 }, }); // Live market quote for an instrument const quote = await client.v2.quotes.getByRef("model-year:honda/cr-v/2026"); // Recalls scoped to an entity const recalls = await client.v2.recalls.entity("make:toyota"); ``` ```python # Listings: ref-native filters page = client.v2.listings.search( filters={"makes": ["make:tesla"], "year": {"min": 2022}}, limit=5 ) # Live market quote for an instrument quote = client.v2.quotes.get_by_ref("model-year:honda/cr-v/2026") # Recalls scoped to an entity recalls = client.v2.recalls.entity("make:toyota") ``` ## Response conventions - **camelCase keys** everywhere. No snake_case in any response. - **Dates are ISO 8601 strings** (`2026-07-23T12:00:00Z`, or `YYYY-MM-DD` for date-grain fields like tape bars). - **Pagination is an object**, not headers. Paged responses carry a `pagination` block with `page`, `limit`. Page size is capped per endpoint, and the cap is in the reference. - **Every response carries `links`**, a map from relation name to a server-relative path for related resources (an entity's quotes, recalls, listings, specs). Follow them instead of constructing URLs. - **`null` means not derivable.** A null `fuelType` on a VIN decode means the registry cannot support that claim for this VIN. It is a statement about coverage, and the API never substitutes a guess where it cannot verify. - **A failed resolution returns nothing.** When `GET /v2/vin/{vin}` answers `"valid": false`, every identity field in that response is `null` and every ref is `null`, including the `nano` and `squish` grains. We do not fill the gap from adjacent records, because a plausible-looking join key is worse than an empty one: you would store it. - **Derived and observed never share a field.** A fact we resolved ourselves and a fact we saw on a listing are different facts, and the payload keeps them apart. A VIN identity's `trim` is the catalogue trim the VIN's own pattern resolves to, tagged by `trimAuthorityTier`. The words a seller wrote on a listing ride in `observedTrim` with their own `authorityTier` and `asOf`. Price on the first and display the second. ## Versioning and stability Within a version, the v2 contract changes **additively**: new fields, new endpoints, new enum members, new error codes. Nothing a version has shipped is renamed, removed, or changed in type. A change like that ships as a new dated version, and the one you pinned keeps answering (see [Versioning](https://cardog.app/docs/versioning)). - **Open sets stay open.** Error codes (16 well-known today), authority keys, and bar sources can gain members. Dispatch on the members you know and fall back gracefully. New members are not a breaking change. - **Refs are permanent.** The ref grammar is versioned (`GRAMMAR_VERSION = 1`, exported by the registry itself), and a ref, once issued, keeps its meaning for the life of the platform. - **v1 is legacy.** It keeps working for existing integrations, but new work should target `/v2` only. The version switcher in these docs marks v1 accordingly. The machine-readable contract is `GET https://api.cardog.app/v2/openapi.json`. These docs, including this page, are generated from it and from the same source modules the server imports. ## Batch semantics Batch endpoints accept the whole batch and resolve each item on its own. One malformed item fails its own row with the standard [error envelope](https://cardog.app/docs/errors), in the position it was sent, and never the whole batch: ```bash curl -X POST "https://api.cardog.app/v2/vin/batch" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" \ -H "content-type: application/json" \ -d '{"vins":["2T3R1RFV7MW180266","NOTAVIN"]}' ``` ```json { "results": [ { "ok": true, "vin": "2T3R1RFV7MW180266", "identity": { "…": "…" } }, { "ok": false, "vin": "NOTAVIN", "error": { "code": "invalid_vin", "…": "…" } } ], "meta": { "requested": 2, "decoded": 1, "failed": 1 } } ``` `results` preserves request order and `meta` carries the counts. A batch takes up to 1,000 VINs and is metered per VIN decoded. Failed rows cost nothing. See [Credits](https://cardog.app/docs/credits). --- # Credits Every metered request on the platform is priced in **credits**, across every route group, REST and MCP alike. A request's price depends on what it answers, not how many bytes it returns. Registry reads are cheap. Identity and market answers cost more. > **The posted card** below is version 13, as of > 2026-10-27, in USD. It is what you > are charged today. `GET https://api.cardog.app/v2/pricing` is the authoritative copy. > This page renders from the same module that endpoint serves, and > `version` only increases, so pin it if you price off our prices. ## The rate card | Family | Unit | Credits | What it covers | | --- | --- | --- | --- | | `vin` | per vin | 2 | Identity decode; batch = N units (one per VIN decoded). | | `entities` | per request | 1 | Registry browse/resolve/dereference. | | `specs` | per request | 1 | Catalog + spec sheets. | | `listings` | per request | 1 | Search, count, facets, detail. | | `instruments` | per request | 5 | Symbology + instrument cards. | | `quotes` | per request | 5 | The live book (multi-quote = one request). | | `tape` | per request | 5 | Prints + daily bars. | | `recalls` | per vin | 5 | The authoritative per-VIN recall check. Transport Canada + NHTSA fused. | | `recalls` | per request | 1 | Recall entity/feed/stats reads (non-VIN). | | `safety` | per request | 1 | NCAP ratings + ODI complaints, keyed by model-year ref. | | `charging` | per request | 1 | EV charging station search + detail (Open Charge Map data). | | `parts` | per request | 3 | OEM part numbers for a model year or build code, linked to the manufacturer's official store. | | `fuel` | per request | 1 | Fuel stations with current pump prices, and daily area prices (Canada). | | `locations` | per request | 1 | Places near a point (fuel stations, chargers, dealers), and one place in full. | | `feedback` | per request | 0 | Report a wrong or missing record, and read it back. Free on every plan; rate limited per key. | | `documents` | per vin | 5 | The factory window sticker for a VIN. Billed only when a label is found; an `unavailable` answer costs 0. | | `documents` | per check | 0 | HEAD existence check for a VIN's window sticker. Free on every plan. | | `documents` | per download | 0 | The window sticker PDF, through the signed 24-hour link a window-sticker read returns (GET or HEAD). No credential, free on every plan. | | `feeds` | per request | 0 | An organization's feed runs: the contract list and the current run's manifest with signed links. Org key only. Free: the paper the data travels under already paid. | | `feeds` | per download | 0 | A feed artifact, through the signed 24-hour link the latest read returns. No credential, free on every plan. | | `history` | per vin | 3 | One kind of event for a VIN (the carriers it ran under today; crashes, inspections and more to follow), every item with its source. Billed only when there is an item; an empty answer costs 0. | | `carriers` | per request | 5 | A carrier by DOT number (declared and observed), or one page of the units observed under it. | | `carriers` | per search | 1 | Find a carrier by name, DOT number or VIN. | | `agent` | per session | 0 | Open a session for the Cardog agent. Free on every plan: a turn is the charged unit, and tool reads inside a turn are never billed. | | `agent` | per thread | 0 | Read the agent's thread history (the list, or one thread's messages). Free on every plan. | Rates are per unit, and most units are one request. The exceptions meter per VIN. Batch decode charges one `vin` unit per VIN decoded, and a failed row costs nothing. The recalls VIN check is priced per VIN checked. ## Plans | Plan | Price | Credits / month | Past the allowance | | --- | --- | --- | --- | | Free | $0/mo | 50 | Pack credits, then 402 | | Starter | $30/mo | 1,000 | Billed overage ($0.05/credit) | | Pro | $100/mo | 5,000 | Billed overage ($0.04/credit) | | Business | $500/mo | 25,000 | Billed overage ($0.03/credit) | | Enterprise | Custom | Custom | Billed overage (contract rate) | **Free is self-serve and needs no credit card** — 50 credits a month at $0, enough to evaluate every group before you talk to anyone. Two rules to plan around: - **Paid tiers are never blocked.** Past the allowance, requests keep working and the excess is billed as overage. A production integration does not stop because a month ran long. - **The free tier stops when it runs out.** Past the allowance, requests draw on any credit packs you have bought. When the allowance and every pack are spent, metered requests return `402 insufficient_credits` until the month resets, you buy a pack, or you upgrade. ## Credit packs A pack is a one-time purchase of prepaid credits, on any plan, including free. Credits are spent in this order: the monthly allowance first, then packs (oldest first), then overage on paid plans. A pack expires 12 months after purchase and is never billed again. On the free plan, a pack is pay-as-you-go: 50 free credits each month, then the pack, with no subscription. ## The budget headers Every metered response carries the current state of your budget: - `X-Credits-Rate` - `X-Credits-Remaining` - `X-Credits-Allowance` - `X-Credits-Reset` - `X-Credits-Source` `X-Credits-Rate` is what this response cost. It is 0 on non-2xx responses, because errors are free. One exception: `304 Not Modified` on a conditional request **is** debited at the route's rate. "Unchanged as of now" is a served answer, priced the same as the `200` it stands in for. `X-Credits-Remaining` counts down against the allowance, and `-1` in Allowance or Remaining means a custom plan that is not capped here. `X-Credits-Reset` is when the month rolls over. ### A worked sequence A fresh free-tier key has a 50-credit allowance. First, a VIN decode at 2 credits: ```bash curl -i "https://api.cardog.app/v2/vin/2T3R1RFV7MW180266" -H "x-api-key: $CARDOG_API_KEY" -H "Cardog-Version: 2026-10-26" ``` ```http X-Credits-Rate: 2 X-Credits-Allowance: 50 X-Credits-Remaining: 48 X-Credits-Reset: 2026-08-01T00:00:00.000Z ``` Then a listings search (1 credit) and a market quote (5 credits): ```http X-Credits-Rate: 1 X-Credits-Allowance: 50 X-Credits-Remaining: 47 X-Credits-Reset: 2026-08-01T00:00:00.000Z ``` ```http X-Credits-Rate: 5 X-Credits-Allowance: 50 X-Credits-Remaining: 42 X-Credits-Reset: 2026-08-01T00:00:00.000Z ``` Read the headers as you go and you can budget mid-task. "Spend at most 20 credits answering this" needs no extra call. ## The free-tier stop Once the allowance and every pack are spent, the free tier answers `402` with the standard [error envelope](https://cardog.app/docs/errors) and the budget headers, so the refusal also tells you when the month resets: ```json { "code": "insufficient_credits", "message": "Monthly credit allowance (50) exhausted. Resets 2026-08-01T00:00:00.000Z.", "hint": "Buy a credit pack (any tier — POST /account/credit-packs/checkout) or upgrade at https://cardog.app/pricing — paid tiers convert the stop into billed overage. Rates, allowances, and packs: GET /v2/pricing", "docs_url": "https://cardog.app/docs/errors/insufficient_credits.md" } ``` One thing to plan for: your remaining balance is cached for up to a minute, so a fast burst can cross the last credit before the stop catches it. Those requests are charged. The stop then lands on the next request. In practice the overshoot is one request, and never more than a minute's worth. ## Reading the card by machine `GET https://api.cardog.app/v2/pricing` returns this entire card (rates, plans, header names) as JSON, with no auth required. If you are an agent, or writing one, read it at integration time and again before long tasks. Never hardcode a rate. The card's `version` only increases, so you can detect change. Both SDKs keep the budget headers of the last metered response parsed on the client, errors included, so a task can budget without reading raw headers: ```typescript await client.v2.vin.getByVin("2T3R1RFV7MW180266"); const { rate, remaining, allowance, reset, source } = client.lastCredits!; ``` ```python client.v2.vin.get_by_vin("2T3R1RFV7MW180266") credits = client.last_credits # rate, remaining, allowance, reset, source ``` `remaining` is allowance headroom plus pack balance, `-1` on a custom plan, and `source` names the pool a debit came from (`allowance`, `pack` or `overage`), null when nothing was debited. It is null until the first metered response, and an unmetered response (the rate card, a signed link) leaves the last set alone. --- # Errors Every non-2xx response from a `/v2/*` route has the same shape, the error envelope. `code` is a value you can dispatch on, `hint` says what to do next, and `suggestions` carries nearest-ref candidates so a mistyped ref can be corrected in one step. ```json { "code": "unknown_entity_refs", "message": "Unknown entity refs: make:teslla", "hint": "Resolve free text to refs at GET /v2/entities/resolve?q=teslla", "refs": ["make:teslla"], "suggestions": [ { "invalid": "make:teslla", "nearest": [{ "ref": "make:tesla", "name": "Tesla" }], "resolve": "/v2/entities/resolve?domain=make&q=teslla" } ] } ``` ## Unknown refs are named, not guessed A well-formed ref that the registry does not recognize always returns a 400 that names it. Suggestions are advisory and are never applied for you. You will not get a fuzzy-matched result or an empty set for a bad ref. ## The envelope | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | string | yes | Error code to branch on. Well-known codes: invalid_request, invalid_filters, invalid_ref, invalid_vin, invalid_nano_refs, version_sunset, unknown_entity_refs, unknown_entity_domain, unknown_spec_attributes, not_found, unauthorized, forbidden, rate_limited, insufficient_credits, internal_error, unavailable. New codes may be added; fall back to message and hint for codes you do not know. | | `message` | string | yes | Readable description naming the offending input | | `hint` | string | no | What to do next, e.g. "Resolve free text at GET /v2/entities/resolve" | | `docs_url` | string | no | URL of a Markdown page explaining this error code and how to fix it | | `details` | object[] | no | Field-level validation failures, one per failing path | | `details[].path` | string | no | The failing field, e.g. "filters.makes.0" | | `details[].message` | string | yes | | | `refs` | string[] | no | The offending refs or attribute ids, verbatim. Set for unknown_entity_refs, unknown_spec_attributes, and invalid_nano_refs. | | `suggestions` | object[] | no | Nearest-ref candidates per offending input. Advisory only; the API does not apply them. | | `suggestions[].invalid` | string | yes | The offending input, verbatim | | `suggestions[].nearest` | object[] | yes | Best candidates, most likely first. Advisory only; the API does not apply them. | | `suggestions[].nearest[].ref` | string | yes | A known entity ref, e.g. "make:tesla" | | `suggestions[].nearest[].name` | string | no | Display name, e.g. "Tesla" | | `suggestions[].resolve` | string | no | Server-relative URL of the resolve call that turns this input into refs, e.g. "/v2/entities/resolve?domain=make&q=teslla" | | `links` | object | no | Related resources, each a server-relative path. invalid_vin carries {"feedback": "/v2/feedback"}: if you know what the vehicle is, report it there. | | `requestId` | string | no | This request's id, the same value as the X-Request-Id response header. Quote it when you report a failure. Set on every error the API serves. | The code set is **open**. New codes are added over time and are never a breaking change. Dispatch on the codes you know and fall back on `message` and `hint`. ## Error codes | Code | Meaning | | --- | --- | | [`invalid_request`](https://cardog.app/docs/errors/invalid_request) | The request body or parameters did not match the operation's schema. | | [`invalid_filters`](https://cardog.app/docs/errors/invalid_filters) | One or more filters used invalid ref syntax, scalar values, or ranges. | | [`invalid_ref`](https://cardog.app/docs/errors/invalid_ref) | A path or query parameter did not match the `{domain}:{key}` ref grammar. | | [`invalid_vin`](https://cardog.app/docs/errors/invalid_vin) | The supplied VIN failed the platform's length or character rules. | | [`invalid_nano_refs`](https://cardog.app/docs/errors/invalid_nano_refs) | A nano filter did not match the VIN-derived nano key grammar. | | [`version_sunset`](https://cardog.app/docs/errors/version_sunset) | The `Cardog-Version` header named a dated version that is past its support window. | | [`unknown_entity_refs`](https://cardog.app/docs/errors/unknown_entity_refs) | The refs were well formed, but the entity registry did not recognize them. | | [`unknown_entity_domain`](https://cardog.app/docs/errors/unknown_entity_domain) | The domain was well formed, but the entity registry does not serve it. | | [`unknown_spec_attributes`](https://cardog.app/docs/errors/unknown_spec_attributes) | A `spec.*` filter named an attribute that is not in the current catalog. | | [`not_found`](https://cardog.app/docs/errors/not_found) | The requested route or resource does not exist. | | [`unauthorized`](https://cardog.app/docs/errors/unauthorized) | The request did not carry a valid API key, session, or internal credential. | | [`forbidden`](https://cardog.app/docs/errors/forbidden) | The credential is valid but does not grant access to the requested route group. | | [`rate_limited`](https://cardog.app/docs/errors/rate_limited) | The credential exhausted the request limit described by the response. | | [`insufficient_credits`](https://cardog.app/docs/errors/insufficient_credits) | A hard-stop plan has no allowance or prepaid pack balance left. | | [`internal_error`](https://cardog.app/docs/errors/internal_error) | The server failed while processing a request that the caller cannot repair. | | [`unavailable`](https://cardog.app/docs/errors/unavailable) | A required platform dependency was temporarily unavailable. | Open a code for what happened, the next action, and a working corrected request. Error envelopes link to the page's markdown twin so an agent can fetch the recovery recipe without parsing HTML. ## HTTP statuses | Status | When | | --- | --- | | 400 | Invalid request — grammar failure or unknown-but-well-formed refs, NAMED in `refs`/`details`, with nearest-ref `suggestions` where computable. Never a fuzzy fallback. | | 401 | No valid credential (API key, session, or internal token) | | 402 | Insufficient credits (free-tier hard stop; paid tiers bill overage instead) | | 403 | The link is invalid or expired (`forbidden`); request `GET /v2/vin/{vin}/window-sticker` again for a fresh one | | 404 | Resource not found | | 429 | Per-minute rate limit exceeded | | 500 | Internal error | | 503 | Temporarily unavailable (`unavailable`); retry with backoff | --- # Versioning Your integration keeps answering the way it did when you built it. A request names the contract version it expects, and the API returns that version's shape, even after a newer version changes it. The API serves 2 dated versions: `2026-10-21`, `2026-10-26`. The latest is `2026-10-26`. ## The header A version is a date, `YYYY-MM-DD`. It travels in one request header and nowhere else: ```bash curl "https://api.cardog.app/v2/vin/2T3R1RFV7MW180266" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` - **No header** returns the oldest version still served, so an integration that never sends the header is never broken by a new version. It moves forward only when that version reaches its sunset. Send the header to get a newer shape. - **A served date** returns that date's shape. - **Your key never selects a version.** Rotating a key never changes an answer, and two services on one key can each ask for their own version. Every `/v2` response says what it served: | header | means | | --- | --- | | `Cardog-Version` | the version this response was shaped to | | `Cardog-Latest-Version` | the newest version the API serves | When the two differ, a newer version exists. The [changelog](https://cardog.app/changelog) says what changed and why. ## The SDKs pin for you `@cardog/api` and the `cardog` Python package send `Cardog-Version` on every `/v2` request. The value is `PINNED_API_VERSION`, the version the SDK release was built against. Upgrading the SDK is how you move to a newer version, and the response types match the version it pins. ```ts import { CardogClient, PINNED_API_VERSION } from "@cardog/api"; const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY }); await client.v2.vin.getByVin("2T3R1RFV7MW180266"); client.lastVersion; // { requested, latest } from the response headers ``` To speak another version on purpose, while you migrate, set `apiVersion` (`api_version=` in Python). The types then describe the pinned version only, so validate what you read. ## How long a version is served A version is served for **12 months after the next version lands**. A response pinned to a version that has been superseded says so in two more headers: | header | means | | --- | --- | | `Deprecation` | when the newer version landed, as `@` and Unix seconds | | `Sunset` | the first day this version is no longer served | with a `Link` header pointing at the migration guide for the newer version, `https://cardog.app/docs/migrations/`. ## Errors | status | code | when | | --- | --- | --- | | `400` | [`invalid_request`](https://cardog.app/docs/errors/invalid_request) | the header is not a date, or names a version the API does not serve. `hint` lists the served versions | | `409` | [`version_sunset`](https://cardog.app/docs/errors/version_sunset) | the version is past its window. `hint` names a served version and links the migration guide | A version request is checked before anything is metered, so neither error costs credits. ## What counts as a new version A new version is cut only for a change that would break a working integration: a field removed, renamed or retyped, or a value that changes meaning. Adding a field, an operation or an error code is not a new version, so dispatch on the codes you know and ignore fields you do not (see [Conventions](https://cardog.app/docs/conventions)). ## v1 v1 is not dated. It is deprecated as of 2026-10-09, stops answering on 2027-12-31, and says so on every response in the same `Deprecation`, `Sunset` and `Link` headers. See the [v1 overview](https://cardog.app/docs/v1). --- # Units A bare number is ambiguous. `0.095` for a rollover probability could mean 9.5% or 0.095%, and nothing in the response would tell you which. Every measure in the API therefore ships as a pair: ```jsonc "possibility": { "value": 9.5, "unitCode": "P1" } ``` ## The codes Unit codes come from **UN/CEFACT Recommendation 20**, the same list [schema.org](https://schema.org/unitCode) uses for `unitCode`. If you already handle `unitCode` from structured data, these are the same codes. | code | means | where you'll see it | | --- | --- | --- | | `P1` | percent | rollover possibility | | `C62` | one (a pure ratio) | static stability factor | | `KMT` | kilometre | odometer, distances | | `SMI` | mile (statute) | odometer, US sources | | `LTR` | litre | engine displacement, fuel capacity | | `CMQ` | cubic centimetre | engine displacement | | `LBR` | pound | weights | | `INH` | inch | dimensions | ## Why a ratio carries a code `C62` is Recommendation 20's code for a dimensionless quantity. The static stability factor is track width divided by centre-of-gravity height, so it has no unit, and `C62` says so explicitly. That is different from a missing unit. `{ value: 1.48, unitCode: "C62" }` renders as `1.48`. A measure with a `value` and no `unitCode` is a bug. Please report it. ## How to read them **Switch on the code. Do not infer the unit from the field name.** The same field can arrive in different units from different sources. An odometer is `KMT` from a Canadian record and `SMI` from a US one, and the field is called `odometer` either way. **Do not parse the code.** It is an identifier from a fixed list, not a display symbol. Map it to whatever your interface shows. **A measure always has both halves.** If `value` is present, `unitCode` is present. Our emit gate rejects a measure with an empty unit, so you will not see a half-populated one. A measure that does not apply is `null` as a whole. --- # Report a wrong or missing record When a decode returns `valid: false` and you know what the vehicle is, or returns a vehicle you can show is wrong, send it to `POST /v2/feedback`. You get a receipt now and can check on it later. Reporting costs 0 credits on every plan. ### File a report ```bash curl -X POST "https://api.cardog.app/v2/feedback" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" \ -H "content-type: application/json" \ -d '{ "kind": "decode_miss", "subject": { "vin": "1HGCM82633A123456" }, "claim": { "year": 2003, "make": "Honda", "model": "Accord" }, "evidence": [{ "type": "url", "value": "https://example.com/window-sticker.pdf" }] }' ``` The response is `202`: ```json { "id": "fb_01J8Z3K5V6W7X8Y9Z0A1B2C3D4", "kind": "decode_miss", "subject": { "vin": "1HGCM82633A123456" }, "status": "received", "received_at": "2026-09-12T14:03:11.000Z", "resolution": null, "links": { "self": "/v2/feedback/fb_01J8Z3K5V6W7X8Y9Z0A1B2C3D4", "subject": "/v2/vin/1HGCM82633A123456" } } ``` Keep the `id`. Sending the same report again within 24 hours returns this same receipt with `200` and stores nothing new, so a retry is safe. ### Kinds | kind | subject | also required | | --- | --- | --- | | `decode_miss` | `vin` | | | `decode_dispute` | `vin` | | | `recall_dispute` | `vin`, or a `recall:` ref | | | `spec_dispute` | any | `claim.field` | | `listing_report` | any | | | `resolve_miss` | any | `note`: the text that did not resolve | | `other` | any | | `subject` takes exactly one of `vin`, `ref` or `listing_id`. `claim` holds what you believe is true: `year`, `make`, `model`, `trim`, or a `field` with `expected` and `observed`. `evidence` takes up to 5 links or quoted excerpts, `note` up to 2,000 characters, and `request_id` names the response you are disputing when there is one. The whole body is capped at 8 KB. Today only `decode_miss` and `decode_dispute` are acted on. Every other kind is stored, and its `resolution.outcome` reads `queued` until it is routed. ### Check on it ```bash curl "https://api.cardog.app/v2/feedback/fb_01J8Z3K5V6W7X8Y9Z0A1B2C3D4" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` Only the key that filed a report can read it. Any other id returns `404`. | status | meaning | | --- | --- | | `received` | Stored, not yet looked at. | | `triaged` | Someone has looked at it. | | `accepted` or `rejected` | The verdict, with `resolution.note` when there is one. | | `shipped` | The fix is live. `resolution.fixed_in` names the version. | ### What a report does not do A report never changes a record by itself. It is evidence we check, and a decode changes only when a fix ships for every VIN it applies to. ### Limits Each key can file 60 reports a minute and 1,000 a day. Past either you get `429 rate_limited` with a `Retry-After` header. Every `invalid_vin` error and every `valid: false` decode carries `links.feedback`, so an agent can find this endpoint from the response it is already holding. --- # Decode a Canadian VIN ```bash curl "https://api.cardog.app/v2/vin/2T3R1RFV7MW180266" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` One VIN in, one identity card out: display names to render, 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. This is a separate product from the free American decoder because of coverage. NHTSA's vPIC is built from US regulatory submissions, so a Canadian-market vehicle often 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. Where a pattern is not 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: ```json { "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" } } ``` > **Store the refs.** 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 the fact is not derivable for this VIN. ## What comes back ![Exploded line drawing of a generic SUV: the body lifted above the engine, the transmission and the chassis with its axles and wheels.](https://cardog.app/images/docs/decode-exploded.png) - **`valid`** is the decoder's verdict. `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`** are display names. Render these and join on the refs. `trim` is the catalogue trim this VIN's own pattern resolves to. A seller's listing text ships separately in `observedTrim`, tagged and dated. - **`refs`** are the join keys, one per domain. Every other endpoint takes them. They follow the [ref grammar](https://cardog.app/docs/ref-grammar). - **`nano` and `squish`** are the two VIN grains, covered below. - **`links`** are server-relative paths to this VIN's related 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 summarizes it into a `specHighlights` block. The complete response shape is in the [VIN reference](https://cardog.app/docs/reference/vin). ## `null` means not derivable A null in any identity field is a statement about the VIN, not a gap in our lookup. It means this VIN's pattern does not derive that fact. It is not a withheld fuzzy match, 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 safe 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 "Cardog-Version: 2026-10-26" \ -H "content-type: application/json" \ -d '{"vins":["2T3R1RFV7MW180266","1HGCM82633A123456"]}' ``` The batch is accepted as a whole and each item resolves on its own. 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 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 requires. The boundary is the border. A trim configuration sold only in Canada has no US submission behind it, so the pattern is absent and 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 rather than a dataset to download. Where a Canadian pattern exists in a public registry we use it. Where it does not, we derive and carry our own pattern extensions. That is what brings 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 arithmetic half of decoding, see [VIN structure](https://cardog.app/docs/vin). ## The grains: `squish` and `nano` Two keys in the identity card are pure projections of the VIN itself: - **`squish`** is WMI + VDS + year, with the plant left out. It is 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](https://cardog.app/docs/market-data). - **`nano`** is the squish plus the plant code (VIN positions 1–8, 10 and 11), plus positions 12–14 when position 3 is `9`, because for a small-volume manufacturer those complete the manufacturer identifier. It is the build grain, and the dedup and comparables key. Because both are projections, you can derive them offline from any VIN with [`@cardog/entities`](https://www.npmjs.com/package/@cardog/entities), with no API calls and no network: ```typescript import { squishFromVin, nanoFromVin } from "@cardog/entities"; const vin = "2T3R1RFV7MW180266"; squishFromVin(vin); // "2T3R1RFVM" : the market grain (WMI + VDS + year) nanoFromVin(vin); // "2T3R1RFV*MW": the build grain (+ plant, check digit masked) ``` The key you compute offline is the key the API returns. The identity card ships it in ref form (`squish:2T3R1RFVM`, `nano:2T3R1RFVMW`: the ref leaves out the masked check digit), 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 that maps a relation name to a server-relative path, so related resources are one request 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 [Recalls by VIN](https://cardog.app/docs/recalls). - `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: ```json { "code": "invalid_vin", "message": "Not a valid VIN: 2T3R1RFV7MW18026", "hint": "A VIN is 17 characters, A–Z (no I/O/Q) and 0–9. If the vehicle is real and we have it wrong, report it at POST /v2/feedback", "docs_url": "https://cardog.app/docs/errors/invalid_vin.md", "links": { "feedback": "/v2/feedback" } } ``` `code` is a value you can dispatch on, `hint` says what to do next, and `docs_url` is fetchable markdown with the full correction recipe, so there is no HTML to parse. `links.feedback` is where to [report a VIN we have wrong](https://cardog.app/docs/feedback), which costs nothing. 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](https://cardog.app/docs/errors). --- - **Everything in one fetch:** https://cardog.app/docs.md - **The contract:** https://api.cardog.app/v2/openapi.json - **Get a key:** https://cardog.app/login?next=/account/api - **MCP:** `claude mcp add --transport http cardog "https://mcp.cardog.io/mcp?api_key=$CARDOG_API_KEY"` --- # VIN structure ![Close-up line drawing of a riveted VIN plate on the dashboard, seen through the lower corner of the windshield; its seventeen characters are blank cells.](https://cardog.app/images/docs/vin-plate.png) A Vehicle Identification Number is 17 characters that identify one vehicle, for its whole life. ISO 3779 sets its content and structure and ISO 3780 how manufacturer codes are assigned. In North America, 49 CFR Part 565 fixes what each position means, and Canada's CMVSS 115 follows the same layout, so a VIN on a Canadian vehicle reads exactly like the one below. ## The 17 positions `2T3R1RFV7MW180266` is a 2021 RAV4 built in Canada. Read left to right, it narrows from the manufacturer to the model to the one vehicle. | Positions | Section | In `2T3R1RFV7MW180266` | | --- | --- | --- | | 1–3 | World Manufacturer Identifier (WMI): region and manufacturer | `2T3` | | 4–8 | Vehicle Descriptor Section (VDS): model, body, restraints, engine, in the manufacturer's code | `R1RFV` | | 9 | Check digit, computed from the other sixteen | `7` | | 10 | Model year | `M` (2021) | | 11 | Assembly plant | `W` | | 12–17 | Serial: the production sequence number | `180266` | Positions 10 to 17 together are the Vehicle Identifier Section (VIS). > The letters I, O and Q never appear in a VIN, in any position, so they can > never be misread as 1 and 0. A "VIN" containing one is a typo or not a VIN. ## The manufacturer: positions 1 to 3 The WMI is assigned to a manufacturer under ISO 3780; in the United States SAE International makes the assignments under contract to NHTSA. The three characters together identify the manufacturer and the type of vehicle, and the first is a region block. First characters you will see on Canadian roads: | First character | Assigned to | | --- | --- | | 1, 4, 5 | United States | | 2 | Canada | | 3 | Mexico (most of the block) | | J | Japan | | K | South Korea | | L | China | | S | United Kingdom (SA to SM) | | V | France (VF to VR) | | W | Germany | The region is where the WMI was assigned, which usually means where the vehicle was built, not where the brand is from: a Japanese brand's car built in Ontario starts with 2. ### Small manufacturers: the extended WMI A manufacturer building fewer than 1,000 vehicles of a type a year (a low-volume manufacturer in 49 CFR 565) does not get a three-character WMI of its own. It gets a six-character identifier: a WMI whose third character is `9`, finished by positions 12 to 14. A `9` in position 3 always means this form. The serial is then only positions 15 to 17. Trailer, RV and specialty builders are the usual carriers of this form. `2A9TR1DA1PA123045` (constructed to pass its check digit; no real vehicle): positions 1–3 `2A9` and 12–14 `123` together identify the manufacturer; 4–8 `TR1DA` are its VDS, 9 `1` the check digit, 10 `P` the model year (2023), 11 `A` the plant, 15–17 `045` the serial. A decoder that reads only positions 1 to 3 merges every small manufacturer that shares a `9` WMI into one. Cardog's identity for these VINs carries the extra three characters, covered [below](#how-cardog-keys-on-a-vin). ## The vehicle: positions 4 to 8 The VDS is the manufacturer's own code. Position 4 is often the line or platform, 5 the restraint system, 6 and 7 the body and series, 8 the engine, but that is a convention, not a rule: each manufacturer defines its own key and, in the United States, files it with NHTSA at least 60 days before the first VIN goes on a vehicle. There is no shared table, which is why a decode is a lookup against a pattern set rather than arithmetic. On current cars, MPVs and light trucks, position 7 must be a letter (the model-year section below says why). ## The check digit: position 9 The check digit catches a mistyped or invented VIN. Each character is turned into a number, multiplied by its position's weight, and the products summed; the remainder after dividing by 11 is the digit, written `X` when it is 10. Position 9 weighs 0, so the digit never checks itself. Paste any VIN to see its arithmetic. 1. **Transliterate letters** to numbers; digits keep their value: ```text A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8 J=1 K=2 L=3 M=4 N=5 P=7 R=9 S=2 T=3 U=4 V=5 W=6 X=7 Y=8 Z=9 ``` 2. **Multiply by the weight** of each position: ```text Position: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Weight: 8 7 6 5 4 3 2 10 0 9 8 7 6 5 4 3 2 ``` 3. **Sum and divide by 11.** For `2T3R1RFV7MW180266` the products sum to 370; 370 ÷ 11 is 33 remainder 7, and position 9 reads 7, so the VIN passes. Cardog records whether a VIN's digit passes but never refuses a decode on it: not every market enforces the digit, and a failing digit is a fact about the VIN worth keeping, not a reason to drop it. ## The model year: position 10 Thirty codes cover thirty model years, then repeat. The letters skip I, O and Q (never used anywhere), U and Z, and the digit 0. ```text Code Years Code Years Code Years A 1980 2010 L 1990 2020 Y 2000 2030 B 1981 2011 M 1991 2021 1 2001 2031 C 1982 2012 N 1992 2022 2 2002 2032 D 1983 2013 P 1993 2023 3 2003 2033 E 1984 2014 R 1994 2024 4 2004 2034 F 1985 2015 S 1995 2025 5 2005 2035 G 1986 2016 T 1996 2026 6 2006 2036 H 1987 2017 V 1997 2027 7 2007 2037 J 1988 2018 W 1998 2028 8 2008 2038 K 1989 2019 X 1999 2029 9 2009 2039 ``` For cars, MPVs and trucks up to 4,536 kg (10,000 lb) GVWR, position 7 settles the cycle under 49 CFR 565: a digit there means 1980 to 2009, a letter means 2010 to 2039. In `2T3R1RFV7MW180266` position 7 is `F`, so `M` is 2021. It is the manufacturer's model year, which need not match the calendar year the vehicle was built in. ## Where the VIN lives on a vehicle ![Line drawing of a sedan with the driver's door open: a plate on the dashboard at the lower driver-side corner of the windshield, and a label on the B-pillar.](https://cardog.app/images/docs/vin-locations.png) - **The dashboard**, at the lower driver-side corner of the windshield. On cars, MPVs and light trucks, 49 CFR 565 requires the VIN inside the cabin and readable through the glass by someone standing outside next to the driver-side windshield pillar. - **The driver's door jamb or B-pillar**, on the compliance label, which on most vehicles repeats the VIN beside the build date and weight ratings. - **Stampings** on the frame, the engine or body panels, on many vehicles. - **The diagnostic port**: most cars from the mid-2000s on report their VIN over OBD-II. - **Paper**: the registration, the insurance slip and the bill of sale. When two of these disagree, the vehicle has a story worth reading before anyone buys it. ## How Cardog keys on a VIN Two grains sit between one VIN and a whole model year, and both are cut from the VIN alone, offline, with no decode: ```text VIN 2T3R1RFV7MW180266 squish 2T3R1RFVM positions 1–8 + 10 squish:2T3R1RFVM nano 2T3R1RFV*MW positions 1–8, *, 10, 11 nano:2T3R1RFVMW nano 2A9TR1DA*PA123 extended WMI: + 12–14 nano:2A9TR1DAPA123 ``` - **`squish`** is positions 1 to 8 plus 10: manufacturer, descriptor and model year, with the plant left out. Every VIN sharing a squish is the same configuration of the same model year, which makes it the market grain: [quotes](https://cardog.app/docs/market-data) take it as an instrument. - **`nano`** is positions 1 to 8, a `*` for the check digit, then 10 and 11: the squish plus the plant, the build grain. For an extended-WMI VIN it adds positions 12 to 14, because there they are part of the manufacturer; for everyone else they are serial and are left out. - **The ref form drops the `*`**: the key `2T3R1RFV*MW` is the ref `nano:2T3R1RFVMW`, and parsing puts it back. ```typescript import { squishFromVin, nanoFromVin, checkDigitOk } from "@cardog/entities"; squishFromVin("2T3R1RFV7MW180266"); // "2T3R1RFVM" nanoFromVin("2T3R1RFV7MW180266"); // "2T3R1RFV*MW" nanoFromVin("2A9TR1DA1PA123045"); // "2A9TR1DA*PA123" checkDigitOk("2T3R1RFV7MW180266"); // true ``` The [vehicle graph](https://cardog.app/docs/vehicle-graph) covers which endpoints take which grain, and the [ref grammar](https://cardog.app/docs/ref-grammar) the full set of ref forms. ## Decoding a VIN with the API The structure tells you where the manufacturer, model year and plant are; it does not tell you the trim, engine or body, because those live in each manufacturer's VDS key. A decode does: ```bash curl "https://api.cardog.app/v2/vin/2T3R1RFV7MW180266" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` [Decode a Canadian VIN](https://cardog.app/docs/decode-canadian-vin) walks through the response, and the [VIN reference](https://cardog.app/docs/reference/vin) has the full shape, batch decoding and the links from a VIN to its recalls, listings and market instrument. Moving from another decoder? The [migration guides](https://cardog.app/resources/migration) map NHTSA vPIC, CarsXE, VinAudit and Auto.dev fields to Cardog's, one by one, and [What a VIN tells you](https://cardog.app/resources/learn/what-a-vin-tells-you) and [Squish and nano build codes](https://cardog.app/resources/learn/squish-and-nano-build-codes) go further into the keys. ## Standards | Standard | What it sets | | --- | --- | | ISO 3779 | VIN content and structure | | ISO 3780 | The World Manufacturer Identifier and its assignment | | 49 CFR Part 565 | VIN requirements in the United States: the meaning of each position, the check digit, the model-year code, placement | | CMVSS 115 | VIN requirements in Canada | --- # Chinese VINs ```bash curl "https://api.cardog.app/v2/vin/LGXCE4CB1R2065480" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` ```json { "vin": "LGXCE4CB1R2065480", "valid": true, "year": 2024, "make": "BYD", "model": "Yuan Plus", "refs": { "make": "make:byd", "model": "model:byd/yuan-plus", "modelYear": "model-year:byd/yuan-plus/2024", "bodyStyle": "body-style:sport-utility-vehicle-suv-multi-purpose-vehicle-mpv", "fuelType": "fuel-type:electric", "electrificationLevel": "electrification-level:bev-battery-electric-vehicle" }, "nano": "nano:LGXCE4CBR2", "squish": "squish:LGXCE4CBR", "specHighlights": { "seatingCapacity": { "value": 5, "unit": "COUNT" }, "driveType": { "value": "drive-type:fwd-front-wheel-drive" }, "fuelTypePrimary": { "value": "fuel-type:electric" } }, "links": { "specs": "/v2/specs/model-year%3Abyd%2Fyuan-plus%2F2024", "recalls": "/v2/vin/LGXCE4CB1R2065480/recalls", "quote": "/v2/quotes/model-year%3Abyd%2Fyuan-plus%2F2024" } } ``` Chinese-built vehicles decode through the same endpoint as every other VIN, with the same identity card, the same refs, and the same price per call. A decode names the model, the model year and the powertrain, and links straight to specs, recalls and market quotes. For the full response shape and batch decoding, see [Decode a Canadian VIN](https://cardog.app/docs/decode-canadian-vin). ## Sample VINs Real vehicles. Paste any of them into the call above. Battery-electric, plug-in hybrid, and export gasoline models all decode. | VIN | Vehicle | Powertrain | | --- | --- | --- | | `LGXCH6CB2R2033401` | 2024 BYD Seal | BEV | | `LGXCH4CD0R2033421` | 2024 BYD Sea Lion 07 | BEV | | `LC0CE4CB0P0384865` | 2023 BYD Dolphin | BEV | | `LGXCE4CB1R2065480` | 2024 BYD Yuan Plus | BEV | | `LGXCF4CD0S0198358` | 2025 BYD Tang L | BEV | | `L6TDC2028SP901912` | 2025 Zeekr 001 | BEV | | `L1NNSGHA7TB248173` | 2026 XPeng G6 | BEV | | `LS6C3E2S4SF028425` | 2025 Deepal S07 | PHEV | | `LNNBBDAT3SDH43288` | 2025 Chery Tiggo 7 | PHEV | | `LVTDB11B0SDD72694` | 2025 Jetour X50 | Gasoline | | `LGWEE4A51RL613983` | 2024 Haval Jolion Pro | Gasoline | | `LFB1FV691N2L12125` | 2022 Hongqi E-HS9 | BEV | | `LSKG48L10PD043188` | 2023 LDV MIFA 9 | BEV | | `LK6ADAE11NB401495` | 2022 Wuling Hongguang MINIEV | BEV | ## Model, year and plant A Chinese WMI does not name a model, and neither does the vehicle descriptor on its own: the same descriptor can carry a different car at a different plant. Every rule we ship is pinned to three parts of the VIN together: ```text LC0CE4CB0P0384865 │││││ ││ │││││ │└─ Plant (11): 0 │││││ └── Model year (10): P = 2023 └┴┴┴┴──── Descriptor (4-8): CE4CB ``` In 2025, `LC0CE4CB` at plant `4` is a BYD Yuan Up and at plant `6` a Yuan Plus: same descriptor, same year, different car. Reading all three is what makes the decode name the right one. The combination is also the `nano` grain in the response, so you can group Chinese VINs the same way offline with [`@cardog/entities`](https://www.npmjs.com/package/@cardog/entities). ## Send us a VIN Holding Chinese VINs we should see? Send them to [`POST /v2/feedback`](https://cardog.app/docs/feedback). It costs nothing. For what the 17 characters mean in general, see [VIN structure](https://cardog.app/docs/vin). --- - **Everything in one fetch:** https://cardog.app/docs.md - **The contract:** https://api.cardog.app/v2/openapi.json - **Get a key:** https://cardog.app/login?next=/account/api --- # Window stickers by VIN ```bash curl "https://api.cardog.app/v2/vin/1GCUYDED1MZ123456/window-sticker" \ -H "x-api-key: $CARDOG_API_KEY" \ -H "Cardog-Version: 2026-10-26" ``` The window sticker is the label on the glass of a new vehicle: trim, every factory option with its price, the destination charge, fuel economy, where it was assembled. It is the manufacturer's own statement of what it built. Nobody is required to keep one after the sale and no government holds a copy, so the only source is the manufacturer, and only some manufacturers serve one by VIN. `GET /v2/vin/{vin}/window-sticker` tells you whether we hold that label, and if not, why. Both answers are a `200`. Only the first costs credits. ## When we hold the label ```json { "vin": "1GCUYDED1MZ123456", "label": { "issuer": "gm", "retrievedAt": "2026-09-14T12:00:00.000Z", "vinVerified": true, "document": { "uri": "lake://oem/window-stickers/3f1a…", "sha256": "3f1a…", "mediaType": "application/pdf", "downloadUrl": "https://api.cardog.app/v2/window-sticker/3f1a…?exp=1790000000&sig=…" } }, "unavailable": null, "links": { "identity": "/v2/vin/1GCUYDED1MZ123456" } } ``` - `issuer` is the manufacturer whose service gave us the label. It is `null` in the rare case where we hold the file but the collecting run did not record who served it. - `retrievedAt` is when we collected it. - `vinVerified` is `true` only when the VIN was read from inside the document. Some manufacturers render the VIN as an image or in an embedded font, so it cannot always be read. `false` means the manufacturer's service returned this label when asked for this VIN, and nothing stronger. Treat an unverified label as the manufacturer's answer, not as proof it belongs to this vehicle. - `document` names the file by its content address and SHA-256. The same file always has the same address, so you can cite it. `downloadUrl` is a signed link to the PDF, good for 24 hours. A held label costs **5 credits**. ## The PDF ```bash curl "$DOWNLOAD_URL" -o sticker.pdf ``` `downloadUrl` is a signed link: `GET /v2/window-sticker/{sha256}?exp=…&sig=…`. Open it exactly as printed. It needs no API key and no session, so you can open it in a browser tab, put it in an `