Feeds

An organization's own vehicle identity rows, rebuilt every night and handed over as files: a full file, a delta file and a deletes file, each with its record count and SHA-256.

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

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

const run = await client.v2.feeds.latest("vehicle-identity@1");
const index = await run.index();

const row = index.lookup("2T3W1RFV3SC776931");
// { pattern: "2T3W1RFV*SC", make: "Toyota", model: "RAV4", model_year: 2025, ... } or null

A feed is your organization's own set of vehicle identity rows. It is rebuilt every night and handed over as files. Each file has a record count and a SHA-256, so you can check it before you load it.

An organization creates and edits its own feed in its workspace. It reads the feed from the workspace, from the API, or through the SDK.

The datasets

Three datasets exist. An organization holds one feed per dataset.

DatasetOne row perWhat it carries
vehicle-identity@1patternThe whole identity record: make, model, model year, body, powertrain, weights, status.
vehicle-recalls@1pattern and recall campaignEvery campaign that names the pattern's make, model and model year: authority, campaign numbers, defect, remedy, status. The key is the pair.
vehicle-safety@1patternThe crash-test stars the record resolves for the pattern, with the grain of the match: configuration, partial, model-year or unrated.

A recall row says the campaign names the model year. Whether one VIN in that model year was built inside the affected range, or has been remedied, is the manufacturer's own record. The feed does not claim it.

The rest of this page describes vehicle identity, vehicle-identity@1, in full; the other two follow the same rules with their own fields, which the workspace lists when you create the feed. Its status is draft. Within a version, fields are only added. A breaking change is a new version that runs beside the old one.

The dataset has 66 fields. There is one row per pattern, not one row per VIN.

Identity

#FieldTypePresentWhat it holds
1patternstringalwaysThe row key. VIN positions 1 to 8, * for the check digit, positions 10 to 11. Positions 12 to 14 are added when position 3 is 9.
2wmistringalwaysVIN positions 1 to 3, the world manufacturer identifier.
3makestringwhen knownThe make's display name.
4make_refrefwhen knownThe make's permanent ref, such as make:toyota. Join on this, not on the name.
5modelstringwhen knownThe model's display name.
6model_refrefwhen knownThe model's permanent ref, such as model:toyota/rav4.
7seriesstringwhen knownThe series, where one is recorded.
8series_refrefwhen knownSeries within the model. Permanent key. Map it to your own codes.
9model_yearintegerwhen knownThe model year.
10trimstringwhen knownThe trim, where one is recorded.
11statusstringalwaysdecoded or undecodable. An undecodable row is a pattern that is on record and has not been named; its value columns are empty.
12status_reasonsstringwhen knownWhy a pattern is undecodable: reason codes separated by ;.

Body

#FieldTypePresentWhat it holds
13vehicle_typestringwhen knownPassenger car, truck, motorcycle, trailer and so on.
14vehicle_type_refrefwhen knownVehicle type. Permanent key, e.g. vehicle-type:passenger-car. Map it to your own codes.
15body_stylestringwhen knownThe body style.
16body_style_refrefwhen knownBody style. Permanent key. Map it to your own codes.
17body_cabstringwhen knownCab type of a pickup or truck. Display name; empty when the registry holds no name for the ref.
18body_cab_refrefwhen knownCab type of a pickup or truck. Permanent key. Map it to your own codes.
19bed_typestringwhen knownBed type of a pickup. Display name; empty when the registry holds no name for the ref.
20bed_type_refrefwhen knownBed type of a pickup. Permanent key. Map it to your own codes.
21doorsintegerwhen knownThe number of doors.

Powertrain

#FieldTypePresentWhat it holds
22fuel_typestringwhen knownThe fuel type.
23fuel_type_refrefwhen knownPrimary fuel. Permanent key. Map it to your own codes.
24fuel_type_secondarystringwhen knownSecond fuel of a dual-fuel vehicle. Display name; empty when the registry holds no name for the ref.
25fuel_type_secondary_refrefwhen knownSecond fuel of a dual-fuel vehicle. Permanent key. Map it to your own codes.
26electrificationstringwhen knownThe electrification level, such as hybrid or battery electric.
27electrification_refrefwhen knownElectrification level. Permanent key, e.g. electrification-level:bev-battery-electric-vehicle. Map it to your own codes.
28drive_typestringwhen knownThe drive type.
29drive_type_refrefwhen knownDrive type. Permanent key. Map it to your own codes.
30transmissionstringwhen knownTransmission. Display name; empty when the registry holds no name for the ref.
31transmission_refrefwhen knownTransmission. Permanent key. Map it to your own codes.

