API conventions

The mechanics every v2 group shares: encoding, response shape, null semantics, and the stability contract.

Every /v2 group speaks the same dialect. Learn the conventions once and every endpoint reads the same way — the reference pages then only have to tell you what an operation returns, never how to talk to it.

Request encoding

Repeatable filters repeat the parameter. No commas, no bespoke 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 of the canonical spec attributes — numeric attributes take .min/.max, availability attributes take repeated values:

http
GET /v2/listings/search?spec.fuelEconomyCombined.min=35&spec.heatedSeatsFront=standard

Unknown spec keys are a 400 unknown_spec_attributes naming the offending keys — the same never-fuzzy law that governs refs.

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 correctly encoded — one more reason to follow links instead of assembling paths.

Response conventions

  • camelCase keys, everywhere. No snake_case anywhere in a 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; the cap is in the reference.
  • Every response carries links — rel → server-relative path to adjacent resources (entity → quotes, recalls, listings, specs). Traverse them instead of constructing URLs.
  • null means "not derivable" — never "unknown to us silently." A null fuelType on a VIN decode says the registry cannot support that claim for this VIN; it is a stated fact about coverage, not a shrug. The API never substitutes a guess where it cannot verify.
  • A failed resolution returns nothing, not something. When GET /v2/vin/{vin} answers "valid": false, every identity field in that response is null and every ref is null — including the nano/squish grains. We do not fall back to adjacent records to fill the gap, 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 somewhere are different facts, and the payload keeps them in different places. 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, display the second, and never let a field's value leave you guessing which one you got.

Versioning and stability

The v2 contract is frozen and changes additively: new fields, new endpoints, new enum members, new error codes — never renames, removals, or type changes on what is already shipped.

  • Open sets stay open. Error codes (15 well-known today), authority keys, bar sources — 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. Store refs; they will not rot.
  • v1 is legacy. It keeps working for existing integrations, but v2 is where the contract lives — new work should target /v2 exclusively, and 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 are all-accepted, per-item-resolved. One malformed item never fails the batch — it fails its own row with the standard error envelope, in the same position it was sent:

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":["1HGCM82633A123456","NOTAVIN"]}'

Example

2 keys
"response": {
"results": [
0: {3 items},
1: {3 items}
],
"meta": {
"requested": 2,
"decoded": 1,
"failed": 1
}
}

results preserves request order; meta carries the counts. Up to 1,000 VINs per batch, metered per VIN decoded — failed rows cost nothing (see Credits & limits).