# 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: 2.0.0-rc.3
- Auth: API key via `x-api-key` or `Authorization: Bearer` — create one at https://cardog.app/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`, `platform` (all other groups below are frozen contract, landing shortly)
- Every docs page has a markdown twin: append `.md`, or send `Accept: text/markdown`

---

# Quickstart

The Cardog API is VIN identity, canonical specs, listings, live market data,
and recalls behind one key.

Base URL: `https://api.cardog.app`

**The ref is the API.** Every parameter is an entity ref (`make:tesla`,
`model-year:honda/cr-v/2026`) or a VIN. Free text is accepted in exactly one
place — `GET /v2/entities/resolve` — which returns refs with confidence. The
API never silently fuzzy-matches: an unknown ref is a 400 that names the ref.

## 1. Get an API key

1. Create an account at [cardog.app](https://cardog.app)
2. Create a key under [Account → API](https://cardog.app/account/api)
3. Copy it immediately — it is shown once

Send it on every request, either way works:

```bash
-H "x-api-key: $CARDOG_API_KEY"
# or
-H "Authorization: Bearer $CARDOG_API_KEY"
```

## 2. Pick your client

curl works everywhere; the SDKs give you the same surface with types. Both
are generated from the same contract as this reference:

```bash
npm install @cardog/api   # TypeScript / JavaScript
pip install cardog        # Python
```

## 3. Resolve free text to refs

Start from whatever you have — a search box string, a typo, a VIN-less
description — and get refs back:

```bash
curl "https://api.cardog.app/v2/entities/resolve?q=2021%20civic" \
  -H "x-api-key: $CARDOG_API_KEY"
```

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const resolved = await client.v2.entities.resolve("2021 civic");
console.log(resolved.best, resolved.candidates);
```

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

resolved = client.v2.entities.resolve("2021 civic")
print(resolved.best, resolved.candidates)
```

The response is a list of candidates ordered best-first, each with a
confidence score. `best` is null when nothing clears the confidence floor —
the API never guesses for you. Hold on to the refs you get back; they are the
currency for every other call.

If you already have a VIN, decode it instead — the identity card carries the
same refs plus links to adjacent resources:

```bash
curl "https://api.cardog.app/v2/vin/1HGCM82633A123456" \
  -H "x-api-key: $CARDOG_API_KEY"
```

```typescript
const identity = await client.v2.vin.getByVin("1HGCM82633A123456");
console.log(identity.refs);
```

```python
identity = client.v2.vin.get("1HGCM82633A123456")
print(identity.refs)
```

## 4. Query with refs

Refs plug into every group — listings, quotes, specs, recalls. **Not every
parameter is a ref**, and the distinction is worth learning once:

- **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 offender.
- **Scalar parameters** are ordinary filters: `year.min`, `year.max`,
  `price.max`, `limit`. They take numbers.

So on listings, a make is a ref and a year is a range — `make=make:tesla`
and `year.min=2022` in the same query is normal, not a mixed metaphor.
Listings filters on **make and model refs only**; there is no
`model-year=` filter, because a model year on this surface is expressed as
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 — both, in one query
curl "https://api.cardog.app/v2/listings/search?make=make:tesla&year.min=2022&limit=5" \
  -H "x-api-key: $CARDOG_API_KEY"

# 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"

# Recalls scoped to an entity — make:, model: or 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"
```

```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("model-year:honda/cr-v/2026")

# Recalls scoped to an entity
recalls = client.v2.recalls.entity("make:toyota")
```

## The ref grammar

A ref is `domain:slug`:

| Part | Meaning | Examples |
| --- | --- | --- |
| domain | The entity domain | `make`, `model`, `model-year`, `fuel-type`, `squish`, `recall` |
| slug | The entity's path in that domain; hierarchical slugs use `/` | `tesla`, `mini/hardtop`, `honda/cr-v/2026` |

Rules that matter:

- **In URL paths**, slashes inside a ref must be URL-encoded:
  `/v2/entities/model-year:honda%2Fcr-v%2F2026`
- **In query strings**, refs are passed raw: `?make=make:tesla`
- Ref filters are repeatable: `?make=make:tesla&make=make:rivian`
- List every domain with a bare `GET /v2/entities` (the domain index),
  browse a domain's members with `GET /v2/entities?domain=make`, or
  dereference any ref with `GET /v2/entities/{ref}`

## Responses link forward

Every v2 response carries a `links` block — rel → server-relative path — so
adjacent resources (entity → quotes, recalls, listings, specs) are one
traversal away. Follow the links instead of building URLs by hand.

## Errors are instructions

Every non-2xx body is the same envelope: a machine-dispatchable `code`, a
`message` naming the offending input, a `hint` saying what to do next, and
nearest-ref `suggestions` where computable. See the [Errors guide](/docs/errors).

## Credits

Metered responses carry `X-Credits-*` headers, and `GET /v2/pricing` is the
machine-readable rate card (no auth required). Budget mid-task from those —
never from hardcoded numbers.

## For agents

Every docs page has a markdown twin: append `.md` to any docs URL, or send
`Accept: text/markdown`. The whole platform — auth, errors, ref grammar,
every operation — is one fetch: [`/docs.md`](/docs.md).

---

# 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](/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"
```

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"
```

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"
```

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. 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](/docs/reference/listings) |
| Canonical specs (155 attributes) | model-year ref | [Specs](/docs/reference/specs) |
| Recall campaigns (TC + NHTSA, fused) | recall ref → model-year refs | [Recalls](/docs/reference/recalls) |
| Live quotes and the tape | model-year ref, squish | [Instruments](/docs/reference/instruments), [Quotes](/docs/reference/quotes), [Tape](/docs/reference/tape) |
| Safety ratings and complaints | model-year ref | [Safety](/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.


---

# The ref grammar

Every identifier in the [Vehicle Graph](/docs/vehicle-graph) — `make:honda`,
`model-year:honda/civic/2021`, `squish:5TDGSKFCR` — obeys one grammar, and
that grammar is published as an open-source package. Install it and your code
can validate, build, parse, and *type* refs entirely offline: no API key, no
network call, no rate limit. The language is free to hold. The answers are
what the API serves.

```bash
npm install @cardog/entities
```

ESM, TypeScript-first, zero dependencies, Apache-2.0. Runs in Node, browsers,
workers, and edge runtimes.

## Validate and build, offline

```ts
import {
  isRef, isRefOf, buildRef, parseRef,
  modelYearRef, parseModelYearRef, recallRef,
} from "@cardog/entities";

isRef("make:tesla");                        // true
isRef("make:Tesla");                        // false — casing is part of the grammar
isRefOf(input, "model-year");               // narrows to EntityRef<"model-year">

modelYearRef("model:tesla/model-y", 2024);  // "model-year:tesla/model-y/2024"
parseModelYearRef("model-year:honda/civic/2021");
// { make: "make:honda", model: "model:honda/civic", year: 2021 }

recallRef("nhtsa", "23V123");               // "recall:nhtsa/23v123"
```

Every builder validates against the grammar and **throws rather than
normalizes** — the same never-fuzzy law the API enforces at its boundary,
available in your process. Validate user input before it ever costs a call;
reject a malformed ref in a form handler; assert grammar in your tests.

## The VIN grains, offline

`nano` and `squish` — the grains between one VIN and a whole model year —
are pure functions of the VIN, so the package derives them locally:

```ts
import { squishFromVin, nanoFromVin, squishRef, nanoRef } from "@cardog/entities";

squishFromVin("5TDGSKFC8RS123456");  // "5TDGSKFCR" — the exact-config market grain
nanoFromVin("5TDGSKFC8RS123456");    // "5TDGSKFC*RS" — the comparables grain
squishRef("5TDGSKFCR");              // "squish:5TDGSKFCR" — ready for /v2/quotes
```

A VIN in your database becomes a market-grain ref without touching the
network — the API call you then make is the one that quotes it, not the one
that derives it.

## Typed unions

The enumerable domains ship as TypeScript unions, so a misspelled ref in a
fixed vocabulary fails at compile time, not at request time:

```ts
import type { FuelTypeRef, BodyStyleRef, EntityDomain } from "@cardog/entities/types";
import type { SpecAttributeId } from "@cardog/entities";

const fuel: FuelTypeRef = "fuel-type:electric";  // ✓ checked by tsc
const attr: SpecAttributeId = "curbWeight";       // the 155-attribute catalog, typed
```

The open-ended domains (`make`, `model`) are typed structurally
(`EntityRef<"make">`) — their vocabularies live in the registry, and free
text meets them through [resolve](/docs/vehicle-graph#entering-the-graph).

The package also carries the spec attribute catalog itself
(`@cardog/entities/spec`) — the same catalog `GET /v2/specs/catalog`
serves, importable for building filter UIs and validating
`spec.{attributeId}` query keys before sending them.

## The stability contract

Refs are permanent join keys, and the package states that machine-checkably:

- `GRAMMAR_VERSION` is `1` — it bumps only for a breaking
  change to the grammar itself, which the platform treats as a
  never-event. New domains and entities are additive.
- `vocabulary` carries the provenance of the build the package was generated
  from, so what your process validates against is auditable.
- A ref, once issued, keeps its meaning for the life of the platform. Store
  refs in your columns and your config; they will not rot.

## Where the API begins

The package deliberately contains **no entity data** — no make list, no model
vocabulary, no display names — and no VIN decoding. Holding the grammar tells
you a ref is *well-formed*; only the registry can tell you what it *names*:

```bash
# free text → refs (the one door for unstructured input)
curl "https://api.cardog.app/v2/entities/resolve?q=2021%20civic" -H "x-api-key: $CARDOG_API_KEY"

# ref → the entity: names, hierarchy, counts, links into every group
curl "https://api.cardog.app/v2/entities/model:honda%2Fcivic" -H "x-api-key: $CARDOG_API_KEY"

# VIN → identity as refs
curl "https://api.cardog.app/v2/vin/5TDGSKFC8RS123456" -H "x-api-key: $CARDOG_API_KEY"
```

That is the intended division of labour: **hold the language locally, ask the
platform for answers.** Validate and construct refs offline all day; the
moment a question needs the registry — resolution, enumeration, specs,
recalls, quotes — [/v2/entities](/docs/reference/entities) and its sibling
groups are the doors.

---

# Authentication

Every `/v2/*` request needs a credential. For developers that credential is
an **API key**, created under [Account → API](https://cardog.app/account/api).

## Creating a key

1. Sign in at [cardog.app](https://cardog.app)
2. Go to [Account → API](https://cardog.app/account/api)
3. Create a key and name it for its environment ("production", "ci", …)
4. Copy it immediately — it is shown once and never again

## Sending the key

Two headers are accepted; they are equivalent:

```bash
# x-api-key header
curl "https://api.cardog.app/v2/vin/1HGCM82633A123456" \
  -H "x-api-key: $CARDOG_API_KEY"

# Authorization bearer
curl "https://api.cardog.app/v2/vin/1HGCM82633A123456" \
  -H "Authorization: Bearer $CARDOG_API_KEY"
```

## Plans

Monthly request allowances per plan (rendered from the platform's own plan
definitions — see [/pricing](https://cardog.app/pricing) for current prices):

| Plan | API requests / month |
| --- | --- |
| Free | 50 |
| Starter | 1,000 |
| Pro | 5,000 |
| Business | 25,000 |
| Enterprise | 100,000 |

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](/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; revoke unused keys in the dashboard
- Monitor usage under [Account → API](https://cardog.app/account/api)

---

# API conventions

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](/docs/errors), 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"]}'
```

```json
{
  "results": [
    { "ok": true, "vin": "1HGCM82633A123456", "identity": { "…": "…" } },
    { "ok": false, "vin": "NOTAVIN", "error": { "code": "invalid_vin", "…": "…" } }
  ],
  "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](/docs/credits)).

---

# Credits & limits

Everything commercial on the platform is denominated in **credits** — one
currency across every route group, REST and MCP alike. A request's price
depends on what it answers, not how many bytes it moves: registry reads are
cheap, identity and market answers cost more.

> **The posted card** below is version 6, as of
> 2026-07-30, 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`
> is monotonic, 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). |

Rates are per *unit*, and most units are one request. The exceptions meter
per VIN: batch decode charges one `vin` unit per VIN decoded (a failed row
costs nothing), and the recalls VIN check prices the compliance answer, not
the transport.

## Plans

| Plan | Price | Credits / month | Past the allowance |
| --- | --- | --- | --- |
| Free | $0/mo | 50 | Hard stop (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) |

Two laws worth internalizing:

- **Paid tiers are never blocked.** Past the allowance, requests keep
  working and the excess is billed as overage. A production integration does
  not fall over because a month ran long.
- **The free tier hard-stops.** At the allowance, metered requests return
  `402 insufficient_credits` until the month resets or you upgrade. It is
  an evaluation tier, not a production tier.

## The budget headers

Every metered response carries the live 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 (0 on non-2xx — errors are
free). One deliberate 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;
`-1` in Allowance/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 (50-credit allowance). First, a VIN
decode — 2 credits:

```bash
curl -i "https://api.cardog.app/v2/vin/1HGCM82633A123456" -H "x-api-key: $CARDOG_API_KEY"
```

```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" is computable without a single extra call.

## The free-tier stop

At the allowance, the free tier answers `402` with the standard
[error envelope](/docs/errors) — and the budget headers, so even the refusal
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 allowance before the stop catches it. Those
requests are charged. The stop then lands on the very next request — the
overshoot is one request in practice, and never more than a minute's worth.

## Budget like a machine

`GET https://api.cardog.app/v2/pricing` returns this entire card — rates, plans, header
names — as JSON, 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 carries a monotonic `version` so you can detect change.

---

# Errors

**Errors are instructions.** Every non-2xx response from a `/v2/*` route is
exactly one shape — the error envelope. `code` is machine-dispatchable,
`hint` says what to DO next, and `suggestions` carries nearest-ref
candidates so a typo'd ref self-corrects in one turn.

```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, never guessed

An unknown-but-well-formed ref is ALWAYS a 400 that names the offending ref.
Suggestions are advisory — never silently applied. You will never get a
quietly fuzzy-matched result or a silent empty set for a bad ref.

## The envelope

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | string | yes | Machine-dispatchable error code. Well-known codes: invalid_request, invalid_filters, invalid_ref, invalid_vin, invalid_nano_refs, unknown_entity_refs, unknown_entity_domain, unknown_spec_attributes, not_found, unauthorized, forbidden, rate_limited, insufficient_credits, internal_error, unavailable. The set is open — new codes are additive. |
| `message` | string | yes | Human/agent-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 | Fetchable Markdown correction page for this error code |
| `details` | object[] | no | Field-level validation failures (grammar errors) |
| `details[].path` | string | no | The failing field, e.g. "filters.makes.0" |
| `details[].message` | string | yes |  |
| `refs` | string[] | no | The offending refs/keys, verbatim (unknown_entity_refs, unknown_spec_attributes, invalid_nano_refs) |
| `suggestions` | object[] | no | Nearest-ref candidates per offending input. Advisory — never silently applied. |
| `suggestions[].invalid` | string | yes | The offending input, verbatim |
| `suggestions[].nearest` | object[] | yes | Best candidates, most likely first. Advisory — never auto-applied. |
| `suggestions[].nearest[].ref` | string | yes | A valid registry ref, e.g. "make:tesla" |
| `suggestions[].nearest[].name` | string | no | Registry display name, e.g. "Tesla" |
| `suggestions[].resolve` | string | no | Server-relative resolve URL, e.g. "/v2/entities/resolve?domain=make&q=teslla" |

The code set is **open**: new codes are additive, never breaking. Dispatch on
the codes you know and fall back on `message`/`hint`.

## Error codes

| Code | Meaning |
| --- | --- |
| [`invalid_request`](/docs/errors/invalid_request) | The request body or parameters did not match the operation's schema. |
| [`invalid_filters`](/docs/errors/invalid_filters) | One or more filters used invalid ref syntax, scalar values, or ranges. |
| [`invalid_ref`](/docs/errors/invalid_ref) | A path or query parameter did not match the `{domain}:{key}` ref grammar. |
| [`invalid_vin`](/docs/errors/invalid_vin) | The supplied VIN failed the platform's length or character rules. |
| [`invalid_nano_refs`](/docs/errors/invalid_nano_refs) | A nano filter did not match the VIN-derived nano key grammar. |
| [`unknown_entity_refs`](/docs/errors/unknown_entity_refs) | The refs were well formed, but the entity registry did not recognize them. |
| [`unknown_entity_domain`](/docs/errors/unknown_entity_domain) | The domain was well formed, but the entity registry does not serve it. |
| [`unknown_spec_attributes`](/docs/errors/unknown_spec_attributes) | A `spec.*` filter named an attribute that is not in the current catalog. |
| [`not_found`](/docs/errors/not_found) | The requested route or resource does not exist. |
| [`unauthorized`](/docs/errors/unauthorized) | The request did not carry a valid API key, session, or internal credential. |
| [`forbidden`](/docs/errors/forbidden) | The credential is valid but does not grant access to the requested route group. |
| [`rate_limited`](/docs/errors/rate_limited) | The credential exhausted the request limit described by the response. |
| [`insufficient_credits`](/docs/errors/insufficient_credits) | A hard-stop plan has no allowance or prepaid pack balance left. |
| [`internal_error`](/docs/errors/internal_error) | The server failed while processing a request that the caller cannot repair. |
| [`unavailable`](/docs/errors/unavailable) | A required platform dependency was temporarily unavailable. |

Open a code for what happened, the exact next action, and a working corrected
request. Error envelopes link directly 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) |
| 404 | Resource not found |
| 429 | Per-minute rate limit exceeded |
| 500 | Internal error |

---

# Decode a Canadian VIN by API

```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:

```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"
  }
}
```

> **`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](/docs/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](/docs/reference/vin).

## `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](/docs/vin).

## 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](/docs/market-data).
- **`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`](https://www.npmjs.com/package/@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](/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",
  "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](/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/account/api
- **MCP:** `claude mcp add --transport http cardog "https://mcp.cardog.io/mcp?api_key=$CARDOG_API_KEY"`

---

# Understanding VINs

A Vehicle Identification Number (VIN) is a unique 17-character code that
identifies a specific vehicle. Based on ISO standards 3779 and 3780, VINs are
crucial for accurate vehicle identification and tracking.

## VIN standards

VINs follow standards established by the International Organization for
Standardization (ISO):

- **ISO 3779** defines the VIN structure
- **ISO 3780** establishes the World Manufacturer Identifier system

Different regions have compatible but distinct implementations:

| Region | Format |
| --- | --- |
| North America (>2,000 vehicles/year) | WMI + Vehicle attributes + Check digit + Model year + Plant code + Sequential number |
| North America (≤2,000 vehicles/year) | WMI + 9 + Vehicle attributes + Check digit + Model year + Plant code + Manufacturer ID + Sequential number |
| European Union (>500 vehicles/year) | WMI + Vehicle characteristics + Vehicle identification |
| European Union (≤500 vehicles/year) | WMI + 9 + Vehicle characteristics + Manufacturer ID + Vehicle identification |

## VIN structure overview

| Section | Positions | Meaning |
| --- | --- | --- |
| WMI | 1–3 | World Manufacturer Identifier — region and manufacturer codes |
| VDS | 4–8 | Vehicle Descriptor Section — vehicle attributes and platform |
| Check digit | 9 | Validation digit (required in North America and China) |
| VIS | 10–17 | Vehicle Identifier Section — unique vehicle details |

```text
1HD1KB4157Y123456
│││││││││││└──────┘
│││││││││││ Serial number (12-17)
│││││││││││
││││││││││└─ Plant code (11)
│││││││││└── Model year (10)
│││││││└──── Check digit (9)
││││││└───── Engine type (8)
│││││└────── Body type (7)
││││└─────── Series (6)
│││└──────── Restraint system (5)
││└───────── Platform (4)
│└────────── Make (2-3)
└─────────── Region (1)
```

## World Manufacturer Identifier (WMI)

The first three characters identify the manufacturer and region. For
manufacturers producing fewer than 1,000 vehicles per year, the third digit is
'9', with additional identification in positions 12–14.

### Region codes (first character)

| First character | Region |
| --- | --- |
| A–C | Africa |
| J–R | Asia |
| S–Z | Europe |
| 1–5, 7 | North America |
| 6 | Oceania |
| 8–9 | South America |

Common examples:

- 1, 4, 5, 7: United States
- 2: Canada
- 3A–3W: Mexico
- J: Japan
- K: Korea
- L: China
- W: Germany
- VF–VR: France
- SA–SM: United Kingdom

## Vehicle Descriptor Section (VDS)

Characters 4–9 describe the vehicle's attributes and include a check digit.
The VDS format varies by manufacturer but typically includes:

| Position | Content |
| --- | --- |
| 4 | Vehicle line, platform, or model series |
| 5 | Safety and restraint system type |
| 6–7 | Body style and series information |
| 8 | Engine type and size |
| 9 | Calculated validation digit |

## Vehicle Identifier Section (VIS)

The final eight characters (10–17) uniquely identify the specific vehicle:

| Position | Content |
| --- | --- |
| 10 | Single character encoding the vehicle's model year |
| 11 | Manufacturing plant identifier |
| 12–17 | Production sequence number (12–14 double as manufacturer ID for small manufacturers) |

## Model year encoding

The 10th position encodes the model year using a repeating pattern
(letters exclude I, O, Q):

```text
Code Year  Code Year  Code Year
A    2010  L    2020  Y    2030
B    2011  M    2021  1    2031
C    2012  N    2022  2    2032
D    2013  P    2023  3    2033
E    2014  R    2024  4    2034
F    2015  S    2025  5    2035
G    2016  T    2026  6    2036
H    2017  V    2027  7    2037
J    2018  W    2028  8    2038
K    2019  X    2029  9    2039
```

The same codes covered 1980–2009 in the previous cycle (A–Y for 1980–2000,
1–9 for 2001–2009).

## Check digit calculation

The 9th position contains a check digit that validates the VIN:

1. **Transliterate letters** to numbers:

   ```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. **Apply weights** per 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. **Calculate**: sum the products and divide by 11. The remainder is the
   check digit (X if 10).

> The letters I, O, and Q are never used in VINs to avoid confusion with
> numbers.

## Decoding VINs with the API

The v2 surface decodes a VIN to its graph identity — display names, entity
refs, nano/squish grains, and links to the instrument, recalls, and listings
for that vehicle:

```bash
curl "https://api.cardog.app/v2/vin/1HGCM82633A123456" \
  -H "x-api-key: $CARDOG_API_KEY"
```

See the [VIN reference](/docs/reference/vin) for the full response shape,
batch decoding, and the VIN → recalls / listings / instrument bridges.

## Where to find a VIN on the vehicle

- Lower corner of the windshield (driver's side)
- Under the hood near the latch
- Front end of the vehicle frame
- Driver's side door pillar

---

# Market data

The platform treats the live vehicle market the way an exchange treats
securities: define the **instrument** (what is being priced), publish a
**quote** (the live book, summarized), and keep a **tape** (every observation,
rolled up into history). Once you read one quote card you can read them all.

## Instruments: what is being priced

An instrument is a slice of the market with enough liquidity to price. Two
grains do most of the work:

- **`model-year:`** — "Honda CR-V 2026" as one instrument. Thousands of
  live listings back it; medians are stable; this is the grain for market
  overviews, dashboards, and "what do these go for."
- **`squish:`** — the first 9 meaningful VIN characters: one exact
  configuration of one model year. Far fewer listings, far sharper answer;
  this is the grain for "what is *this car* worth," appraisal, and
  trade-in logic. Derive it from any VIN, offline, forever.

Between them sit qualified sub-grains (`mmy`, `mmy.drive`, `mmy.fuel`, `mmy.body`, `squish`) that split a model year by
drive, fuel, or body where those materially reprice the vehicle.

Use `GET /v2/instruments?q=` for symbology search, or take the
`instrument` link any VIN decode or entity response hands you.

## Reading a quote card

```bash
curl "https://api.cardog.app/v2/quotes/model-year:honda%2Fcr-v%2F2026" \
  -H "x-api-key: $CARDOG_API_KEY"
```

```json
{
  "ref": "model-year:honda/cr-v/2026",
  "grain": "mmy",
  "liveCount": 31192,
  "bestPrice": 32800,
  "priceP25": 47071,
  "priceMedian": 49775,
  "priceP75": 52399,
  "avgDomDays": 154,
  "cuts30d": 284,
  "avgCutPct30d": 3.24,
  "sightings30d": 4659,
  "asOf": "2026-07-12T06:35:22.181Z",
  "links": {
    "instrument": "/v2/instruments/model-year%3Ahonda%2Fcr-v%2F2026",
    "history": "/v2/tape/history/model-year%3Ahonda%2Fcr-v%2F2026"
  }
}
```

How to read it:

- **`liveCount`** — active listings backing the quote right now. It is the
  quote's confidence: a median over 31,192 cars is a market fact; a median
  over 4 is an anecdote. Check it before trusting the numbers.
- **`bestPrice` / `priceP25` / `priceMedian` / `priceP75`** — the
  book's shape. Median is the market price; the P25–P75 band is the honest
  spread; best is the floor (and often an outlier — that gap between best
  and P25 above is real, and typical).
- **`avgDomDays`** — average days-on-market: how fast the market clears.
- **`cuts30d` / `avgCutPct30d`** — price cuts observed in the trailing
  30 days and their average size. Cuts are the market conceding; rising cut
  counts lead falling medians.
- **`sightings30d`** — observation volume behind the trailing stats.
- **`asOf`** — when this quote was computed. Quotes describe a moment, not a promise.

Need several instruments at once? `GET /v2/quotes?refs=a,b,c` quotes up to
20 refs in one request, returned in request order.

## History: daily bars over a window

```bash
curl "https://api.cardog.app/v2/tape/history/model-year:honda%2Fcr-v%2F2026?window=1m" \
  -H "x-api-key: $CARDOG_API_KEY"
```

History returns ascending daily **bars** — `day`, `median`, `p25`,
`p75`, `best`, plus `count` or `prints` depending on the bar's
`source`. A `"fold"` bar is a once-daily snapshot of the live book; a
`"prints"` bar is built from the individual observations on days with no
snapshot. The set of sources is open. Windows: `1w`, `1m`, `3m`, `6m`, `ytd`, `1y`, `3y`, `5y`, `10y`, `all`.

## The tape: prints

A **print** is one observation of one listing — a price seen on a seller's
site at a moment in time, projected onto its instrument:

```bash
curl "https://api.cardog.app/v2/tape/live" -H "x-api-key: $CARDOG_API_KEY"
```

Each print carries the instrument `ref`, `price`, the seller `domain`
it was observed on, the timestamp, and `vinTail` — the last 6 of the VIN,
enough to dedup a vehicle across sites without ever publishing the full VIN.
The live feed also reports throughput (`perMinute`, `today`), which is
the honest answer to "how alive is this data."

Everything above is *derived* from prints: bars aggregate prints, quotes
aggregate bars plus the live book. The tape is the ground truth.

## What we are, and are not

Cardog runs the rails: we observe the market, publish the instruments, and
serve the same quotes to everyone. We hold no inventory, take no side of any
trade, and sell no placement in the numbers — a quote reads the same whether
the asker is a buyer, a dealer, or a lender. Neutral market infrastructure
is the product; the moment the rail trades on its own rail, the quote stops
being worth citing.

---

# Transport Canada recall lookup by VIN, by API

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

```json
{
  "vin": "2T3R1RFV7MW180266",
  "modelYearRef": "model-year:toyota/rav4/2021",
  "resolved": true,
  "total": 2,
  "recalls": [
    {
      "ref": "recall:tc/2023684",
      "authority": "tc",
      "authorityLabel": "Transport Canada",
      "campaignNumber": "2023684",
      "component": "Airbag",
      "defectSummary": "On certain vehicles, a sensor for the occupant classification system (OCS) may not have been manufactured properly. …",
      "recallDate": "2023-12-20",
      "unitsAffected": 99965,
      "affects": [
        { "modelYearRef": "model-year:toyota/rav4/2021", "year": 2021, "unitsAffected": 99965 }
      ]
    }
  ],
  "asOf": "2026-07-23T10:04:24.442Z",
  "links": { "identity": "/v2/vin/2T3R1RFV7MW180266" }
}
```

A VIN in, the campaigns affecting that vehicle out — Transport Canada and
NHTSA fused into one answer, each campaign carrying its issuing authority
and official campaign number so the result is citable, not just
displayable. 5 credits per VIN checked.

The VIN is bridged to its model-year node in the graph, and the campaigns
keyed to that node come back —
[Decode a Canadian VIN by API](/docs/decode-canadian-vin) covers the
identity the check bridges through.

## What SOR/2024-274 requires

**SOR/2024-274** amended Canada's Motor Vehicle Safety Regulations: s.15.05
obliges vehicle manufacturers to operate a public, VIN-searchable recall
lookup, so that anyone holding a VIN can learn whether that vehicle has
outstanding recalls. If you build for dealers, insurers, lenders, fleets,
or a registry, that per-VIN check is the one your surface has to get right
— and getting it right means getting the *negative* answer right, which is
the part most integrations break on.

## `resolved: false` is not "no recalls"

The check has two distinct empty states, and conflating them is the classic
compliance bug:

```json
{
  "vin": "1HGCM82633A123456",
  "modelYearRef": null,
  "resolved": false,
  "total": 0,
  "recalls": [],
  "asOf": "2026-07-23T10:04:24.442Z",
  "links": { "identity": "/v2/vin/1HGCM82633A123456" }
}
```

- **`resolved: true, total: 0`** — the VIN is bridged to the graph and the
  corpus holds *no campaigns* for it. That is a clean answer you can act on.
- **`resolved: false`** — the VIN is not bridged yet. `total: 0` here is
  *vacuous*: the platform is telling you it cannot support a recall claim
  for this VIN, not that the vehicle is clear.

Branch on `resolved` before you branch on `total`. A compliance surface
that renders `resolved: false` as "No recalls ✓" is wrong in exactly the
way s.15.05 exists to prevent — this is the same null-semantics law that
runs through the whole API: absence of evidence is stated, never dressed up
as evidence of absence.

## Both authorities, kept distinct

`total: 2` on the RAV4 above, second record elided: the same occupant
classification defect exists as both Transport Canada campaign `2023684`
and NHTSA campaign `23V865000`. The corpus fuses the authorities into one
answer but keeps their records distinct — each stays citable against its
own issuer.

## Citing a result

Every campaign is attributable, because an uncited recall answer is worth
nothing in a dispute:

- **`authority` / `authorityLabel`** — who issued the campaign
  (`"tc"` / Transport Canada, `"nhtsa"` / NHTSA — the set is open).
- **`campaignNumber`** — the authority's own identifier, resolvable in
  their systems.
- **`asOf`** — corpus freshness at the moment of the check. Persist it
  with the check: "no recalls as of {asOf}, per {authority}" is a defensible
  record; "no recalls" is not.

## Sweeping at entity scope

Fleets and portfolios do not check VIN-by-VIN first — they watch the
entities they hold. Any make, model, or model-year ref sweeps its campaigns:

```bash
# Everything affecting a model year you hold in volume
curl "https://api.cardog.app/v2/recalls/entity/model-year:toyota%2Frav4%2F2021" \
  -H "x-api-key: $CARDOG_API_KEY"

# The whole make, for portfolio triage
curl "https://api.cardog.app/v2/recalls/entity/make:toyota" \
  -H "x-api-key: $CARDOG_API_KEY"
```

The pattern that scales: sweep the entity refs you hold on a schedule, and
when a new campaign lands, run the per-VIN check across the affected slice
of your fleet. `GET /v2/recalls/feed` (newest campaigns first) and
`GET /v2/recalls/stats` (corpus counters and latest recall date) drive the
watch loop. The full route surface for the group is in the
[recalls reference](/docs/reference/recalls).

## The affects grain

A campaign's scope is **campaign × model-year**: each entry in `affects`
binds one model-year ref with its `unitsAffected` where the authority
published one. Two consequences worth knowing. First, `affects` is scoped
to how you asked — an entity query lists only the years in your scope, the
campaign detail (`GET /v2/recalls/{recall-ref}`) lists them all. Second,
the grain is the authority's, not the vehicle's: a campaign names model
years, and the per-VIN check is that grain bridged through the VIN's
identity. Where a manufacturer scopes below the model year (a VIN range, a
plant window), the authoritative per-VIN answer is the manufacturer's own
s.15.05 lookup — the campaign record here tells you exactly which one to
ask, and cite.

## Pricing

A VIN check is 5 credits. Non-VIN registry reads in the
group — entity sweeps, the feed, stats, campaign detail — are
1 credit each. See [Credits & limits](/docs/credits).

## What is not here

**Recall completion status is not in this API.** Whether a specific vehicle
has had a specific campaign *performed* lives with the manufacturer, and we
do not redistribute it. If your product needs "was this fixed," you need an
OEM relationship, not a data vendor.

**If you need one lookup, use Transport Canada's public recall database.**
It is free and it is the primary source. What we add is VIN-level matching,
NHTSA fusion, entity-scoped sweeps, and an API you can put behind a
product. If you're doing this once by hand, see
[How to look up vehicle recalls by VIN](/blog/look-up-recalls-by-vin).

**Push notification is not built.** Recall watches — tell me when a
campaign lands on a VIN I care about — are on the roadmap, not in the API.
Today you poll `GET /v2/recalls/feed`.

---

- **Everything in one fetch:** https://cardog.app/docs.md
- **The contract:** https://api.cardog.app/v2/openapi.json
- **Get a key:** https://cardog.app/account/api
- **MCP:** `claude mcp add --transport http cardog "https://mcp.cardog.io/mcp?api_key=$CARDOG_API_KEY"`

---

# Crash ratings and safety equipment, by VIN


Crash ratings answer to two different keys, and which one you use decides
how precise the answer is.

### By model year — every configuration NHTSA rated

```bash
curl "https://api.cardog.app/v2/safety/model-year:ram/1500/2026" \
  -H "x-api-key: $CARDOG_API_KEY"
```

You get `ratings` — every configuration NHTSA tested for that model year —
plus `best`, the highest-scoring one. Use this when you have a year, make
and model but not a specific vehicle.

### By build code — the rating for *that* vehicle

```bash
curl "https://api.cardog.app/v2/safety/nano:1C6SRFVTTN" \
  -H "x-api-key: $CARDOG_API_KEY"
```

The **nano** is the build code from a VIN decode — VIN positions 1–8 plus
10, returned as `refs.nano` on `/v2/vin`. It identifies the exact
configuration: cab, drivetrain, fuel, weight class.

This is the one you want when you have a VIN, because NHTSA does not rate
"a 2026 Ram 1500". It rates a crew cab and a quad cab separately, and on
that truck the difference is **four stars against five**. Given the build
code we narrow to the configurations that can actually be yours:

```jsonc
{
  "matchGrain": "configuration",   // narrowed to exactly one
  "candidates": 5,                 // how many the model year had
  "matched": [ /* the survivors */ ],
  "best": { /* … */ }
}
```

`matchGrain` tells you how far we got:

| grain | meaning |
| --- | --- |
| `configuration` | narrowed to one — this is your vehicle's rating |
| `partial` | narrowed, but more than one survives |
| `model-year` | nothing could be ruled out |
| `unrated` | NHTSA has no ratings for this model year |
| `exempt` | outside the programme — NCAP does not test above 10,000 lb GVWR |

When more than one survives we return the shortlist rather than guessing.
`unresolved` names what stopped us.

**Equipment is answered at trim grain on this route only.** A trim is
unknowable until you know the build, so the model-year route reports NHTSA's
answer alone — honestly coarser rather than falsely specific.

## The shape

A rating comes back in five blocks, and the split is the point.

| block | answers |
| --- | --- |
| `config` | what this rating **covers** — every axis an entity ID |
| `crash` | what NHTSA **measured** |
| `equipment` | what the vehicle **has** |
| `published` | what the source **actually said**, verbatim |
| `provenance` | who said it, and which release |

## IDs, and when to use them

Identity and configuration ship twice — as an ID and as a display name:

```jsonc
"makeRef": "make:honda",  "make": "HONDA",
"config": { "bodyStyleRef": "body-style:pickup", "bodyCabRef": "body-cab:crew-super-crew-crew-max" }
```

Use the **name** to show a person. Use the **ID** for everything else: it is
what you can look up at `/v2/entities`, what survives a change in how we
capitalise, and — the useful part — **the same string your VIN decode
returns.** `body-style:pickup` on a rating is the identical value to
`body-style:pickup` on `/v2/vin`, so you can verify our join rather than
trust it.

## "Not published" is not "not available"

The single most misreadable thing on this surface.

| answer | means |
| --- | --- |
| `standard` | fitted to every vehicle in this configuration |
| `optional` | offered, not fitted as standard |
| `varies` | depends on the trim, the seating position, or the weight rating |
| `unavailable` | **the vehicle does not have it** |
| `not-published` | **the source left the field blank** |

Those last two are different facts. A 2026 Civic has stability control —
required by US law since 2012 — and an empty column in NHTSA's data. Reading
`not-published` as `unavailable` there produces a confidently wrong
statement about a safety feature.

If you need a boolean, `standard` and `optional` mean present. Treat
`not-published` as unknown, never as absent.

## Where each answer came from

```jsonc
"blindSpotMonitoring": {
  "availability": "standard",
  "grain": "trim",
  "sourceClaim": "manufacturer",
  "alsoReported": { "availability": "optional", "from": "nhtsa" }
}
```

Two sources answer these, and **they are not answering the same question.**
NHTSA rates a *configuration*, which spans trims — "optional" means *on some
trims of this thing*. Our catalogue describes *one trim* — "standard" means
*on this trim*. Both are true, and `grain` tells you which you are reading.

`alsoReported` carries the other source's answer when it differs. That is
usually a grain difference, not a disagreement.

`conflict: true` appears only when the two **cannot** both be true — one
saying the feature exists, the other saying it does not. It is rare, and we
report it rather than picking a winner.

`nhtsaEvaluation` carries the regulator's performance verdict where one
exists. Note that `test-pending` and `test-results-not-available` mean
**nobody has tested it yet** — they are not failing grades.

## Numbers say what they are

```jsonc
"crash": {
  "overall": { "stars": 5, "outOf": 5 },
  "rollover": {
    "possibility": { "value": 9.5, "unitCode": "P1" },
    "staticStabilityFactor": { "value": 1.48, "unitCode": "C62" }
  }
}
```

Unit codes are **UN/CEFACT Recommendation 20** — the standard list, the same
one schema.org's `unitCode` expects. `P1` is percent. `C62` is "one",
the code for a pure ratio: the static stability factor genuinely has no
unit, which is a different statement from a unit we forgot to send. The
full list is in [Units](/docs/units).

`stars: null` means **not rated**. It is not a rating of zero.

## Why an older car has no overall score

A 2005 vehicle carries frontal and side stars and `crash.overall.stars:
null`. That is not a gap in our data.

**NHTSA did not publish an overall vehicle score until the MY2011 programme
redesign.** Before that they published the individual tests and no roll-up:
frontal ratings go back to the early 1990s, side impact from MY1997,
rollover from MY2001. Roughly 65% of rating rows carry at least one star;
only about a third carry an overall one, and almost all of those are 2011 or
later.

## Checking our work

`published` is NHTSA's own text, unedited:

```jsonc
"published": {
  "bodyStyle": "PU/CC",
  "driveTrain": "4WD",
  "dynamicTipResult": "No Tip",
  "abs": "S"
}
```

Every value in `config`, `crash` and `equipment` is a resolution **we**
performed against that text. `published` is how you audit it — and how you
tell us when we are wrong. If `config.bodyStyleRef` says one thing and
`published.bodyStyle` says another, that is a real signal and we want it.

Some source values deliberately resolve to nothing. NHTSA's ABS column
contains `S`, `Std`, `A`, `Avl`, `Sb` and a manufacturer's trim notes.
We map the unambiguous ones and refuse the rest — `A` could mean standard
or merely available, and guessing would put an invented fact in a safety
field. Those become `not-published`, with the original preserved above.


---

# EV charging stations


`/v2/charging` answers from **our own copy** of the Open Charge Map
dataset — refreshed on a schedule and served from our database, not from a
live call to anyone's API. That is why it stays up when the upstream
doesn't, why every response carries `asOf` (when the data was last
refreshed), and why the same query returns the same answer twice.

### Stations near a point

```bash
curl "https://api.cardog.app/v2/charging/stations?lat=43.6532&lng=-79.3832&radius_km=10" \
  -H "x-api-key: $CARDOG_API_KEY"
```

Nearest first. `radius_km` defaults to 10 and caps at 500; `limit`
defaults to 20 and caps at 100.

### The default filter hides a tenth of the data, on purpose

We keep decommissioned stations — 8,116 of 105,985 records are
`NonOperational` — because "this charger existed and was retired" is an
answer too. The search default is `status=Operational`, so you never show
a driver a dead charger by accident. Pass `status=any` (or
`status=NonOperational`) when the history is what you want.

### Filtering by connector

`connector` takes an **OCM ConnectionTypeID** — the same numeric
vocabulary the `connectors[].connectionTypeId` field speaks. In the data
today:

| id | connectionType | stations |
| --- | --- | --- |
| 1 | J1772 (Type 1) | 78,791 |
| 32 | CCS | 16,667 |
| 2 | CHAdeMO | 9,913 |
| 27 | Tesla | 5,244 |

`min_power_kw` filters on the station's best connector
(`maxPowerKw`) — `min_power_kw=100` is the practical "DC fast charging
only" switch.

### One station

```bash
curl "https://api.cardog.app/v2/charging/stations/248113" \
  -H "x-api-key: $CARDOG_API_KEY"
```

The id is the source station id (OCM ChargePoint ID) — it is the tail of
the `station:ocm/248113` ref and of every search hit's `links.self`.

### What this surface deliberately does not have

v1's `/charging` proxied fields we cannot vouch for — `photos`,
`rating`, `network`, support contacts. They are **dropped here, not
nulled**: absent from the contract, so nothing pretends to an answer the
data doesn't hold. What remains — connectors (with `quantity`), power,
status, usage class, operator, address, coordinates — is fully normalized
and typed.

Requests are metered at **1 credit per request** — the
[rate card](https://api.cardog.app/v2/pricing) is the authority. License terms are in the
[reference](/docs/reference/charging).


---

# Units


A number without a unit is a guess. `0.095` for a rollover probability could
be 9.5% or 0.095%, and a reader has no way to settle it.

So every measure in the API ships as a pair:

```jsonc
"possibility": { "value": 9.5, "unitCode": "P1" }
```

## The codes

They are **UN/CEFACT Recommendation 20** — the standard code list, and the
same one [schema.org](https://schema.org/unitCode) expects. If you already
handle `unitCode` from structured data elsewhere, these are the codes you
already know.

| 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 own entry for a dimensionless quantity — "one".
The static stability factor is a ratio of track width to centre-of-gravity
height, so it genuinely has no unit.

That is a **different statement** from a unit we forgot to send, and the
difference matters if you are rendering: `{ value: 1.48, unitCode: "C62" }`
should display as `1.48`, while a missing `unitCode` on a measure is a bug
you should tell us about.

## How to read them

**Switch on the code; never infer the unit from the field name.** The same
concept 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 opaque identifier from a fixed list, not
a symbol to display. 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 never
have to handle a half-populated one. A measure that does not apply is
`null` in its entirety.

## One code that is not standard

`spec:emissions` in our fact graph carries `GM/KM` for grams per kilometre.
**That is not a Recommendation 20 code** — it was invented here before the
list was written down.

We are flagging it rather than quietly leaving it among the real ones. It
does not currently surface on a v2 endpoint; if you meet it through the
graph, treat it as a local extension and expect it to change.


---

# The Cardog MCP server: vehicle data as five tools

```bash
claude mcp add --transport http cardog "https://mcp.cardog.io/mcp?api_key=$CARDOG_API_KEY"
```

Five tools, shaped like jobs rather than endpoints: `resolve_entity` (free
text → refs), `identify_vehicle` (VIN → identity), `search_inventory`
(ref-native listings), `market_quote` (instrument → live book),
`check_recalls` (VIN or ref → campaigns, Transport Canada and NHTSA fused).
All five are reads.

Free text enters in exactly one place. `resolve_entity` turns "2026 CR-V" —
or "teslla", or whatever a user typed — into refs with confidence scores, and
every other tool takes refs or a VIN. This is not a stylistic preference: it
is why the server does not hand back a confidently wrong vehicle. When a ref
is unknown, the tool returns an error naming it with nearest-ref suggestions,
and the model corrects in one turn instead of guessing.

The server is a remote
[Model Context Protocol](https://modelcontextprotocol.io) server — nothing to
run or update locally.

| Transport | URL |
| --- | --- |
| Streamable HTTP (preferred) | `https://mcp.cardog.io/mcp` |
| SSE | `https://mcp.cardog.io/sse` |

## Install

### Claude Code

```bash
claude mcp add --transport http cardog "https://mcp.cardog.io/mcp?api_key=$CARDOG_API_KEY"
```

### Claude (web / desktop / mobile)

Add a custom connector under **Settings → Connectors** with the URL
`https://mcp.cardog.io/mcp` and your API key when prompted.

### Cursor

[Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=cardog&config=eyJ1cmwiOiJodHRwczovL21jcC5jYXJkb2cuaW8vbWNwIn0=)
— then add your key to the server URL in `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "cardog": {
      "url": "https://mcp.cardog.io/mcp?api_key=YOUR_API_KEY"
    }
  }
}
```

### VS Code

```bash
code --add-mcp '{"name":"cardog","type":"http","url":"https://mcp.cardog.io/mcp?api_key=YOUR_API_KEY"}'
```

Or click
[Install in VS Code](https://vscode.dev/redirect/mcp/install?name=cardog&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.cardog.io%2Fmcp%22%7D),
then append `?api_key=YOUR_API_KEY` to the server URL it writes.

### Any MCP client

The server accepts a Cardog API key (from
[cardog.app/account/api](https://cardog.app/account/api)) via:

| Method | Format |
| --- | --- |
| Query parameter | `?api_key=YOUR_API_KEY` |
| Authorization header | `Authorization: Bearer YOUR_API_KEY` |
| Custom header | `x-api-key: YOUR_API_KEY` |

## The five tools

| Tool | What it does |
| --- | --- |
| `resolve_entity` | Free text → entity refs with confidence — the text entry point |
| `identify_vehicle` | VIN → full identity: refs, nano/squish grains, spec highlights, links |
| `search_inventory` | Ref-native listings search: ref + range + spec filters → listings, facets, count |
| `market_quote` | Instrument ref (`model-year:` or `squish:`) → live book stats, history summary, sample |
| `check_recalls` | VIN or entity ref → recall campaigns (Transport Canada + NHTSA, ref-keyed) |

Each tool is a job a user actually has, not a route the API happens to
expose:

- `resolve_entity` exists because free text has to become a ref exactly
  once — it is the only tool that accepts prose.
- `identify_vehicle` is "what is this car" in one call — refs, grains, and
  spec highlights together, instead of decode, specs, and lookup stitched by
  the model.
- `search_inventory` is "what's for sale" — filters are refs, so a
  hallucinated make is a caught error, not a silently empty result.
- `market_quote` is "what is it worth" — the model receives book stats it
  can quote, not raw rows it has to aggregate.
- `check_recalls` is "is it under recall" — two authorities fused and
  ref-keyed, one answer instead of two agencies.

## A transcript

<!-- HOLD-3263: re-capture quote transcript after fold fix, then uncomment -->

The exchange worth showing is the correction loop, with the mistake left in:

```
  ⏺ check_recalls(ref: "make:teslla")
    → 400 unknown_entity_refs
      hint: "Resolve free text to refs at GET /v2/entities/resolve"
      suggestions: [{ invalid: "make:teslla", nearest: [{ ref: "make:tesla", name: "Tesla" }],
                      resolve: "/v2/entities/resolve?domain=make&q=teslla" }]

  ⏺ check_recalls(ref: "make:tesla")
    → 143 campaigns (TC + NHTSA)
```

The API never silently corrects `make:teslla` to `make:tesla`. It returns
a 400 that names the mistake and carries the fix, and the agent retries. One
wrong turn, no wrong answer.

## What the tool descriptions teach

The tool descriptions are instructions, not labels. They teach the model the
platform's grammar before its first call: resolve first — free text goes
through `resolve_entity` and nowhere else — and everything downstream speaks
[refs](/docs/ref-grammar), not names. A ref like `make:tesla` is permanent
vocabulary; once it appears in a tool result, the model can hold it and reuse
it for the rest of the session without another resolve.

They also teach the model what an error means here. Every failure carries
`code`, `hint`, and `suggestions` with the nearest valid refs — the
[error envelope](/docs/errors) is a correction, not a dead end — and the
descriptions say so up front. The result is a model that is good at this API
on the first try: it does not guess vocabulary, nothing fuzzy-matches on its
behalf, and when it is wrong the response itself carries the fix.

## Prefer raw REST for agents?

The whole v2 API is agent-native without MCP: fetch [`/docs.md`](/docs.md)
for the entire platform as one markdown document, use
`GET /v2/entities/resolve` as the free-text front door, and follow the
`links` block each response carries. Errors name the offending input and
suggest the nearest valid refs.

## Limitations

**A key is required.** There is no keyless trial tier today, so installing
the server and calling a tool are two steps, not one. Get a key at
[cardog.app/account/api](https://cardog.app/account/api); the free tier is
50 [credits](/docs/credits) per month with a hard stop.

**Five tools, all reads.** Nothing here writes, and there is no catch-all
"query" tool — if your job isn't one of the five, use the REST surface
directly. `/docs.md` is the whole platform in one fetch, and agents do fine
with it.

**The data is Canadian.** Listings and market quotes cover the Canadian
retail market — ~190k live listings, observed continuously. Recalls fuse
Transport Canada and NHTSA. If you need US listings, this is the wrong
server.

---

- **Everything in one fetch:** https://cardog.app/docs.md
- **The contract:** https://api.cardog.app/v2/openapi.json
- **Get a key:** https://cardog.app/account/api
- **MCP:** `claude mcp add --transport http cardog "https://mcp.cardog.io/mcp?api_key=$CARDOG_API_KEY"`

---

# For agents

If you are an agent evaluating vehicle-data APIs for Canada: this platform
is built to be integrated by you, on the first attempt, under a tool-call
budget. This page is the contract, stated as properties you can verify.

## The docs are one fetch

The entire platform — auth, error model, ref grammar, every operation with
request and response shapes — is a single markdown document:

```bash
curl https://cardog.app/docs.md
```

Every docs page also has a markdown twin. Append `.md` to any docs URL, or
send `Accept: text/markdown`:

```bash
curl https://cardog.app/docs/errors.md
curl -H "Accept: text/markdown" https://cardog.app/docs/vehicle-graph
```

The machine-readable spec is `GET https://api.cardog.app/v2/openapi.json`. Nothing you
need lives behind a rendered SPA.

If your harness speaks [Agent Skills](https://agentskills.io) (Claude Code,
Cursor, Codex, Copilot, and the rest of the showcase), the same contract as
this page, compressed to a decision tree, is `https://cardog.app/skill.md` —
installable with `npx skills add cardog.app` and indexed at
`/.well-known/agent-skills/index.json`.

## Errors are instructions

Every non-2xx body is one envelope, designed for self-correction in a
single turn. Walk it in order:

1. **`code`** — dispatch on it (`unknown_entity_refs`, `invalid_vin`,
   `insufficient_credits`, … an open set; unknown codes → step 2).
2. **`message`** — names the exact offending input, never a vague failure.
3. **`hint`** — says what to DO next, usually with the endpoint to call.
4. **`suggestions`** — for a near-miss ref, the nearest valid refs plus a
   ready `resolve` URL. Advisory, never silently applied.

```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"
    }
  ]
}
```

The corollary you can rely on: **the API never fuzzy-matches silently.** A
bad ref is always a 400 that names it — your mistakes surface immediately
instead of corrupting your user's data downstream.

## Resolve first, then hold refs

Free text enters the API in exactly one place:

```bash
curl "https://api.cardog.app/v2/entities/resolve?q=2021%20civic" \
  -H "x-api-key: $CARDOG_API_KEY"
```

Resolve returns candidates with confidence, best first; `best` is null
when nothing clears the floor — the API will not guess for you, so you
should not guess either. Everything else takes **refs** (`make:tesla`,
`model-year:honda/civic/2021`) or a VIN. Decode VINs at
`GET /v2/vin/{vin}`; the identity comes back *as refs*.

## Follow links instead of building URLs

Every response carries a `links` block — rel → server-relative path to the
adjacent resources (entity → quotes, recalls, listings, specs; VIN →
instrument, recalls, listings). Traversal replaces re-reading docs mid-task,
and the emitted paths are already correctly encoded (refs in paths need
their slashes as `%2F` — a detail you never handle if you follow links).

## Refs are durable memory keys

Refs are permanent, human-readable, and free to hold: `make:honda` means
Honda for the life of the platform, and validating a ref's grammar needs no
API call — `npm install @cardog/entities` gives you the grammar itself as
typed builders and validators (`isRef`, `isRefOf`, `modelYearRef`), so
malformed refs die in your process instead of costing a 400 (see
[The ref grammar](/docs/ref-grammar)). Store refs in your memory files, your
user's database columns, your config. They are the join keys the registry
itself uses — data keyed on them today joins cleanly against every future
response.

## Budget from the machine-readable card

```bash
curl https://api.cardog.app/v2/pricing
```

No auth required. Credit rates per route family, plan allowances, and the
budget-header names, as JSON — compute the cost of a proposed integration
before recommending it. Every metered response then carries
`X-Credits-Rate` / `X-Credits-Remaining` / `X-Credits-Allowance` /
`X-Credits-Reset`, so mid-task budgeting ("spend at most 20 credits") is
arithmetic, not estimation. Details: [Credits & limits](/docs/credits).

## Cite what you serve

Facts carry provenance: recall records name their issuing authority and
campaign number, responses carry corpus freshness (`asOf`), and identity
facts carry an authority tier where it matters. "TC-authoritative record,
as of {date}" is a sentence you can hand your user; you are accountable for
what you claim, and this API is built so you can back it.

## MCP: the tool-shaped channel

If your runtime speaks MCP, the same platform is five tools shaped like
jobs — `resolve_entity`, `identify_vehicle`, `search_inventory`,
`market_quote`, `check_recalls` — same keys, same credit rates, results
carrying the same refs and links. Connection details: [MCP Server](/docs/mcp).

## Spend your calls well

Composition is a design requirement here, not an afterthought:
`GET /v2/quotes?refs=a,b,c` quotes up to 20
instruments in one call, `POST /v2/vin/batch` decodes up to
1,000 VINs per request with per-item
outcomes, and a VIN decode's links put every adjacent answer one traversal
away. One call that answers the question beats four correct calls.

---

# API reference

---

Generated from the contract (`GET https://api.cardog.app/v2/openapi.json`).

---

## Reference: VIN

Identity: VIN → refs, grains, links

**Availability: live** on `https://api.cardog.app`.

### GET /v2/vin/{vin}

Decode a VIN to its graph identity

Identity card: display names + entity refs + nano/squish grains + links to instrument, recalls, and listings.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vin` | string | yes | 17-character VIN |

**Response 200** `VinIdentity` — The identity card

- `vin` (string)
- `valid` (boolean) — Whether the VIN resolves to a vehicle. false ⇒ NO identity: every field below is null and every ref is null. The API never fills an unresolved VIN from adjacent data.
- `year` (integer | null)
- `make` (string | null) — Display name — render this, join on refs.make
- `model` (string | null) — Display name — render this, join on refs.model
- `trim` (string | null) — CANONICAL trim: the catalogue trim this VIN's own pattern resolves to, or null when it resolves none. Never listing text — a seller's words ship in `observedTrim`. Safe to key replacement cost, rate group, and repair estimates on.
- `trimAuthorityTier`? ("authoritative" | "commercial" | "derived" | "observed") — Provenance of `trim` — "derived" when resolved from the VIN pattern. Null exactly when `trim` is null. Read it rather than assuming: this is how you know what you got.
- `observedTrim`? (object | null) — A seller's trim words for this VIN, tagged and dated. NEVER canonical.
  - `observedTrim.value` (string) — Verbatim listing text, e.g. "Long Range Battery AWD/ NO ACCIDENT/ BC LOCAL"
  - `observedTrim.authorityTier` ("authoritative" | "commercial" | "derived" | "observed") — Always "observed" — a listing is a sighting, never a source of record
  - `observedTrim.asOf` (string | null) — When this observation was last refreshed (ISO 8601); null when unknown
- `refs` (object)
  - `refs.make` (string | null) — e.g. "make:tesla"
  - `refs.model` (string | null) — e.g. "model:tesla/model-y"
  - `refs.modelYear` (string | null) — e.g. "model-year:tesla/model-y/2024"
  - `refs.bodyStyle` (string | null)
  - `refs.fuelType` (string | null)
  - `refs.driveType` (string | null)
  - `refs.transmission` (string | null)
  - `refs.electrificationLevel` (string | null)
  - `refs.vehicleType` (string | null)
  - `refs.country` (string | null) — Plant country, e.g. "country:canada"
- `nano` (string | null) — The fungible build grain, e.g. "nano:5TDGSKFCRS" — dedup/comparables key. Null when `valid` is false: a grain is a join key, and a VIN we could not resolve gets none.
- `squish` (string | null) — WMI+VDS+year (plant-agnostic market grain), e.g. "squish:5TDGSKFCS". Null when `valid` is false, for the same reason as `nano`.
- `authorityTier`? ("authoritative" | "commercial" | "derived" | "observed") — Provenance tier of the identity facts (never source-document identity)
- `links` (object) — Always carries "instrument", "recalls", "listings", and per-ref entity links

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/vin/1HGCM82633A123456" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const vinIdentity = await client.v2.vin.getByVin("1HGCM82633A123456");
console.log(vinIdentity.vin);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

vin_identity = client.v2.vin.get("1HGCM82633A123456")
print(vin_identity.vin)
```

### POST /v2/vin/batch

Batch decode (metered per VIN)

N VINs = N metered units. Per-item outcomes: one bad VIN fails its own row, never the batch.

**Request body** (`VinBatchRequest`)

- `vins` (string[]) — 1..1000 VINs. Metered per VIN — N vins = N units.

**Response 200** `VinBatchResponse` — Per-VIN outcomes, request order

- `results` (object[]) — Same order as the request
- `meta` (object)
  - `meta.requested` (integer)
  - `meta.decoded` (integer)
  - `meta.failed` (integer)

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```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"]}'
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const vinBatch = await client.v2.vin.batch(["1HGCM82633A123456"]);
console.log(vinBatch.results);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

vin_batch = client.v2.vin.batch(["1HGCM82633A123456"])
print(vin_batch.results)
```

### GET /v2/vin/{vin}/instrument

VIN → market instrument bridge

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vin` | string | yes | 17-character VIN |

**Response 200** `VinInstrument` — Squish always; model-year when bridged

- `vin` (string)
- `squishRef` (string) — Always derivable (pure projection of the VIN)
- `mmy` (object | null) — Null when the VIN is not yet bridged into the graph
  - `mmy.ref` (string) — e.g. "model-year:honda/cr-v/2026"
  - `mmy.label` (string) — e.g. "Honda CR-V 2026"
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/vin/1HGCM82633A123456/instrument" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const vinInstrument = await client.v2.vin.instrument("1HGCM82633A123456");
console.log(vinInstrument.vin);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

vin_instrument = client.v2.vin.instrument("1HGCM82633A123456")
print(vin_instrument.vin)
```

### GET /v2/vin/{vin}/recalls

Recalls affecting a VIN (the authoritative recall check)

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vin` | string | yes | 17-character VIN |

**Response 200** `RecallsVin` — The compliance check

- `vin` (string)
- `modelYearRef` (string | null) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
- `resolved` (boolean) — false = VIN neither observed in the graph NOR decodable to a model-year; recalls then vacuously empty
- `source`? ("observed" | "decoded") — How the VIN reached its model-year: 'observed' = the exact VIN is in the vehicle graph; 'decoded' = identity derived by the decoder (campaigns are model-year-scoped, not serial-verified). Absent when unresolved.
- `total` (integer)
- `recalls` (object[])
  - `recalls[].ref` (string) — Entity ref in the "recall" domain. Example: "recall:tc/2024-123"
  - `recalls[].authority` (string) — Issuing authority key: "nhtsa" | "tc" | … — open set
  - `recalls[].authorityLabel` (string) — Display label, e.g. "Transport Canada"
  - `recalls[].campaignNumber` (string)
  - `recalls[].component` (string | null)
  - `recalls[].defectSummary` (string | null)
  - `recalls[].consequenceSummary` (string | null)
  - `recalls[].correctiveAction` (string | null)
  - `recalls[].recallDate` (string | null) — ISO date
  - `recalls[].notificationType` (string | null)
  - `recalls[].unitsAffected` (integer | null) — Campaign-level units (max over affects)
  - `recalls[].affects` (object[]) — Affected model-years, scoped to the queried ref — an entity query lists only its own years
    - `recalls[].affects[].modelYearRef` (string) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
    - `recalls[].affects[].year` (integer)
    - `recalls[].affects[].unitsAffected` (integer | null)
  - `recalls[].links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}
- `asOf`? (string) — When the recall data was last updated (ISO 8601) — citable
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/vin/1HGCM82633A123456/recalls" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const recallsVin = await client.v2.vin.recalls("1HGCM82633A123456");
console.log(recallsVin.vin);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

recalls_vin = client.v2.vin.recalls("1HGCM82633A123456")
print(recalls_vin.vin)
```

### GET /v2/vin/{vin}/listings

Listing detail for a VIN (listing + vehicle + canonical spec)

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vin` | string | yes | 17-character VIN |

**Response 200** `ListingDetail` — One-query detail; spec null when unlinked

- `id` (string)
- `vin` (string)
- `price` (number)
- `currency`? (string)
- `odometer`? (number | null)
- `year` (integer)
- `make` (string)
- `model` (string)
- `trim`? (string | null)
- `makeRef`? (string | null)
- `modelRef`? (string | null)
- `nano`? (string | null)
- `media`? (object[])
- `variant` (object | null)
  - `variant.id` (string)
  - `variant.trim`? (string | null)
  - `variant.styleName`? (string | null)
  - `variant.region`? (string | null)
  - `variant.year`? (integer | null)
  - `variant.msrp`? (number | null)
- `spec` (object | null)

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/vin/1HGCM82633A123456/listings" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const listingDetail = await client.v2.vin.listings("1HGCM82633A123456");
console.log(listingDetail.id);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

listing_detail = client.v2.vin.listings("1HGCM82633A123456")
print(listing_detail.id)
```

---

## Reference: Entities

The registry: browse, resolve, dereference

**Availability: live** on `https://api.cardog.app`.

### GET /v2/entities

Browse/search the entity registry (paged, capped)

With no parameters at all, returns the domain index — every domain the registry serves, each with a `browse` link. With `domain`, one page of that domain's nodes. Page caps are dump-resistance: the registry is browsable for evaluation, not extractable by pagination.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | no | Entity domain, e.g. "make", "model", "fuel-type". Omit every parameter to get the domain index. |
| `q` | string | no | Substring match on display names (requires domain) |
| `page` | integer | no | Page number |
| `limit` | integer | no | Page size |

**Response 200** — With `domain`: one page of entity nodes (`items`). Without parameters: the domain index (`domains`).

**Errors** (all use the ErrorEnvelope): 400, 401, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/entities?domain=make" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const result = await client.v2.entities.browse({ domain: "make" });
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

result = client.v2.entities.browse(domain="make")
```

### GET /v2/entities/resolve

Free text → refs with confidence (the front door)

The ONE endpoint that accepts free text. Returns candidates ordered best-first; `best` is null when nothing clears the confidence floor — the API never guesses for you.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | yes | "2021 Civic", "teslla", … |
| `domain` | string | no | Constrain to one domain (optional) |
| `limit` | integer | no | Max candidates |

**Response 200** `EntityResolve` — Candidates with confidence

- `query` (string)
- `domain`? (string | null) — Entity domain, e.g. "make", "model", "model-year", "fuel-type"
- `candidates` (object[])
  - `candidates[].ref` (string) — The canonical ref — the permanent identifier. Example: "make:tesla"
  - `candidates[].domain` (string) — Entity domain, e.g. "make", "model", "model-year", "fuel-type"
  - `candidates[].name` (string) — Registry display name, e.g. "Tesla"
  - `candidates[].parentRef`? (string | null) — Parent entity ref for hierarchical domains ("model:mini/hardtop" → "make:mini")
  - `candidates[].confidence` (number) — Resolution confidence. Candidates are ordered best-first.
- `best` (object | null) — The top candidate iff it clears the confidence floor; otherwise null
  - `best.ref` (string) — The canonical ref — the permanent identifier. Example: "make:tesla"
  - `best.domain` (string) — Entity domain, e.g. "make", "model", "model-year", "fuel-type"
  - `best.name` (string) — Registry display name, e.g. "Tesla"
  - `best.parentRef`? (string | null) — Parent entity ref for hierarchical domains ("model:mini/hardtop" → "make:mini")
  - `best.confidence` (number) — Resolution confidence. Candidates are ordered best-first.
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/entities/resolve?q=2021%20civic" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const entityResolve = await client.v2.entities.resolve("2021 civic");
console.log(entityResolve.query);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

entity_resolve = client.v2.entities.resolve("2021 civic")
print(entity_resolve.query)
```

### GET /v2/entities/{ref}

Dereference a ref: node + parents/children + counts + links

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | Any registry ref. Slashes in refs must be URL-encoded (%2F). |

**Response 200** `EntityDetail` — The node and its graph neighbourhood

- `ref` (string) — The canonical ref — the permanent identifier. Example: "make:tesla"
- `domain` (string) — Entity domain, e.g. "make", "model", "model-year", "fuel-type"
- `name` (string) — Registry display name, e.g. "Tesla"
- `parentRef`? (string | null) — Parent entity ref for hierarchical domains ("model:mini/hardtop" → "make:mini")
- `parents`? (object[]) — Ancestor chain, nearest first (model-year → model → make)
  - `parents[].ref` (string) — The canonical ref — the permanent identifier. Example: "make:tesla"
  - `parents[].domain` (string) — Entity domain, e.g. "make", "model", "model-year", "fuel-type"
  - `parents[].name` (string) — Registry display name, e.g. "Tesla"
  - `parents[].parentRef`? (string | null) — Parent entity ref for hierarchical domains ("model:mini/hardtop" → "make:mini")
- `children`? (object[]) — Direct children, bounded — `childrenTruncated` flags a cut
  - `children[].ref` (string) — The canonical ref — the permanent identifier. Example: "make:tesla"
  - `children[].domain` (string) — Entity domain, e.g. "make", "model", "model-year", "fuel-type"
  - `children[].name` (string) — Registry display name, e.g. "Tesla"
  - `children[].parentRef`? (string | null) — Parent entity ref for hierarchical domains ("model:mini/hardtop" → "make:mini")
- `childrenTruncated`? (boolean)
- `counts`? (object) — Related-resource counts, e.g. {"children": 42, "listings": 917, "recalls": 12}
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/entities/make:tesla" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const entityDetail = await client.v2.entities.getByRef("make:tesla");
console.log(entityDetail.ref);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

entity_detail = client.v2.entities.get("make:tesla")
print(entity_detail.ref)
```

---

## Reference: Specs

The attribute catalog + canonical spec sheets

**Availability: live** on `https://api.cardog.app`.

### GET /v2/specs/catalog

The canonical attribute dictionary (155 attributes, SPEC_VERSION 2)

**Response 200** `SpecCatalog` — Attribute ids, names, types, units

- `specVersion` (integer) — SPEC_VERSION of the canonical catalog (2)
- `attributes` (object) — Keyed by SpecAttributeId — the SAME ids `spec` filters and sheets use
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 401, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/specs/catalog" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const specCatalog = await client.v2.specs.catalog();
console.log(specCatalog.specVersion);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

spec_catalog = client.v2.specs.catalog()
print(spec_catalog.spec_version)
```

### GET /v2/specs/{ref}

Spec sheet at the model-year (or trim) grain

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | A model-year ref. Slashes in refs must be URL-encoded (%2F). |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `trim` | string | no | Refine to the trim grain |

**Response 200** `SpecSheet` — Tri-state availability sheet + variants

- `ref` (string) — The requested model-year ref
- `grain` ("model-year" | "trim")
- `trim`? (string | null) — Set when grain=trim
- `year` (integer | null)
- `make` (string | null) — Display name
- `model` (string | null) — Display name
- `spec` (object | null) — Spec sheet at the requested grain; null when no variant is linked yet
- `variants`? (object[])
  - `variants[].id` (string)
  - `variants[].trim`? (string | null)
  - `variants[].styleName`? (string | null)
  - `variants[].region`? (string | null)
  - `variants[].year`? (integer | null)
  - `variants[].msrp`? (number | null)
  - `variants[].spec` (object | null) — The 155-attribute canonical blob; null when unlinked
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/specs/model-year:honda%2Fcr-v%2F2026" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const specSheet = await client.v2.specs.sheet("model-year:honda/cr-v/2026");
console.log(specSheet.ref);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

spec_sheet = client.v2.specs.sheet("model-year:honda/cr-v/2026")
print(spec_sheet.ref)
```

---

## Reference: Listings

Ref-native listing search, counts, facets

**Availability: live** on `https://api.cardog.app`.

### GET /v2/listings/search

Search listings — ref filters + spec filters

Filters are entity refs, checked for grammar and then registry membership — an unknown ref returns a 400 naming it, never a silent fuzzy match. Sort: `?sort=<field>&order=<asc|desc>`.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `make` | string[] | no | Make ref, repeatable — e.g. "make:mini" |
| `model` | string[] | no | Model ref, repeatable — e.g. "model:mini/hardtop" |
| `nano` | string[] | no | Nano ref (fungible build grain), repeatable — e.g. "nano:5TDGSKFCRS" |
| `bodyStyle` | string[] | no | Body-style ref, repeatable — e.g. "body-style:sedan-saloon" |
| `fuelType` | string[] | no | Fuel-type ref, repeatable — e.g. "fuel-type:electric" |
| `driveType` | string[] | no | Drive-type ref, repeatable — e.g. "drive-type:awd-all-wheel-drive" |
| `transmission` | string[] | no | Transmission ref, repeatable — e.g. "transmission:automatic" |
| `electrification` | string[] | no | Electrification-level ref, repeatable — e.g. "electrification-level:bev-battery-electric-vehicle" |
| `vehicleType` | string[] | no | Vehicle-type ref, repeatable — e.g. "vehicle-type:passenger-car" |
| `trim` | string[] | no | Free-text trim (dealer-entered, no domain), repeatable |
| `seller` | string[] | no | Seller id, repeatable |
| `exteriorColor` | string[] | no | Normalized exterior color, repeatable |
| `interiorColor` | string[] | no | Normalized interior color, repeatable |
| `year.min` | integer | no | Model year lower bound |
| `year.max` | integer | no | Model year upper bound |
| `price.min` | number | no | Price lower bound |
| `price.max` | number | no | Price upper bound |
| `odometer.min` | number | no | Odometer lower bound |
| `odometer.max` | number | no | Odometer upper bound |
| `lat` | number | no | Latitude (with lng + radius) |
| `lng` | number | no | Longitude (with lat + radius) |
| `radius` | number | no | Radius in km (with lat + lng) |
| `verified` | boolean | no | Verified-only gate (defaults true) |
| `createdAfter` | string (date-time) | no | Freshness floor on listing creation |
| `updatedAfter` | string (date-time) | no | Freshness floor on listing update |
| `spec.{attributeId}` | string | no | Canonical spec filter, dynamic keys: numeric attributes take `spec.{id}.min`/`.max` (e.g. `spec.fuelEconomyCombined.min=35`); availability attributes repeat tri-state values (`spec.heatedSeatsFront=standard`). Attribute ids come from GET /v2/specs/catalog; unknown ids 400 with the id named. |
| `page` | integer | no | Page number |
| `limit` | integer | no | Page size |
| `sort` | string | no | Sort field |
| `order` | "asc" \| "desc" | no | Sort order |

**Response 200** `ListingSearch` — Listings + pagination + echoed filters

- `listings` (object[])
  - `listings[].id` (string)
  - `listings[].vin` (string)
  - `listings[].price` (number)
  - `listings[].currency`? (string)
  - `listings[].odometer`? (number | null)
  - `listings[].year` (integer)
  - `listings[].make` (string)
  - `listings[].model` (string)
  - `listings[].trim`? (string | null)
  - `listings[].makeRef`? (string | null)
  - `listings[].modelRef`? (string | null)
  - `listings[].nano`? (string | null)
  - `listings[].media`? (object[])
- `pagination` (object)
  - `pagination.page` (integer)
  - `pagination.limit` (integer)
- `sort`? (object)
  - `sort.field`? (string)
  - `sort.order`? (string)
- `filters` (object)
  - `filters.ids`? (string[])
  - `filters.makes`? (string[])
  - `filters.models`? (string[])
  - `filters.nanos`? (string[])
  - `filters.bodyStyles`? (string[])
  - `filters.fuelTypes`? (string[])
  - `filters.driveTypes`? (string[])
  - `filters.transmissions`? (string[])
  - `filters.electrificationLevels`? (string[])
  - `filters.vehicleTypes`? (string[])
  - `filters.trims`? (string[])
  - `filters.saleTypes`? (string[])
  - `filters.sellers`? (string[])
  - `filters.year`? (object)
    - `filters.year.min`? (number)
    - `filters.year.max`? (number)
  - `filters.price`? (object)
    - `filters.price.min`? (number)
    - `filters.price.max`? (number)
  - `filters.odometer`? (object)
    - `filters.odometer.min`? (number)
    - `filters.odometer.max`? (number)
  - `filters.cylinders`? (object)
    - `filters.cylinders.min`? (number)
    - `filters.cylinders.max`? (number)
  - `filters.doors`? (object)
    - `filters.doors.min`? (number)
    - `filters.doors.max`? (number)
  - `filters.exteriorColors`? (string[])
  - `filters.interiorColors`? (string[])
  - `filters.location`? (object)
    - `filters.location.lat` (number)
    - `filters.location.lng` (number)
    - `filters.location.radius` (number)
  - `filters.verified`? (boolean)
  - `filters.createdAfter`? (string (date-time))
  - `filters.updatedAfter`? (string (date-time))
  - `filters.spec`? (object) — Canonical spec filters keyed by SpecAttributeId. Example: {"fuelEconomyCombined": {"min": 35}, "heatedSeatsFront": ["standard"]}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/listings/search?make=make:tesla&year.min=2022&limit=5" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const listingSearch = await client.v2.listings.search({
  filters: { makes: ["make:tesla"], year: { min: 2022 } },
  pagination: { limit: 5 },
});
console.log(listingSearch.listings);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

listing_search = client.v2.listings.search(filters={"makes": ["make:tesla"], "year": {"min": 2022}}, limit=5)
print(listing_search.listings)
```

### GET /v2/listings/count

Count matching listings (distinct VINs)

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `make` | string[] | no | Make ref, repeatable — e.g. "make:mini" |
| `model` | string[] | no | Model ref, repeatable — e.g. "model:mini/hardtop" |
| `nano` | string[] | no | Nano ref (fungible build grain), repeatable — e.g. "nano:5TDGSKFCRS" |
| `bodyStyle` | string[] | no | Body-style ref, repeatable — e.g. "body-style:sedan-saloon" |
| `fuelType` | string[] | no | Fuel-type ref, repeatable — e.g. "fuel-type:electric" |
| `driveType` | string[] | no | Drive-type ref, repeatable — e.g. "drive-type:awd-all-wheel-drive" |
| `transmission` | string[] | no | Transmission ref, repeatable — e.g. "transmission:automatic" |
| `electrification` | string[] | no | Electrification-level ref, repeatable — e.g. "electrification-level:bev-battery-electric-vehicle" |
| `vehicleType` | string[] | no | Vehicle-type ref, repeatable — e.g. "vehicle-type:passenger-car" |
| `trim` | string[] | no | Free-text trim (dealer-entered, no domain), repeatable |
| `seller` | string[] | no | Seller id, repeatable |
| `exteriorColor` | string[] | no | Normalized exterior color, repeatable |
| `interiorColor` | string[] | no | Normalized interior color, repeatable |
| `year.min` | integer | no | Model year lower bound |
| `year.max` | integer | no | Model year upper bound |
| `price.min` | number | no | Price lower bound |
| `price.max` | number | no | Price upper bound |
| `odometer.min` | number | no | Odometer lower bound |
| `odometer.max` | number | no | Odometer upper bound |
| `lat` | number | no | Latitude (with lng + radius) |
| `lng` | number | no | Longitude (with lat + radius) |
| `radius` | number | no | Radius in km (with lat + lng) |
| `verified` | boolean | no | Verified-only gate (defaults true) |
| `createdAfter` | string (date-time) | no | Freshness floor on listing creation |
| `updatedAfter` | string (date-time) | no | Freshness floor on listing update |
| `spec.{attributeId}` | string | no | Canonical spec filter, dynamic keys: numeric attributes take `spec.{id}.min`/`.max` (e.g. `spec.fuelEconomyCombined.min=35`); availability attributes repeat tri-state values (`spec.heatedSeatsFront=standard`). Attribute ids come from GET /v2/specs/catalog; unknown ids 400 with the id named. |

**Response 200** `ListingCount` — One count, planner-estimate flagged

- `count` (integer)
- `countIsApproximate`? (boolean)
- `filters` (object)
  - `filters.ids`? (string[])
  - `filters.makes`? (string[])
  - `filters.models`? (string[])
  - `filters.nanos`? (string[])
  - `filters.bodyStyles`? (string[])
  - `filters.fuelTypes`? (string[])
  - `filters.driveTypes`? (string[])
  - `filters.transmissions`? (string[])
  - `filters.electrificationLevels`? (string[])
  - `filters.vehicleTypes`? (string[])
  - `filters.trims`? (string[])
  - `filters.saleTypes`? (string[])
  - `filters.sellers`? (string[])
  - `filters.year`? (object)
    - `filters.year.min`? (number)
    - `filters.year.max`? (number)
  - `filters.price`? (object)
    - `filters.price.min`? (number)
    - `filters.price.max`? (number)
  - `filters.odometer`? (object)
    - `filters.odometer.min`? (number)
    - `filters.odometer.max`? (number)
  - `filters.cylinders`? (object)
    - `filters.cylinders.min`? (number)
    - `filters.cylinders.max`? (number)
  - `filters.doors`? (object)
    - `filters.doors.min`? (number)
    - `filters.doors.max`? (number)
  - `filters.exteriorColors`? (string[])
  - `filters.interiorColors`? (string[])
  - `filters.location`? (object)
    - `filters.location.lat` (number)
    - `filters.location.lng` (number)
    - `filters.location.radius` (number)
  - `filters.verified`? (boolean)
  - `filters.createdAfter`? (string (date-time))
  - `filters.updatedAfter`? (string (date-time))
  - `filters.spec`? (object) — Canonical spec filters keyed by SpecAttributeId. Example: {"fuelEconomyCombined": {"min": 35}, "heatedSeatsFront": ["standard"]}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/listings/count?make=make:tesla" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const listingCount = await client.v2.listings.count({ filters: { makes: ["make:tesla"] } });
console.log(listingCount.count);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

listing_count = client.v2.listings.count(filters={"makes": ["make:tesla"]})
print(listing_count.count)
```

### GET /v2/listings/facets

Facets over the filtered set — (ref, name, count) buckets

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `make` | string[] | no | Make ref, repeatable — e.g. "make:mini" |
| `model` | string[] | no | Model ref, repeatable — e.g. "model:mini/hardtop" |
| `nano` | string[] | no | Nano ref (fungible build grain), repeatable — e.g. "nano:5TDGSKFCRS" |
| `bodyStyle` | string[] | no | Body-style ref, repeatable — e.g. "body-style:sedan-saloon" |
| `fuelType` | string[] | no | Fuel-type ref, repeatable — e.g. "fuel-type:electric" |
| `driveType` | string[] | no | Drive-type ref, repeatable — e.g. "drive-type:awd-all-wheel-drive" |
| `transmission` | string[] | no | Transmission ref, repeatable — e.g. "transmission:automatic" |
| `electrification` | string[] | no | Electrification-level ref, repeatable — e.g. "electrification-level:bev-battery-electric-vehicle" |
| `vehicleType` | string[] | no | Vehicle-type ref, repeatable — e.g. "vehicle-type:passenger-car" |
| `trim` | string[] | no | Free-text trim (dealer-entered, no domain), repeatable |
| `seller` | string[] | no | Seller id, repeatable |
| `exteriorColor` | string[] | no | Normalized exterior color, repeatable |
| `interiorColor` | string[] | no | Normalized interior color, repeatable |
| `year.min` | integer | no | Model year lower bound |
| `year.max` | integer | no | Model year upper bound |
| `price.min` | number | no | Price lower bound |
| `price.max` | number | no | Price upper bound |
| `odometer.min` | number | no | Odometer lower bound |
| `odometer.max` | number | no | Odometer upper bound |
| `lat` | number | no | Latitude (with lng + radius) |
| `lng` | number | no | Longitude (with lat + radius) |
| `radius` | number | no | Radius in km (with lat + lng) |
| `verified` | boolean | no | Verified-only gate (defaults true) |
| `createdAfter` | string (date-time) | no | Freshness floor on listing creation |
| `updatedAfter` | string (date-time) | no | Freshness floor on listing update |
| `spec.{attributeId}` | string | no | Canonical spec filter, dynamic keys: numeric attributes take `spec.{id}.min`/`.max` (e.g. `spec.fuelEconomyCombined.min=35`); availability attributes repeat tri-state values (`spec.heatedSeatsFront=standard`). Attribute ids come from GET /v2/specs/catalog; unknown ids 400 with the id named. |

**Response 200** `ListingFacets` — Ref-native facets, feature facets, ranges, histogram

- `makes` (object[])
  - `makes[].ref` (string)
  - `makes[].name` (string)
  - `makes[].count` (integer)
  - `makes[].parentRef`? (string)
- `models` (object[])
  - `models[].ref` (string)
  - `models[].name` (string)
  - `models[].count` (integer)
  - `models[].parentRef`? (string)
- `bodyStyles` (object[])
  - `bodyStyles[].ref` (string)
  - `bodyStyles[].name` (string)
  - `bodyStyles[].count` (integer)
  - `bodyStyles[].parentRef`? (string)
- `fuelTypes` (object[])
  - `fuelTypes[].ref` (string)
  - `fuelTypes[].name` (string)
  - `fuelTypes[].count` (integer)
  - `fuelTypes[].parentRef`? (string)
- `driveTypes` (object[])
  - `driveTypes[].ref` (string)
  - `driveTypes[].name` (string)
  - `driveTypes[].count` (integer)
  - `driveTypes[].parentRef`? (string)
- `transmissions` (object[])
  - `transmissions[].ref` (string)
  - `transmissions[].name` (string)
  - `transmissions[].count` (integer)
  - `transmissions[].parentRef`? (string)
- `electrificationLevels` (object[])
  - `electrificationLevels[].ref` (string)
  - `electrificationLevels[].name` (string)
  - `electrificationLevels[].count` (integer)
  - `electrificationLevels[].parentRef`? (string)
- `vehicleTypes` (object[])
  - `vehicleTypes[].ref` (string)
  - `vehicleTypes[].name` (string)
  - `vehicleTypes[].count` (integer)
  - `vehicleTypes[].parentRef`? (string)
- `exteriorColors` (object[])
  - `exteriorColors[].ref` (string)
  - `exteriorColors[].name` (string)
  - `exteriorColors[].count` (integer)
  - `exteriorColors[].parentRef`? (string)
- `saleTypes` (object[])
  - `saleTypes[].ref` (string)
  - `saleTypes[].name` (string)
  - `saleTypes[].count` (integer)
  - `saleTypes[].parentRef`? (string)
- `years`? (object)
  - `years.min` (number)
  - `years.max` (number)
- `prices`? (object)
  - `prices.min` (number)
  - `prices.max` (number)
- `odometer`? (object)
  - `odometer.min` (number)
  - `odometer.max` (number)
- `priceHistogram`? (object[])
  - `priceHistogram[].min` (number)
  - `priceHistogram[].max` (number)
  - `priceHistogram[].count` (number)
- `featureFacets`? (object[])
  - `featureFacets[].id` (string)
  - `featureFacets[].name` (string)
  - `featureFacets[].category` ("tech" | "comfort" | "safety" | "ev")
  - `featureFacets[].count` (integer)
- `specRanges`? (object)
- `featureFacetsAreApproximate`? (boolean)
- `totalCount` (integer)
- `totalCountIsApproximate`? (boolean)

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/listings/facets?make=make:tesla" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const listingFacets = await client.v2.listings.facets({ filters: { makes: ["make:tesla"] } });
console.log(listingFacets.makes);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

listing_facets = client.v2.listings.facets(filters={"makes": ["make:tesla"]})
print(listing_facets.makes)
```

### GET /v2/listings/id/{id}

One listing by id

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Listing id |

**Response 200** `ListingDetail` — Listing + vehicle + canonical spec

- `id` (string)
- `vin` (string)
- `price` (number)
- `currency`? (string)
- `odometer`? (number | null)
- `year` (integer)
- `make` (string)
- `model` (string)
- `trim`? (string | null)
- `makeRef`? (string | null)
- `modelRef`? (string | null)
- `nano`? (string | null)
- `media`? (object[])
- `variant` (object | null)
  - `variant.id` (string)
  - `variant.trim`? (string | null)
  - `variant.styleName`? (string | null)
  - `variant.region`? (string | null)
  - `variant.year`? (integer | null)
  - `variant.msrp`? (number | null)
- `spec` (object | null)

**Errors** (all use the ErrorEnvelope): 400, 401, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/listings/id/$LISTING_ID" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const listingDetail = await client.v2.listings.byId("$LISTING_ID");
console.log(listingDetail.id);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

listing_detail = client.v2.listings.by_id("$LISTING_ID")
print(listing_detail.id)
```

### GET /v2/listings/vin/{vin}

Listing detail by VIN — listing + variant + full canonical spec, one query

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vin` | string | yes | 17-character VIN |

**Response 200** `ListingDetail` — spec is null when the nano has no variant link yet

- `id` (string)
- `vin` (string)
- `price` (number)
- `currency`? (string)
- `odometer`? (number | null)
- `year` (integer)
- `make` (string)
- `model` (string)
- `trim`? (string | null)
- `makeRef`? (string | null)
- `modelRef`? (string | null)
- `nano`? (string | null)
- `media`? (object[])
- `variant` (object | null)
  - `variant.id` (string)
  - `variant.trim`? (string | null)
  - `variant.styleName`? (string | null)
  - `variant.region`? (string | null)
  - `variant.year`? (integer | null)
  - `variant.msrp`? (number | null)
- `spec` (object | null)

**Errors** (all use the ErrorEnvelope): 400, 401, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/listings/vin/1HGCM82633A123456" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const listingDetail = await client.v2.listings.byVin("1HGCM82633A123456");
console.log(listingDetail.id);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

listing_detail = client.v2.listings.by_vin("1HGCM82633A123456")
print(listing_detail.id)
```

---

## Reference: Instruments

Market symbology + instrument cards

**Availability: live** on `https://api.cardog.app`.

### GET /v2/instruments

Symbology search / most-liquid browse

With `q`: typeahead over live instruments. Without: the most-liquid screener rows.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | no | Typeahead query (min 2 chars) |
| `limit` | integer | no | Row cap |

**Response 200** `InstrumentSearch` — Screener rows with quote line + sparkline

- `query`? (string | null)
- `items` (object[])
  - `items[].ref` (string)
  - `items[].label`? (string)
  - `items[].makeName`? (string)
  - `items[].spark`? (number[]) — Trailing 45d of daily medians, ascending — the row's inline sparkline
  - `items[].median30d`? (number | null) — Median 30 days ago — basis for the 30-day change
  - `items[].count` (integer)
  - `items[].best` (number | null)
  - `items[].p25` (number | null)
  - `items[].median` (number | null)
  - `items[].p75` (number | null)
  - `items[].avgDom` (number | null)
  - `items[].cuts30d` (integer | null)
  - `items[].cutPct30d` (number | null)
  - `items[].sightings30d` (integer | null)
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/instruments?q=cr-v" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const instrumentSearch = await client.v2.instruments.search({ q: "cr-v" });
console.log(instrumentSearch.query);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

instrument_search = client.v2.instruments.search(q="cr-v")
print(instrument_search.query)
```

### GET /v2/instruments/{ref}

The instrument card: quote + history + live sample

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | An instrument ref (model-year: or squish:). Slashes in refs must be URL-encoded (%2F). |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `window` | "1w" \| "1m" \| "3m" \| "6m" \| "ytd" \| "1y" \| "3y" \| "5y" \| "10y" \| "all" | no | History window |

**Response 200** `InstrumentCard` — Quote + daily bars + bounded live sample

- `instrument` (object)
  - `instrument.ref` (string)
  - `instrument.grain` ("mmy" | "mmy.drive" | "mmy.fuel" | "mmy.body" | "squish")
  - `instrument.label`? (string)
- `quote` (object)
  - `quote.ref` (string) — Instrument ref, e.g. "model-year:honda/cr-v/2026"
  - `quote.grain` ("mmy" | "mmy.drive" | "mmy.fuel" | "mmy.body" | "squish")
  - `quote.liveCount` (integer) — Active listings backing the quote
  - `quote.bestPrice` (number | null)
  - `quote.priceP25` (number | null)
  - `quote.priceMedian` (number | null)
  - `quote.priceP75` (number | null)
  - `quote.avgDomDays` (number | null) — Average days-on-market
  - `quote.cuts30d` (integer | null) — Price cuts observed, trailing 30d
  - `quote.avgCutPct30d` (number | null) — Average cut size, %, trailing 30d
  - `quote.sightings30d` (integer | null)
  - `quote.asOf` (string) — When the quote was last computed (ISO 8601)
  - `quote.links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}
- `history` (object[])
  - `history[].day` (string) — ISO date (YYYY-MM-DD)
  - `history[].median` (number | null)
  - `history[].p25` (number | null)
  - `history[].p75` (number | null)
  - `history[].best` (number | null)
  - `history[].count` (integer | null) — Live listings that day (source "fold" bars)
  - `history[].prints` (integer | null) — Prints aggregated (source "prints" bars)
  - `history[].source` (string) — How the bar was built: "fold" (daily snapshot of live listings) or "prints" (aggregated observed prints) — open set
- `listings` (object[]) — Bounded sample of the live book
  - `listings[].vin` (string)
  - `listings[].price` (number)
  - `listings[].odometer` (number | null)
  - `listings[].domain` (string | null)
  - `listings[].listedAt` (string) — ISO 8601
  - `listings[].score` (number | null)
  - `listings[].sourceUri` (string | null)
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/instruments/model-year:honda%2Fcr-v%2F2026?window=1y" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const instrumentCard = await client.v2.instruments.getByRef("model-year:honda/cr-v/2026", { window: "1y" });
console.log(instrumentCard.instrument);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

instrument_card = client.v2.instruments.get("model-year:honda/cr-v/2026", window="1y")
print(instrument_card.instrument)
```

---

## Reference: Quotes

The live book per instrument

**Availability: live** on `https://api.cardog.app`.

### GET /v2/quotes

Multi-quote: one call, up to 20 instruments

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `refs` | string | yes | Comma-separated instrument refs, e.g. "model-year:honda/cr-v/2026,model-year:toyota/rav4/2026". Unknown refs 400, named. |

**Response 200** `MultiQuote` — Quotes in request order

- `quotes` (object[]) — Same order as the request
  - `quotes[].ref` (string) — Instrument ref, e.g. "model-year:honda/cr-v/2026"
  - `quotes[].grain` ("mmy" | "mmy.drive" | "mmy.fuel" | "mmy.body" | "squish")
  - `quotes[].liveCount` (integer) — Active listings backing the quote
  - `quotes[].bestPrice` (number | null)
  - `quotes[].priceP25` (number | null)
  - `quotes[].priceMedian` (number | null)
  - `quotes[].priceP75` (number | null)
  - `quotes[].avgDomDays` (number | null) — Average days-on-market
  - `quotes[].cuts30d` (integer | null) — Price cuts observed, trailing 30d
  - `quotes[].avgCutPct30d` (number | null) — Average cut size, %, trailing 30d
  - `quotes[].sightings30d` (integer | null)
  - `quotes[].asOf` (string) — When the quote was last computed (ISO 8601)
  - `quotes[].links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/quotes?refs=model-year:honda/cr-v/2026,model-year:toyota/rav4/2026" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const multiQuote = await client.v2.quotes.getMany(["model-year:honda/cr-v/2026", "model-year:toyota/rav4/2026"]);
console.log(multiQuote.quotes);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

multi_quote = client.v2.quotes.get_many(["model-year:honda/cr-v/2026", "model-year:toyota/rav4/2026"])
print(multi_quote.quotes)
```

### GET /v2/quotes/{ref}

The live book for one instrument

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | An instrument ref. Slashes in refs must be URL-encoded (%2F). |

**Response 200** `Quote` — NBBO-style quote line: best, percentiles, DOM, cuts

- `ref` (string) — Instrument ref, e.g. "model-year:honda/cr-v/2026"
- `grain` ("mmy" | "mmy.drive" | "mmy.fuel" | "mmy.body" | "squish")
- `liveCount` (integer) — Active listings backing the quote
- `bestPrice` (number | null)
- `priceP25` (number | null)
- `priceMedian` (number | null)
- `priceP75` (number | null)
- `avgDomDays` (number | null) — Average days-on-market
- `cuts30d` (integer | null) — Price cuts observed, trailing 30d
- `avgCutPct30d` (number | null) — Average cut size, %, trailing 30d
- `sightings30d` (integer | null)
- `asOf` (string) — When the quote was last computed (ISO 8601)
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/quotes/model-year:honda%2Fcr-v%2F2026" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const quote = await client.v2.quotes.getByRef("model-year:honda/cr-v/2026");
console.log(quote.ref);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

quote = client.v2.quotes.get("model-year:honda/cr-v/2026")
print(quote.ref)
```

---

## Reference: Tape

Prints + daily bars

**Availability: live** on `https://api.cardog.app`.

### GET /v2/tape/live

The live tape: freshest prints, seconds old

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Print cap |

**Response 200** `TapeLive` — Prints + throughput counters

- `prints` (object[]) — Newest first, bounded window
  - `prints[].ref` (string) — Instrument ref, e.g. "model-year:toyota/rav4/2021"
  - `prints[].label`? (string) — Registry display label, e.g. "Toyota RAV4 2021"
  - `prints[].price` (number)
  - `prints[].domain` (string) — Seller website domain the print was observed on
  - `prints[].at` (string) — Observation timestamp (ISO 8601)
  - `prints[].vinTail` (string) — Last 6 of the VIN — enough to dedup, never the full VIN
- `perMinute` (integer) — Prints/minute, trailing 5 minutes
- `today` (integer) — Prints since midnight
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/tape/live" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const tapeLive = await client.v2.tape.live();
console.log(tapeLive.prints);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

tape_live = client.v2.tape.live()
print(tape_live.prints)
```

### GET /v2/tape/history/{ref}

Daily bars for one instrument over a window

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | An instrument ref. Slashes in refs must be URL-encoded (%2F). |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `window` | "1w" \| "1m" \| "3m" \| "6m" \| "ytd" \| "1y" \| "3y" \| "5y" \| "10y" \| "all" | no | History window |

**Response 200** `TapeHistory` — Ascending daily bars from daily snapshots and observed prints

- `ref` (string)
- `grain` ("mmy" | "mmy.drive" | "mmy.fuel" | "mmy.body" | "squish")
- `window` ("1w" | "1m" | "3m" | "6m" | "ytd" | "1y" | "3y" | "5y" | "10y" | "all")
- `bars` (object[]) — Ascending by day
  - `bars[].day` (string) — ISO date (YYYY-MM-DD)
  - `bars[].median` (number | null)
  - `bars[].p25` (number | null)
  - `bars[].p75` (number | null)
  - `bars[].best` (number | null)
  - `bars[].count` (integer | null) — Live listings that day (source "fold" bars)
  - `bars[].prints` (integer | null) — Prints aggregated (source "prints" bars)
  - `bars[].source` (string) — How the bar was built: "fold" (daily snapshot of live listings) or "prints" (aggregated observed prints) — open set
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/tape/history/model-year:honda%2Fcr-v%2F2026?window=1y" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const tapeHistory = await client.v2.tape.history("model-year:honda/cr-v/2026", { window: "1y" });
console.log(tapeHistory.ref);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

tape_history = client.v2.tape.history("model-year:honda/cr-v/2026", window="1y")
print(tape_history.ref)
```

---

## Reference: Recalls

TC+NHTSA fused, ref-keyed compliance

**Availability: live** on `https://api.cardog.app`.

### GET /v2/recalls/vin/{vin}

The compliance check: recalls affecting a VIN

The authoritative is-this-vehicle-under-recall check — Transport Canada + NHTSA fused, ref-keyed.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vin` | string | yes | 17-character VIN |

**Response 200** `RecallsVin` — Campaigns; `resolved` distinguishes unbridged VINs from clean ones

- `vin` (string)
- `modelYearRef` (string | null) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
- `resolved` (boolean) — false = VIN neither observed in the graph NOR decodable to a model-year; recalls then vacuously empty
- `source`? ("observed" | "decoded") — How the VIN reached its model-year: 'observed' = the exact VIN is in the vehicle graph; 'decoded' = identity derived by the decoder (campaigns are model-year-scoped, not serial-verified). Absent when unresolved.
- `total` (integer)
- `recalls` (object[])
  - `recalls[].ref` (string) — Entity ref in the "recall" domain. Example: "recall:tc/2024-123"
  - `recalls[].authority` (string) — Issuing authority key: "nhtsa" | "tc" | … — open set
  - `recalls[].authorityLabel` (string) — Display label, e.g. "Transport Canada"
  - `recalls[].campaignNumber` (string)
  - `recalls[].component` (string | null)
  - `recalls[].defectSummary` (string | null)
  - `recalls[].consequenceSummary` (string | null)
  - `recalls[].correctiveAction` (string | null)
  - `recalls[].recallDate` (string | null) — ISO date
  - `recalls[].notificationType` (string | null)
  - `recalls[].unitsAffected` (integer | null) — Campaign-level units (max over affects)
  - `recalls[].affects` (object[]) — Affected model-years, scoped to the queried ref — an entity query lists only its own years
    - `recalls[].affects[].modelYearRef` (string) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
    - `recalls[].affects[].year` (integer)
    - `recalls[].affects[].unitsAffected` (integer | null)
  - `recalls[].links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}
- `asOf`? (string) — When the recall data was last updated (ISO 8601) — citable
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/recalls/vin/1HGCM82633A123456" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const recallsVin = await client.v2.recalls.vin("1HGCM82633A123456");
console.log(recallsVin.vin);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

recalls_vin = client.v2.recalls.vin("1HGCM82633A123456")
print(recalls_vin.vin)
```

### GET /v2/recalls/entity/{ref}

Recalls scoped to a make:, model:, or model-year: ref

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | A make:, model:, or model-year: ref. Slashes in refs must be URL-encoded (%2F). |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Campaign cap |

**Response 200** `RecallsEntity` — Campaigns, with affected model-years scoped to the queried ref

- `ref` (string) — The queried entity ref (make:, model:, or model-year:)
- `total` (integer) — Total campaigns in scope (page may be shorter)
- `recalls` (object[])
  - `recalls[].ref` (string) — Entity ref in the "recall" domain. Example: "recall:tc/2024-123"
  - `recalls[].authority` (string) — Issuing authority key: "nhtsa" | "tc" | … — open set
  - `recalls[].authorityLabel` (string) — Display label, e.g. "Transport Canada"
  - `recalls[].campaignNumber` (string)
  - `recalls[].component` (string | null)
  - `recalls[].defectSummary` (string | null)
  - `recalls[].consequenceSummary` (string | null)
  - `recalls[].correctiveAction` (string | null)
  - `recalls[].recallDate` (string | null) — ISO date
  - `recalls[].notificationType` (string | null)
  - `recalls[].unitsAffected` (integer | null) — Campaign-level units (max over affects)
  - `recalls[].affects` (object[]) — Affected model-years, scoped to the queried ref — an entity query lists only its own years
    - `recalls[].affects[].modelYearRef` (string) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
    - `recalls[].affects[].year` (integer)
    - `recalls[].affects[].unitsAffected` (integer | null)
  - `recalls[].links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/recalls/entity/make:toyota" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const recallsEntity = await client.v2.recalls.entity("make:toyota");
console.log(recallsEntity.ref);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

recalls_entity = client.v2.recalls.entity("make:toyota")
print(recalls_entity.ref)
```

### GET /v2/recalls/feed

Latest campaigns, newest first

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Campaign cap |

**Response 200** `RecallsFeed` — The live recall log

- `recalls` (object[])
  - `recalls[].ref` (string) — Entity ref in the "recall" domain. Example: "recall:tc/2024-123"
  - `recalls[].authority` (string) — Issuing authority key: "nhtsa" | "tc" | … — open set
  - `recalls[].authorityLabel` (string) — Display label, e.g. "Transport Canada"
  - `recalls[].campaignNumber` (string)
  - `recalls[].component` (string | null)
  - `recalls[].defectSummary` (string | null)
  - `recalls[].consequenceSummary` (string | null)
  - `recalls[].correctiveAction` (string | null)
  - `recalls[].recallDate` (string | null) — ISO date
  - `recalls[].notificationType` (string | null)
  - `recalls[].unitsAffected` (integer | null) — Campaign-level units (max over affects)
  - `recalls[].affects` (object[]) — Affected model-years, scoped to the queried ref — an entity query lists only its own years
    - `recalls[].affects[].modelYearRef` (string) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
    - `recalls[].affects[].year` (integer)
    - `recalls[].affects[].unitsAffected` (integer | null)
  - `recalls[].links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 401, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/recalls/feed" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const recallsFeed = await client.v2.recalls.feed();
console.log(recallsFeed.recalls);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

recalls_feed = client.v2.recalls.feed()
print(recalls_feed.recalls)
```

### GET /v2/recalls/stats

Recall dataset counts (campaigns, affected model-year links, freshness)

**Response 200** `RecallsStats` — Counts plus the last-updated date, citable

- `campaigns` (integer)
- `links` (integer) — campaign × model-year affect rows
- `latest` (string | null) — Most recent recall date on record (ISO date)

**Errors** (all use the ErrorEnvelope): 401, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/recalls/stats" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const recallsStats = await client.v2.recalls.stats();
console.log(recallsStats.campaigns);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

recalls_stats = client.v2.recalls.stats()
print(recalls_stats.campaigns)
```

### GET /v2/recalls/{ref}

One campaign by recall ref

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | A recall: ref. Slashes in refs must be URL-encoded (%2F). |

**Response 200** `Recall` — The campaign record

- `ref` (string) — Entity ref in the "recall" domain. Example: "recall:tc/2024-123"
- `authority` (string) — Issuing authority key: "nhtsa" | "tc" | … — open set
- `authorityLabel` (string) — Display label, e.g. "Transport Canada"
- `campaignNumber` (string)
- `component` (string | null)
- `defectSummary` (string | null)
- `consequenceSummary` (string | null)
- `correctiveAction` (string | null)
- `recallDate` (string | null) — ISO date
- `notificationType` (string | null)
- `unitsAffected` (integer | null) — Campaign-level units (max over affects)
- `affects` (object[]) — Affected model-years, scoped to the queried ref — an entity query lists only its own years
  - `affects[].modelYearRef` (string) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
  - `affects[].year` (integer)
  - `affects[].unitsAffected` (integer | null)
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/recalls/recall:tc%2F2024-123" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const recall = await client.v2.recalls.getByRef("recall:tc/2024-123");
console.log(recall.ref);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

recall = client.v2.recalls.get("recall:tc/2024-123")
print(recall.ref)
```

---

## Reference: Safety

NCAP ratings + complaints

**Availability: live** on `https://api.cardog.app`.

### GET /v2/safety/{ref}

NCAP ratings for a model year

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | A model-year ref. Slashes in refs must be URL-encoded (%2F). |

**Response 200** `Safety` — Rated configurations + the headline rating

- `modelYearRef` (string) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
- `best` (object | null)
  - `best.ref` (string) — Per-configuration rating identity — the citable key
  - `best.modelYearRef` (string) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
  - `best.makeRef` (string | null) — Entity ref in the "make" domain. Example: "make:toyota"
  - `best.modelRef` (string | null) — Entity ref in the "model" domain. Example: "model:toyota/rav4"
  - `best.year` (integer)
  - `best.make` (string | null) — Display name, derived from makeRef
  - `best.model` (string | null) — Display name, derived from modelRef
  - `best.config` (object)
    - `best.config.bodyStyleRef` (string | null) — e.g. "body-style:pickup"
    - `best.config.bodyCabRef` (string | null) — e.g. "body-cab:crew-super-crew-crew-max"
    - `best.config.driveTypeRefs` (string[])
    - `best.config.fuelTypeRef` (string | null)
    - `best.config.electrificationLevelRef` (string | null)
    - `best.config.nhtsaClass` ("passenger-car" | "suv" | "truck" | "van" | "bus")
    - `best.config.nhtsaSizeClass` ("mini" | "light" | "compact" | "medium" | "heavy")
    - `best.config.variant` (string | null)
  - `best.crash` (object)
    - `best.crash.overall` (object)
      - `best.crash.overall.stars` (integer | null)
      - `best.crash.overall.outOf` (5)
    - `best.crash.frontal` (object)
      - `best.crash.frontal.overall` (object)
      - `best.crash.frontal.driver` (object)
      - `best.crash.frontal.passenger` (object)
    - `best.crash.side` (object)
      - `best.crash.side.overall` (object)
      - `best.crash.side.driver` (object)
      - `best.crash.side.passenger` (object)
      - `best.crash.side.barrier` (object)
      - `best.crash.side.pole` (object)
    - `best.crash.combined` (object)
      - `best.crash.combined.front` (object)
      - `best.crash.combined.rear` (object)
    - `best.crash.rollover` (object)
      - `best.crash.rollover.stars` (object)
      - `best.crash.rollover.possibility` (object | null)
      - `best.crash.rollover.staticStabilityFactor` (object | null)
      - `best.crash.rollover.dynamicTip` ("no-tip" | "tip" | "not-tested")
  - `best.equipment` (object)
  - `best.published` (object)
    - `best.published.bodyStyle` (string | null)
    - `best.published.driveTrain` (string | null)
    - `best.published.vehicleClass` (string | null)
    - `best.published.vehicleType` (string | null)
    - `best.published.dynamicTipResult` (string | null)
    - `best.published.rolloverPossibility` (number | null)
    - `best.published.abs` (string | null)
    - `best.published.esc` (string | null)
    - `best.published.backupCamera` (string | null)
    - `best.published.blindSpotDetection` (string | null)
    - `best.published.frontCollisionWarning` (string | null)
    - `best.published.laneDepartureWarning` (string | null)
    - `best.published.crashImminentBrake` (string | null)
    - `best.published.dynamicBrakeSupport` (string | null)
    - `best.published.headSideAirbag` (string | null)
    - `best.published.torsoSideAirbag` (string | null)
  - `best.provenance` (object)
    - `best.provenance.sourceRef` (string)
    - `best.provenance.productionRelease` (integer)
  - `best.tested`? (boolean)
- `ratings` (object[])
  - `ratings[].ref` (string) — Per-configuration rating identity — the citable key
  - `ratings[].modelYearRef` (string) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
  - `ratings[].makeRef` (string | null) — Entity ref in the "make" domain. Example: "make:toyota"
  - `ratings[].modelRef` (string | null) — Entity ref in the "model" domain. Example: "model:toyota/rav4"
  - `ratings[].year` (integer)
  - `ratings[].make` (string | null) — Display name, derived from makeRef
  - `ratings[].model` (string | null) — Display name, derived from modelRef
  - `ratings[].config` (object)
    - `ratings[].config.bodyStyleRef` (string | null) — e.g. "body-style:pickup"
    - `ratings[].config.bodyCabRef` (string | null) — e.g. "body-cab:crew-super-crew-crew-max"
    - `ratings[].config.driveTypeRefs` (string[])
    - `ratings[].config.fuelTypeRef` (string | null)
    - `ratings[].config.electrificationLevelRef` (string | null)
    - `ratings[].config.nhtsaClass` ("passenger-car" | "suv" | "truck" | "van" | "bus")
    - `ratings[].config.nhtsaSizeClass` ("mini" | "light" | "compact" | "medium" | "heavy")
    - `ratings[].config.variant` (string | null)
  - `ratings[].crash` (object)
    - `ratings[].crash.overall` (object)
      - `ratings[].crash.overall.stars` (integer | null)
      - `ratings[].crash.overall.outOf` (5)
    - `ratings[].crash.frontal` (object)
      - `ratings[].crash.frontal.overall` (object)
      - `ratings[].crash.frontal.driver` (object)
      - `ratings[].crash.frontal.passenger` (object)
    - `ratings[].crash.side` (object)
      - `ratings[].crash.side.overall` (object)
      - `ratings[].crash.side.driver` (object)
      - `ratings[].crash.side.passenger` (object)
      - `ratings[].crash.side.barrier` (object)
      - `ratings[].crash.side.pole` (object)
    - `ratings[].crash.combined` (object)
      - `ratings[].crash.combined.front` (object)
      - `ratings[].crash.combined.rear` (object)
    - `ratings[].crash.rollover` (object)
      - `ratings[].crash.rollover.stars` (object)
      - `ratings[].crash.rollover.possibility` (object | null)
      - `ratings[].crash.rollover.staticStabilityFactor` (object | null)
      - `ratings[].crash.rollover.dynamicTip` ("no-tip" | "tip" | "not-tested")
  - `ratings[].equipment` (object)
  - `ratings[].published` (object)
    - `ratings[].published.bodyStyle` (string | null)
    - `ratings[].published.driveTrain` (string | null)
    - `ratings[].published.vehicleClass` (string | null)
    - `ratings[].published.vehicleType` (string | null)
    - `ratings[].published.dynamicTipResult` (string | null)
    - `ratings[].published.rolloverPossibility` (number | null)
    - `ratings[].published.abs` (string | null)
    - `ratings[].published.esc` (string | null)
    - `ratings[].published.backupCamera` (string | null)
    - `ratings[].published.blindSpotDetection` (string | null)
    - `ratings[].published.frontCollisionWarning` (string | null)
    - `ratings[].published.laneDepartureWarning` (string | null)
    - `ratings[].published.crashImminentBrake` (string | null)
    - `ratings[].published.dynamicBrakeSupport` (string | null)
    - `ratings[].published.headSideAirbag` (string | null)
    - `ratings[].published.torsoSideAirbag` (string | null)
  - `ratings[].provenance` (object)
    - `ratings[].provenance.sourceRef` (string)
    - `ratings[].provenance.productionRelease` (integer)
  - `ratings[].tested`? (boolean)
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/safety/model-year:honda%2Fcr-v%2F2026" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const safety = await client.v2.safety.ratings("model-year:honda/cr-v/2026");
console.log(safety.modelYearRef);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

safety = client.v2.safety.ratings("model-year:honda/cr-v/2026")
print(safety.model_year_ref)
```

### GET /v2/safety/{ref}/complaints

ODI complaints for a model year, paged

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ref` | string | yes | A model-year ref. Slashes in refs must be URL-encoded (%2F). |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | integer | no | Page number |
| `limit` | integer | no | Page size |

**Response 200** `Complaints` — Complaints, newest first

- `modelYearRef` (string | null) — Null when the query grain was broader than one model year
- `totalCount` (integer)
- `complaints` (object[])
  - `complaints[].ref` (string) — Incident identity: "complaint:nhtsa/{odino}" — NHTSA reuses the ODI number across component rows; this ref addresses the complaint, not a source row
  - `complaints[].modelYearRef` (string | null) — Entity ref in the "model-year" domain. Example: "model-year:toyota/rav4/2021"
  - `complaints[].component` (string | null)
  - `complaints[].summary` (string | null)
  - `complaints[].incidentDate` (string | null) — ISO date
  - `complaints[].filedDate` (string | null) — ISO date
  - `complaints[].crash` (boolean | null)
  - `complaints[].fire` (boolean | null)
  - `complaints[].injuries` (integer | null)
  - `complaints[].deaths` (integer | null)
  - `complaints[].source` (string) — "nhtsa" — open set
- `pagination` (object)
  - `pagination.page` (integer)
  - `pagination.limit` (integer)
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/safety/model-year:honda%2Fcr-v%2F2026/complaints" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const complaints = await client.v2.safety.complaints("model-year:honda/cr-v/2026");
console.log(complaints.modelYearRef);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

complaints = client.v2.safety.complaints("model-year:honda/cr-v/2026")
print(complaints.model_year_ref)
```

---

## Reference: Charging

EV charging stations (Open Charge Map data, ODbL)

**Availability: live** on `https://api.cardog.app`.

### GET /v2/charging/stations

EV charging stations near a point

Radius search over our own copy of the Open Charge Map dataset — no live upstream. Nearest first. Open Charge Map data is ODbL 1.0: responses carry the `attribution` string; surface it wherever stations render.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `lat` | number | yes | Search-center latitude |
| `lng` | number | yes | Search-center longitude |
| `radius_km` | number | no | Search radius in km |
| `connector` | integer | no | OCM ConnectionTypeID filter — stations with at least one matching connector |
| `min_power_kw` | number | no | Minimum station max-power in kW |
| `status` | "Operational" \| "NonOperational" \| "any" | no | Status filter; default serves only operational stations |
| `limit` | integer | no | Station cap |

**Response 200** `ChargingSearch` — Stations nearest first, with the ODbL attribution

- `total` (integer) — Stations returned (bounded by limit)
- `stations` (object[]) — Nearest first
  - `stations[].ref` (string) — Entity ref in the "station" domain. Example: "station:ocm/248113"
  - `stations[].id` (string) — Source station id (OCM ChargePoint ID)
  - `stations[].source` (string) — Provenance: "ocm" today — open set
  - `stations[].name` (string | null)
  - `stations[].address` (object)
    - `stations[].address.street` (string | null)
    - `stations[].address.city` (string | null)
    - `stations[].address.state` (string | null)
    - `stations[].address.postalCode` (string | null)
    - `stations[].address.country` (string | null) — ISO 3166-1 alpha-2
  - `stations[].location` (object)
    - `stations[].location.latitude` (number)
    - `stations[].location.longitude` (number)
  - `stations[].connectors` (object[])
    - `stations[].connectors[].connectionTypeId` (integer) — OCM ConnectionTypeID (0 = unspecified)
    - `stations[].connectors[].connectionType` (string) — Normalized name, e.g. "CCS Type 1", "NACS"
    - `stations[].connectors[].powerKW` (number) — Rated power in kW (0 = unknown)
    - `stations[].connectors[].currentType` ("AC" | "DC")
    - `stations[].connectors[].level` (integer) — Charging level 1–3 (0 = unspecified)
    - `stations[].connectors[].voltage` (number) — Volts (0 = unknown)
    - `stations[].connectors[].amperage` (number) — Amps (0 = unknown)
    - `stations[].connectors[].quantity` (integer | null) — Connectors of this type at the station
  - `stations[].maxPowerKw` (number | null) — Max rated connector power at the station
  - `stations[].numberOfPoints` (integer | null) — Charge points at the station
  - `stations[].operator` (string | null)
  - `stations[].status` (string) — Station status: "Operational" | "NonOperational" — decommissioned stations are kept; consumers must filter
  - `stations[].usage` (string | null) — Usage class, e.g. "Public", "Private - Restricted Access"
  - `stations[].isMembershipRequired` (boolean | null)
  - `stations[].isPayAtLocation` (boolean | null)
  - `stations[].dateCreated` (string | null) — ISO 8601 — when the source first recorded the station
  - `stations[].dateLastVerified` (string | null) — ISO 8601 — last source verification
  - `stations[].links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}
- `asOf`? (string) — When the data was last refreshed (ISO 8601) — citable
- `attribution` (string) — ODbL attribution — must accompany rendered station data
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/charging/stations?lat=43.6532&lng=-79.3832&radius_km=10" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const chargingSearch = await client.v2.charging.search({ lat: 43.6532, lng: -79.3832, radius_km: 10 });
console.log(chargingSearch.total);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

charging_search = client.v2.charging.search(lat=43.6532, lng=-79.3832, radius_km=10)
print(charging_search.total)
```

### GET /v2/charging/stations/{id}

One charging station by source id

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Source station id (OCM ChargePoint ID) |

**Response 200** `ChargingStationDetail` — The station record, with the ODbL attribution

- `station` (object)
  - `station.ref` (string) — Entity ref in the "station" domain. Example: "station:ocm/248113"
  - `station.id` (string) — Source station id (OCM ChargePoint ID)
  - `station.source` (string) — Provenance: "ocm" today — open set
  - `station.name` (string | null)
  - `station.address` (object)
    - `station.address.street` (string | null)
    - `station.address.city` (string | null)
    - `station.address.state` (string | null)
    - `station.address.postalCode` (string | null)
    - `station.address.country` (string | null) — ISO 3166-1 alpha-2
  - `station.location` (object)
    - `station.location.latitude` (number)
    - `station.location.longitude` (number)
  - `station.connectors` (object[])
    - `station.connectors[].connectionTypeId` (integer) — OCM ConnectionTypeID (0 = unspecified)
    - `station.connectors[].connectionType` (string) — Normalized name, e.g. "CCS Type 1", "NACS"
    - `station.connectors[].powerKW` (number) — Rated power in kW (0 = unknown)
    - `station.connectors[].currentType` ("AC" | "DC")
    - `station.connectors[].level` (integer) — Charging level 1–3 (0 = unspecified)
    - `station.connectors[].voltage` (number) — Volts (0 = unknown)
    - `station.connectors[].amperage` (number) — Amps (0 = unknown)
    - `station.connectors[].quantity` (integer | null) — Connectors of this type at the station
  - `station.maxPowerKw` (number | null) — Max rated connector power at the station
  - `station.numberOfPoints` (integer | null) — Charge points at the station
  - `station.operator` (string | null)
  - `station.status` (string) — Station status: "Operational" | "NonOperational" — decommissioned stations are kept; consumers must filter
  - `station.usage` (string | null) — Usage class, e.g. "Public", "Private - Restricted Access"
  - `station.isMembershipRequired` (boolean | null)
  - `station.isPayAtLocation` (boolean | null)
  - `station.dateCreated` (string | null) — ISO 8601 — when the source first recorded the station
  - `station.dateLastVerified` (string | null) — ISO 8601 — last source verification
  - `station.links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}
- `asOf`? (string) — When the data was last refreshed (ISO 8601) — citable
- `attribution` (string) — ODbL attribution — must accompany rendered station data

**Errors** (all use the ErrorEnvelope): 400, 401, 402, 404, 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/charging/stations/248113" \
  -H "x-api-key: $CARDOG_API_KEY"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient({ apiKey: process.env.CARDOG_API_KEY });

const chargingStationDetail = await client.v2.charging.station("248113");
console.log(chargingStationDetail.station);
```

**Example — Python** (`pip install cardog`)

```python
import os

from cardog import Cardog

client = Cardog(api_key=os.environ["CARDOG_API_KEY"])

charging_station_detail = client.v2.charging.station("248113")
print(charging_station_detail.station)
```

---

## Reference: Platform

Pricing, spec, meta

**Availability: live** on `https://api.cardog.app`.

### GET /v2/pricing

The machine-readable rate card *(no auth required)*

Credit rates per family + plan allowances, as JSON. Budget mid-task with this plus the X-Credits-* headers on every metered response. Unauthenticated.

**Response 200** `Pricing` — Rates, plans, credit headers

- `version` (integer) — Monotonic rate-card version — bump on ANY price change
- `asOf` (string) — ISO 8601 — when this card took effect
- `currency` (string) — ISO 4217, e.g. "USD"
- `rates` (object[]) — Only ENFORCED rates appear
  - `rates[].family` (string) — Billing family. The ten route groups (vin, entities, specs, listings, instruments, quotes, tape, recalls, safety, charging) plus composite products — open set.
  - `rates[].unit` (string) — What one metered unit is: "request" | "vin" (batch = N units) | "check"
  - `rates[].credits` (number) — Credits debited per unit. 0 = unmetered class read.
  - `rates[].description`? (string)
- `plans` (object[])
  - `plans[].id` (string) — Stable plan id, e.g. "free", "starter", "growth", "enterprise"
  - `plans[].name` (string)
  - `plans[].monthlyPrice` (number | null) — Monthly base price in `currency`; null = sales-led (contact)
  - `plans[].monthlyCredits` (integer | null) — The monthly credit allowance; null = custom (sales-led)
  - `plans[].overage` (object)
    - `plans[].overage.billed` (boolean) — true = past-allowance usage is billed, not blocked; false = hard stop (free tier)
    - `plans[].overage.pricePerCredit` (number | null) — Overage price per credit in `currency`; null when overage is not billed
  - `plans[].rateLimits`? (object)
    - `plans[].rateLimits.perMinute` (integer | null) — Token-bucket rate; null = plan-custom
  - `plans[].selfServe` (boolean) — true = instant key, no sales call
  - `plans[].requiresCard`? (boolean) — false on the evaluation tier
  - `plans[].description`? (string)
- `packs`? (object[]) — Prepaid one-time credit top-ups, all tiers (free included). Spend order per debit: allowance → packs (FIFO by purchase) → overage; packs expire 12 months after purchase.
  - `packs[].id` (string) — Stable pack id, e.g. "pack-250"
  - `packs[].credits` (integer) — Credits granted by one purchase
  - `packs[].price` (number) — One-time price in `currency`
  - `packs[].description`? (string)
- `customTopUp`? (object) — Caller-chosen top-up amount at a flat per-credit rate (no volume bonus); mechanics identical to packs.
  - `customTopUp.ratePerCredit` (number) — Flat price per credit in `currency`; no volume bonus
  - `customTopUp.minCredits` (integer) — Smallest purchasable amount
  - `customTopUp.maxCredits` (integer) — Largest purchasable amount
  - `customTopUp.expiryMonths` (integer) — Months from purchase until unspent credits expire
- `creditHeaders`? (string[]) — The response headers that carry live budget state
- `signupGrant`? (object) — Present only while a signup credit grant is running: new accounts created before `endsAt` receive `credits` prepaid credits. Absent when no campaign is open.
  - `signupGrant.credits` (integer) — Credits granted to a new account
  - `signupGrant.endsAt` (string) — ISO 8601 — the last moment a signup qualifies
  - `signupGrant.expiryMonths` (integer) — Months until unspent granted credits expire
  - `signupGrant.description` (string) — Human-readable offer, e.g. plan-equivalent framing
- `links`? (object) — Related resources, rel → server-relative path. Example: {"recalls": "/v2/recalls/entity/model-year:toyota/rav4/2021"}
- `docs_url`? (string)

**Errors** (all use the ErrorEnvelope): 429, 500

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/pricing"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient();

const pricing = await client.v2.platform.pricing();
console.log(pricing.version);
```

**Example — Python** (`pip install cardog`)

```python
from cardog import Cardog

client = Cardog()

pricing = client.v2.platform.pricing()
print(pricing.version)
```

### GET /v2/openapi.json

This document *(no auth required)*

**Response 200** — The OpenAPI 3.0 spec for the v2 surface

**Example — cURL**

```bash
curl "https://api.cardog.app/v2/openapi.json"
```

**Example — TypeScript** (`npm install @cardog/api`)

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

const client = new CardogClient();

const result = await client.v2.platform.openapi();
```

**Example — Python** (`pip install cardog`)

```python
from cardog import Cardog

client = Cardog()

result = client.v2.platform.openapi()
```