Weight

#FieldTypePresentWhat it holds
32gvwr_classstringwhen knownThe gross vehicle weight rating class.
33gvwr_class_refrefwhen knownGross vehicle weight rating class. Permanent key, e.g. gvwr:class-2e-6-001-7-000-lb-2-722-3-175-kg. Map it to your own codes.
34wheelsintegerwhen knownNumber of wheels.
35axlesintegerwhen knownNumber of axles.
36shipping_weightintegerwhen knownShipping weight, as the record holds it.
37gross_weightintegerwhen knownGross weight, as the record holds it.

Trailer

#FieldTypePresentWhat it holds
38trailer_typestringwhen knownTrailer type. Display name; empty when the registry holds no name for the ref.
39trailer_type_refrefwhen knownTrailer type. Permanent key. Map it to your own codes.
40trailer_body_typestringwhen knownTrailer body type. Display name; empty when the registry holds no name for the ref.
41trailer_body_type_refrefwhen knownTrailer body type. Permanent key. Map it to your own codes.

Provenance

#FieldTypePresentWhat it holds
42sourcestringwhen knownHow the row was named: decode when the VIN pattern itself resolves it, reference when a reference record filled it. It never says who filed the pattern.
43make_sourcestringwhen knownWhich mapper wrote make: decode, catalogue, fill, miit or nhtsa-safety.
44model_sourcestringwhen knownWhich mapper wrote model: decode, catalogue, fill, miit or nhtsa-safety.
45series_sourcestringwhen knownWhich mapper wrote series: decode, catalogue, fill, miit or nhtsa-safety.
46vehicle_type_sourcestringwhen knownWhich mapper wrote vehicle_type: decode, catalogue, fill, miit or nhtsa-safety.
47body_style_sourcestringwhen knownWhich mapper wrote body_style: decode, catalogue, fill, miit or nhtsa-safety.
48body_cab_sourcestringwhen knownWhich mapper wrote body_cab: decode, catalogue, fill, miit or nhtsa-safety.
49bed_type_sourcestringwhen knownWhich mapper wrote bed_type: decode, catalogue, fill, miit or nhtsa-safety.
50fuel_type_sourcestringwhen knownWhich mapper wrote fuel_type: decode, catalogue, fill, miit or nhtsa-safety.
51fuel_type_secondary_sourcestringwhen knownWhich mapper wrote fuel_type_secondary: decode, catalogue, fill, miit or nhtsa-safety.
52electrification_sourcestringwhen knownWhich mapper wrote electrification: decode, catalogue, fill, miit or nhtsa-safety.
53drive_type_sourcestringwhen knownWhich mapper wrote drive_type: decode, catalogue, fill, miit or nhtsa-safety.
54transmission_sourcestringwhen knownWhich mapper wrote transmission: decode, catalogue, fill, miit or nhtsa-safety.
55gvwr_class_sourcestringwhen knownWhich mapper wrote gvwr_class: decode, catalogue, fill, miit or nhtsa-safety.
56trailer_type_sourcestringwhen knownWhich mapper wrote trailer_type: decode, catalogue, fill, miit or nhtsa-safety.
57trailer_body_type_sourcestringwhen knownWhich mapper wrote trailer_body_type: decode, catalogue, fill, miit or nhtsa-safety.
58model_year_sourcestringwhen knownWhich mapper wrote model_year: decode, catalogue, fill, miit or nhtsa-safety.
59trim_sourcestringwhen knownWhich mapper wrote trim: decode, catalogue, fill, miit or nhtsa-safety.
60doors_sourcestringwhen knownWhich mapper wrote doors: decode, catalogue, fill, miit or nhtsa-safety.
61wheels_sourcestringwhen knownWhich mapper wrote wheels: decode, catalogue, fill, miit or nhtsa-safety.
62axles_sourcestringwhen knownWhich mapper wrote axles: decode, catalogue, fill, miit or nhtsa-safety.
63shipping_weight_sourcestringwhen knownWhich mapper wrote shipping_weight: decode, catalogue, fill, miit or nhtsa-safety.
64gross_weight_sourcestringwhen knownWhich mapper wrote gross_weight: decode, catalogue, fill, miit or nhtsa-safety.
65identity_buildstringalwaysThe identity build the row was produced from.
66updated_atdatealwaysThe date the row last changed, YYYY-MM-DD.

A field that is "when known" is empty in CSV and null in JSON when there is no value.

The row key

pattern is the part of a VIN that names the vehicle's make, model and model year:

TEXT
VIN       2 T 3 W 1 R F V 3 S C 7 7 6 9 3 1
position  1 2 3 4 5 6 7 8 9 10 11 ...
pattern   2 T 3 W 1 R F V * S C

Positions 1 to 8, an asterisk in place of the check digit at position 9, then positions 10 and 11. When position 3 is 9, positions 12 to 14 are added, so the pattern is 14 characters. Every VIN that shares a pattern shares a row.

Which rows a feed covers

A manufacturer's feed covers the rows Cardog's identity record names as that manufacturer's makes. Scope is set by Cardog from who the organization is. It is not a setting.

Create and configure a feed

In your organization's workspace, open Feeds and choose New feed. An owner or an admin can create and change a feed.

Customization is a projection over the dataset:

  • Choose fields. Include any of the 66.
  • Rename columns. A column name starts with a letter and may use letters, digits, spaces, _, . and -, up to 64 characters.
  • Reorder columns. pattern is the row key and stays column one.

A projection selects, renames and reorders. It never computes a value.

If you save no projection, the feed carries every field under its own name, in the order of the table above.

Formats

FormatFilesNotes
CSVfull.csv, delta.csv, deletes.csvOne row per line.
JSON Linesfull.jsonl, delta.jsonl, deletes.jsonlOne JSON object per line.
JSONfull.json, delta.json, deletes.jsonOne JSON array.

Each format is available plain or gzipped. A gzipped file adds .gz to its name.

CSV has three more settings:

SettingChoicesDefault
Delimitercomma, pipe, tab, semicoloncomma
Header lineon, offon
Line endingLF, CRLFLF

A CSV value that contains the delimiter or a double quote is wrapped in double quotes, and a quote inside it is doubled (RFC 4180).

In JSON Lines and JSON, the keys are your column names in column order. Integers are numbers and an empty value is null.

The nightly run

Every night the feed is rebuilt from the identity record and compared with the run that is current. The result is a run:

FileHolds
fullEvery row in the feed.
deltaThe rows that are new or changed since the current run.
deletesThe keys that were removed since the current run. Written only when keys were removed.

Each file has a record count, a size in bytes and a SHA-256.

The run id is the SHA-256 of the uncompressed full file. The same data is the same run, whether or not the file is gzipped.

A pointer named "latest" says which run is current. It moves only after every file of the run is written. A reader never sees a run that is half written.

The first run of a feed has nothing to compare with, so its delta file has no rows and its manifest says kind: "full". The same is true of the first run after you change the feed's columns, format or CSV settings: files written under one shape are not compared with files written under another.

The daily record

Every night is recorded with its outcome, whatever happened:

OutcomeWhat happenedWhat is current afterwards
publishedThe run was written and the latest pointer moved to it.The new run.
unchangedThe data is identical to the current run, so the run id is the same.The same run.
heldThe run changes more rows than the limit allows. It is written and held for review.The previous run.
refusedThe run removes too large a share of the rows. It is not published.The previous run.
failedThe run did not complete.The previous run.

Held and refused are checks on our side, not errors on yours. A run that fails a check is never handed to you as the current run. You keep reading the previous run until a run is published.

The profile

Each run carries a profile of its full file: how many rows have a value in each field, and breakdowns by make, model year, vehicle type, fuel type, status and source. The workspace shows it with the feed.

Read a feed

In the workspace

Open Feeds, choose the feed, then choose a run. Each file has a download link. A link is signed and good for 24 hours.

With the API

Feeds are read with the organization's key, which starts with crdg_org_. Create one under Developer in the organization's workspace. A personal key or a session is 403 forbidden. Every feed read costs 0 credits.

Bash
curl "https://api.cardog.app/v2/feeds" \
  -H "x-api-key: $CARDOG_ORG_KEY"

Example

1 keys
"response": {
"feeds": [
0: {2 items}
]
}

GET /v2/feeds lists the organization's active feeds. latest is null until a run has been published.

Bash
curl "https://api.cardog.app/v2/feeds/vehicle-identity@1/latest" \
  -H "x-api-key: $CARDOG_ORG_KEY"

Example

6 keys
"response": {
"contract": {
"dataset": "vehicle-identity@1",
"fieldmap": "recall-tool@1",
"format": "csv.gz",
"status": "active",
"cadence": {3 items}
},
"dataset": {
"id": "vehicle-identity@1",
"title": "Vehicle identity",
"status": "draft",
"semantics": {2 items},
"fields": [1 item]
},
"manifest": {
"schema": "feed-manifest@1",
"dataset": "vehicle-identity@1",
"runId": "9f2c4e1b…",
"kind": "delta",
"baselineRunId": "41d07a3c…",
"artifacts": [3 items],
"producedAt": "2026-10-02T06:00:00.000Z"
},
"artifacts": [
0: {6 items}
],
"columns": [
0: {3 items},
1: {3 items},
2: {3 items}
],
"layout": {
"delimiter": ",",
"header": true,
"lineEnding": "lf"
}
}

The example is shortened: one dataset field, one artifact link and three columns are shown. Its counts and hashes are illustrative.

  • manifest describes the run. kind is delta when the run was compared with an earlier one, and baselineRunId names that run. kind is full when there was nothing to compare with.
  • artifacts is one entry per file, each with a signed url.
  • columns is the files' columns in order: the name you chose, the dataset field behind it, and its type.
  • layout is how a CSV file was written.
  • dataset is the declaration the rows are read by.

columns and layout describe the run, not the feed's current settings. A feed edited since still reads the files it wrote.

The file

Bash
curl "$ARTIFACT_URL" -o full.csv.gz

Open an artifact url exactly as printed. It needs no API key and costs 0 credits. It is good for 24 hours. After that, or if it has been altered, it answers 403 forbidden. Request /latest again for a fresh link. Do not store the link; store the run id.

StatusWhen
400 invalid_requestThe dataset id is not name@version.
403 forbiddenThe credential is not an organization key, or the artifact link is expired or not valid.
404 not_foundThe organization has no active feed for that dataset, or no run has been published yet.
503 unavailableThe run or the file could not be read right now. Retry with backoff.

Read a feed with the SDK

client.v2.feeds is in @cardog/api. Construct the client with the organization's key.

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

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

const { feeds } = await client.v2.feeds.list();
for (const feed of feeds) {
  console.log(feed.contract.dataset, feed.contract.format, feed.latest?.runId ?? "no run yet");
}

const run = await client.v2.feeds.latest("vehicle-identity@1");
console.log(run.runId, run.manifest.kind, run.columns.map((c) => c.name));

latest() returns a run. These are the ways to read it:

MemberReturns
run.rows(role)An async iterator of rows, streamed. The file is never held in memory.
run.toJSON(role)Every row as an array.
run.index()The full file in memory, keyed by pattern, with lookup(vin).
run.columnsThe files' columns in order: name, source, type.
run.manifestThe run's manifest.
run.artifact(role)The file's entry (name, records, bytes, sha256, url, expiresAt), or null when the run has no such file.
run.runIdThe run id.

role is "full" (the default), "delta" or "deletes".

Rows are objects keyed by your column names. Integers are numbers and an empty value is null. Gzip, the delimiter, quoting and the header are handled for you, in the browser and in Node, with web streams only.

TypeScript
for await (const row of run.rows()) {
  console.log(row.pattern, row.make, row.model_year);
}

const all = await run.toJSON();

A row from a deletes file carries the key column alone.

Answer a VIN from the feed

TypeScript
const index = await run.index();

const row = index.lookup("2T3W1RFV3SC776931");
if (row === null) {
  // The pattern is not in the feed. Say the VIN is not recognized.
} else {
  console.log(row.make, row.model, row.model_year); // "Toyota" "RAV4" 2025
}

lookup(vin) masks the VIN to its pattern and returns the row, or null when the pattern is not in the feed. It throws only when the string is not a well-formed VIN. index.get(pattern) takes a pattern directly, and index.size is the number of rows.

The index is keyed by the first column, so it works whatever you named your columns. The fields on the row are the columns you chose, under the names you gave them.

Without the SDK, build the pattern yourself and look it up in your own store:

TypeScript
function patternOf(vin: string): string {
  const v = vin.trim().toUpperCase();
  const tail = v[2] === "9" ? v.slice(11, 14) : "";
  return v.slice(0, 8) + "*" + v.slice(9, 11) + tail;
}

nanoFromVin in @cardog/entities is the same rule with VIN validation, and it is what the SDK calls.

Why this shape

Canada's Motor Vehicle Safety Regulations section 15.05 requires a designated company's VIN recall lookup tool to return make, model and model year for a VIN and to say when a VIN is not recognized. A feed is reference data that answers that: a row gives the three values, and null is the VIN that is not recognized.

Every row for a VIN

A recalls feed holds several rows for one pattern. lookupAll returns all of them, and an empty list when the pattern is not on the record.

TypeScript
const recalls = await client.v2.feeds.latest("vehicle-recalls@1");
const index = await recalls.index();
const campaigns = index.lookupAll("2T3W1RFV3SC776931");
// [{ pattern, recall_ref, authority, campaign_number, recall_status, … }, …]

Keep your own store current

One call brings your copy up to the current run and tells you where you now stand. Keep the run id it returns and pass it back next time.

TypeScript
type Row = Record<string, string | number | boolean | null>;

// Your own storage. These five calls are yours to write.
declare const store: {
  lastRunId(): Promise<string | null>;
  clear(): Promise<void>;
  upsert(key: string, row: Row): Promise<void>;
  remove(key: string): Promise<void>;
  saveRunId(runId: string): Promise<void>;
};

const result = await client.v2.feeds.sync("vehicle-identity@1", {
  since: await store.lastRunId(),
  reset: () => store.clear(),
  upsert: (row) => store.upsert(String(Object.values(row)[0]), row),
  remove: (key) => store.remove(key),
});

await store.saveRunId(result.runId);
result.mode; // "full", "delta" or "unchanged"

What sync does with the run:

modeWhenWhat is called
unchangedThe current run is the one you hold.Nothing.
deltaThe current run was built on top of exactly the run you hold.remove for every key that left, then upsert for every new or changed row.
fullYour first sync, a night you missed, or a change to the feed's columns or format.reset, then upsert for every row.

The rules behind it:

  • A delta row is a whole row. Insert it if the key is new, replace it if the key exists.
  • A deletes row is a key. Remove that row.
  • A delta applies to exactly one earlier run, the one baselineRunId names. If that is not the run you hold, sync applies the full file instead. It needs reset for that, so rows that have left are not kept.
  • Every file is checked against the SHA-256 in the manifest before a row is applied. A file that does not match throws FeedIntegrityError and none of your handlers is called.
  • Save the run id only after sync returns. If your process stops part way, the next sync with the old id applies the same run again, and applying it twice is harmless.

To ask only whether there is anything new:

TypeScript
const current = await client.v2.feeds.currentRunId("vehicle-identity@1");
if (current !== (await store.lastRunId())) {
  // a new run is waiting
}

Verify a file

Two checks, both with tools you already have.

Bash
# 1. The file you downloaded is the file the run recorded.
shasum -a 256 full.csv.gz
# equals artifacts[].sha256 for full.csv.gz

# 2. The full file is the run.
gunzip -c full.csv.gz | shasum -a 256
# equals manifest.runId

artifacts[].sha256 is the hash of the file as served, so for a gzipped file it is the hash of the gzipped bytes. The run id is the hash of the full file uncompressed. For a feed that is not gzipped the two are the same value.

artifacts[].records is the number of rows in the file, not counting a CSV header line. Compare it with what you loaded.

TypeScript
const full = run.artifact("full");
const rows = await run.toJSON();
if (full === null || rows.length !== full.records) {
  throw new Error("row count does not match the run");
}

Not yet

Push delivery to your own SFTP server, S3 bucket or webhook is coming. A destination can be saved in the workspace today and shows as pending its first delivery. Until then a feed is read by pulling it, in any of the three ways above.